Получение данных

Nuxt предоставляет композаблы для получения данных в приложении.

Nuxt поставляется с двумя композаблами и встроенной библиотекой для получения данных в браузере или на сервере: useFetch, useAsyncData и $fetch.

В двух словах:

И useFetch, и useAsyncData имеют общий набор опций и паттернов, которые мы подробно рассмотрим в следующих разделах.

Зачем нужны useFetch и useAsyncData

Nuxt выполняет изоморфный (универсальный) код и на сервере, и в браузере. Если в setup компонента вызывать только $fetch, запрос может уйти дважды: при SSR для HTML и снова при гидратации на клиенте — отсюда риски рассинхрона, лишняя задержка до интерактива и непредсказуемое поведение.

Композаблы useFetch и useAsyncData решают это: если данные уже получены на сервере, они попадают в полезную нагрузку (payload) и на клиенте повторный запрос не делается.

Полезная нагрузка — это объект JavaScript, доступный через useNuxtApp().payload. Он используется на клиенте, чтобы избежать повторного запроса одних и тех же данных при выполнении кода в браузере во время гидратации.

Используйте Nuxt DevTools: на вкладке Payload видна сериализованная полезная нагрузка.
app.vue
<script setup lang="ts">
const { data } = await useFetch('/api/data')

async function handleFormSubmit () {
  const res = await $fetch('/api/submit', {
    method: 'POST',
    body: {
      // My form data
    },
  })
}
</script>

<template>
  <div v-if="data == null">
    No data
  </div>
  <div v-else>
    <form @submit="handleFormSubmit">
      <!-- form input tags -->
    </form>
  </div>
</template>

В примере выше useFetch выполнит запрос на сервере и передаст результат в браузер; у $fetch такого механизма нет — его уместнее использовать для запросов только с клиента (например, по событию).

Suspense

Под капотом Nuxt использует <Suspense> Vue, чтобы не завершать навигацию, пока асинхронные данные для экрана не готовы. Композаблы загрузки данных подстраиваются под это поведение; конкретные опции задаются на каждый вызов.

Можно добавить <NuxtLoadingIndicator>, чтобы показывать полосу прогресса между переходами по страницам.

$fetch

Nuxt использует библиотеку ofetch, которая автоматически импортируется как псевдоним $fetch во всем приложении.

pages/todos.vue
<script setup lang="ts">
async function addTodo () {
  const todo = await $fetch('/api/todos', {
    method: 'POST',
    body: {
      // My todo data
    },
  })
}
</script>
Помните, что использование только $fetch не обеспечит дедупликацию сетевых вызовов и предотвращение навигации.
Рекомендуется использовать $fetch для взаимодействия на стороне клиента (event-based) или в сочетании с useAsyncData при получении исходных данных компонента.
Узнайте больше о $fetch.

Передача клиентских заголовков в API

При вызове useFetch на сервере Nuxt использует useRequestFetch и проксирует заголовки и куки клиента (кроме тех, что не предназначены для проброса, например host).

<script setup lang="ts">
const { data } = await useFetch('/api/echo')
</script>
// /api/echo.ts
export default defineEventHandler(event => parseCookies(event))

Ниже — как вручную взять useRequestHeaders и передать куки в API с серверной части запроса, пришедшего от клиента. Изоморфный $fetch с теми же заголовками даёт эндпоинту тот же cookie, что и у браузера. Это нужно, если вы не используете useFetch.

<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])

async function getCurrentUser () {
  return await $fetch('/api/me', { headers })
}
</script>
Также можно вызвать useRequestFetch — заголовки к запросу подставятся автоматически.
Будьте очень осторожны, прежде чем проксировать заголовки на внешний API, и включайте только те заголовки, которые вам нужны. Не все заголовки безопасны, некоторые могут привести к нежелательному поведению. Вот список распространенных заголовков, которые НЕ следует проксировать:
  • host, accept
  • content-length, content-md5, content-type
  • x-forwarded-host, x-forwarded-port, x-forwarded-proto
  • cf-connecting-ip, cf-ray

useFetch

Композабл useFetch использует $fetch под капотом для выполнения SSR-безопасных сетевых вызовов в функции setup.

app.vue
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>

<template>
  <p>Page visits: {{ count }}</p>
</template>

Этот композабл — обёртка над useAsyncData и утилитой $fetch.

Узнать больше Docs > 3 X > API > Composables > Use Fetch.
Прочитайте и отредактируйте живой пример в Docs > 3 X > Examples > Features > Data Fetching.

useAsyncData

Композабл useAsyncData оборачивает асинхронную логику и возвращает результат после его завершения.

useFetch(url) почти эквивалентно useAsyncData(url, () => event.$fetch(url)).
Это сахар для типичных сценариев разработки. Подробнее о event.fetch см. в useRequestFetch.

Бывают случаи, когда использование композабла useFetch не подходит, например, когда CMS или сторонние разработчики предоставляют свой собственный слой запросов. В этом случае вы можете использовать useAsyncData, чтобы обернуть ваши вызовы и сохранить преимущества, предоставляемые композаблами.

pages/users.vue
<script setup lang="ts">
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))

// This is also possible:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>
Первый аргумент useAsyncData — уникальный ключ для кэширования ответа второго аргумента (функции запроса). Этот ключ можно игнорировать, передавая напрямую функцию запроса, ключ будет сгенерирован автоматически.

Поскольку авто-генерируемый ключ учитывает только файл и строку, в которой вызывается useAsyncData, рекомендуется всегда создавать свой собственный ключ, чтобы избежать нежелательного поведения, например, при создании собственной обертки над useAsyncData.

Установка ключа может быть полезна для обмена одними и теми же данными между компонентами с помощью useNuxtData или для обновления выбранных данных.
pages/users/[id].vue
<script setup lang="ts">
const { id } = useRoute().params

const { data, error } = await useAsyncData(`user:${id}`, () => {
  return myGetFunction('users', { id })
})
</script>

Композабл useAsyncData - это отличный способ обернуть и дождаться завершения нескольких запросов $fetch, а затем обработать результаты.

<script setup lang="ts">
const { data: discounts, status } = await useAsyncData('cart-discount', async (_nuxtApp, { signal }) => {
  const [coupons, offers] = await Promise.all([
    $fetch('/cart/coupons', { signal }),
    $fetch('/cart/offers', { signal }),
  ])

  return { coupons, offers }
})
// discounts.value.coupons
// discounts.value.offers
</script>
useAsyncData предназначен для загрузки и кэширования данных, а не для побочных эффектов вроде вызова экшенов Pinia — иначе возможны повторные запуски и странное поведение с пустыми значениями. Для побочных эффектов используйте callOnce.
<script setup lang="ts">
const offersStore = useOffersStore()

// you can't do this
await useAsyncData(() => offersStore.getOffer(route.params.slug))
</script>
Узнайте больше о useAsyncData.

Возвращаемые значения

useFetch и useAsyncData имеют одинаковые возвращаемые значения, перечисленные ниже.

  • data: результат работы переданной асинхронной функции.
  • refresh/execute: функция, которая может быть использована для обновления данных, возвращенных функцией handler.
  • clear: сбрасывает data в undefined (или в значение из options.default(), если оно задано), error в null, status в idle и помечает текущие запросы отменёнными.
  • error: объект ошибки, если получение данных не удалось.
  • status: строка, указывающая на статус запроса данных ("idle", "pending", "success", "error").
data, error и status — это ref из Vue, к значениям которых в <script setup> обращаются через .value.

По умолчанию Nuxt ждёт завершения refresh, прежде чем его можно снова вызвать.

Если вы не получили данные на сервере (например, с помощью server: false), то данные не будут получены до завершения гидратации. Это означает, что даже если вы ожидаете useFetch на стороне клиента, data останется null внутри <script setup>.

Параметры

useAsyncData и useFetch возвращают один и тот же тип объекта и принимают общий набор опций в качестве последнего аргумента. С их помощью можно управлять поведением композаблов, например, блокировкой навигации, кэшированием или выполнением.

Отложенная загрузка

По умолчанию композаблы, выполняющие получение данных, ждут завершения своей асинхронной функции перед переходом на новую страницу через встроенный в Vue <Suspense>. Эту возможность можно игнорировать при навигации на стороне клиента с помощью опции lazy. В этом случае вам придется вручную обрабатывать состояние загрузки, используя значение status.

app.vue
<script setup lang="ts">
const { status, data: posts } = useFetch('/api/posts', {
  lazy: true,
})
</script>

<template>
  <!-- you will need to handle a loading state -->
  <div v-if="status === 'pending'">
    Loading ...
  </div>
  <div v-else>
    <div v-for="post in posts">
      <!-- do something -->
    </div>
  </div>
</template>

В качестве альтернативы вы можете использовать useLazyFetch и useLazyAsyncData как удобные методы для выполнения того же самого.

<script setup lang="ts">
const { status, data: posts } = useLazyFetch('/api/posts')
</script>
Узнайте больше о useLazyFetch.
Узнайте больше о useLazyAsyncData.

Получение данных только на клиенте

По умолчанию композаблы для получения данных будут выполнять свою асинхронную функцию как на клиенте, так и на сервере. Установите опцию server в значение false, чтобы выполнять вызов только на стороне клиента. При первоначальной загрузке данные не будут извлечены до завершения гидратации, поэтому вам придется обрабатывать состояние ожидания, хотя при последующей навигации на стороне клиента данные будут загружены перед загрузкой страницы.

В сочетании с опцией lazy это может быть полезно для данных, которые не нужны при первом рендере (например, данные, не относящиеся к SEO).

/* This call is performed before hydration */
const articles = await useFetch('/api/article')

/* This call will only be performed on the client */
const { status, data: comments } = useFetch('/api/comments', {
  lazy: true,
  server: false,
})

Композабл useFetch предназначен для вызова в методе setup или непосредственно на верхнем уровне функции в хуках жизненного цикла, в противном случае следует использовать метод $fetch.

Минимизация размера полезной нагрузки

Опция pick позволяет минимизировать размер полезной нагрузки, хранящейся в HTML-документе, выбирая только те поля, которые вы хотите вернуть из композаблов.

<script setup lang="ts">
/* only pick the fields used in your template */
const { data: mountain } = await useFetch('/api/mountains/everest', {
  pick: ['title', 'description'],
})
</script>

<template>
  <h1>{{ mountain.title }}</h1>
  <p>{{ mountain.description }}</p>
</template>

Если вам нужно больше контроля или отображение нескольких объектов, вы можете использовать функцию transform для изменения результата запроса.

const { data: mountains } = await useFetch('/api/mountains', {
  transform: (mountains) => {
    return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))
  },
})
И pick, и transform не предотвращают появление ненужных данных в самом начале. Но они предотвращают их добавление в полезную нагрузку, передаваемую от сервера к клиенту.

Кэширование и повторное получение данных

Ключи

useFetch и useAsyncData используют ключи для предотвращения повторного запроса одних и тех же данных.

  • useFetch использует переданный URL в качестве ключа. Альтернативно ключ можно задать в объекте options последнего аргумента.
  • useAsyncData использует свой первый аргумент в качестве ключа, если он является строкой. Если первым аргументом является функция-обработчик, выполняющая запрос, то для вас будет сгенерирован ключ, уникальный для имени файла и номера строки экземпляра useAsyncData.
Чтобы получить кэшированные данные по ключу, вы можете использовать useNuxtData.

Общее состояние и согласованность опций

Если несколько компонентов вызывают useAsyncData или useFetch с одним и тем же ключом, они разделяют одни и те же ref data, error и status. Это удобно, но часть опций при этом должна совпадать.

Следующие опции обязаны совпадать у всех вызовов с одним ключом:

  • функция handler
  • опция deep
  • функция transform
  • массив pick
  • функция getCachedData
  • значение default
// ❌ This will trigger a development warning
const { data: users1 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { deep: false })
const { data: users2 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { deep: true })

Эти опции могут отличаться без предупреждений:

  • server
  • lazy
  • immediate
  • dedupe
  • watch
// ✅ This is allowed
const { data: users1 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { immediate: true })
const { data: users2 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { immediate: false })

Для независимых экземпляров используйте разные ключи:

// These are completely independent instances
const { data: users1 } = useAsyncData('users-1', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))
const { data: users2 } = useAsyncData('users-2', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))

Реактивные ключи

Ключом может быть computed, обычный ref или геттер — данные будут перезапрашиваться при изменении зависимостей:

// Using a computed property as a key
const userId = ref('123')
const { data: user } = useAsyncData(
  computed(() => `user-${userId.value}`),
  () => fetchUser(userId.value),
)

// When userId changes, the data will be automatically refetched
// and the old data will be cleaned up if no other components use it
userId.value = '456'

Обновить и выполнить

Чтобы получить или обновить данные вручную, вызовите execute или refresh из возвращаемого композаблом объекта.

<script setup lang="ts">
const { data, error, execute, refresh } = await useFetch('/api/users')
</script>

<template>
  <div>
    <p>{{ data }}</p>
    <button @click="() => refresh()">
      Refresh data
    </button>
  </div>
</template>

execute — псевдоним refresh с тем же поведением; удобнее, когда выборка не выполняется сразу.

Для глобального повторного запроса или инвалидации кэшированных данных см. clearNuxtData и refreshNuxtData.

Очистка

Если вы хотите очистить предоставленные данные по какой-либо причине, не зная конкретного ключа, который нужно передать в clearNuxtData, вы можете использовать функцию clear, предоставляемую композаблом.

<script setup lang="ts">
const { data, clear } = await useFetch('/api/users')

const route = useRoute()
watch(() => route.path, (path) => {
  if (path === '/') {
    clear()
  }
})
</script>

Наблюдение

Чтобы повторно запускать функцию получения данных при каждом изменении других реактивных значений в вашем приложении, используйте опцию watch. Вы можете использовать ее для одного или нескольких наблюдаемых элементов.

<script setup lang="ts">
const id = ref(1)

const { data, error, refresh } = await useFetch('/api/users', {
  /* Changing the id will trigger a refetch */
  watch: [id],
})
</script>

Обратите внимание, что наблюдение за реактивным значением не изменит получаемый URL. Например, будет продолжена выборка того же начального ID пользователя, потому что URL строится в момент вызова функции.

<script setup lang="ts">
const id = ref(1)

const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
  watch: [id],
})
</script>

Если вам нужно изменить URL на основе реактивного значения, вместо него лучше использовать вычисляемый URL.

Если переданы реактивные опции выборки, Nuxt следит за ними и вызывает повторную загрузку. Иногда удобно отключить это поведение, указав watch: false.

const id = ref(1)

// Won't automatically refetch when id changes
const { data, execute } = await useFetch('/api/users', {
  query: { id }, // id is watched by default
  watch: false, // disables automatic watching of id
})

// doesn't trigger refetch
id.value = 2

Вычисляемый URL

Иногда вам может потребоваться вычислить URL из реактивных значений и обновлять данные каждый раз, когда они меняются. Вместо того чтобы жонглировать данными, вы можете прикрепить каждый параметр как реактивное значение. Nuxt будет автоматически использовать реактивное значение и обновлять данные при каждом его изменении.

<script setup lang="ts">
const id = ref(null)

const { data, status } = useLazyFetch('/api/user', {
  query: {
    user_id: id,
  },
})
</script>

В случае более сложного построения URL можно использовать обратный вызов в качестве вычисляемого геттера, который возвращает строку URL.

При каждом изменении зависимости данные будут извлекаться по новому построенному URL. В сочетании с режимом без немедленного запроса вы можете дождаться изменения реактивного значения перед первой загрузкой.

<script setup lang="ts">
const id = ref(null)

const { data, status } = useLazyFetch(() => `/api/users/${id.value}`, {
  immediate: false,
})

const pending = computed(() => status.value === 'pending')
</script>

<template>
  <div>
    <!-- disable the input while fetching -->
    <input
      v-model="id"
      type="number"
      :disabled="pending"
    >

    <div v-if="status === 'idle'">
      Type an user ID
    </div>

    <div v-else-if="pending">
      Loading ...
    </div>

    <div v-else>
      {{ data }}
    </div>
  </div>
</template>

Если вам нужно принудительно обновлять данные при изменении других реактивных значений, вы также можете наблюдать за другими значениями.

Не немедленно

Композабл useFetch начнет получать данные в момент вызова. Вы можете предотвратить это, установив immediate: false, например, чтобы дождаться взаимодействия с пользователем.

Таким образом, вам понадобится status для обработки жизненного цикла выборки и execute для запуска выборки данных.

<script setup lang="ts">
const { data, error, execute, status } = await useLazyFetch('/api/comments', {
  immediate: false,
})
</script>

<template>
  <div v-if="status === 'idle'">
    <button @click="execute">
      Get data
    </button>
  </div>

  <div v-else-if="status === 'pending'">
    Loading comments...
  </div>

  <div v-else>
    {{ data }}
  </div>
</template>

Для более точного контроля переменная status может быть:

  • idle, когда получение данных еще не началось
  • pending, когда получение данных началось, но еще не завершилось
  • error, когда получение данных завершилось неудачно
  • success, когда получение данных завершилось успешно

Передача заголовков и куки

В браузере вызов $fetch отправляет на API заголовки пользователя, в том числе cookie. При SSR запрос $fetch идёт «внутри» сервера и по умолчанию не содержит куки из браузера и не пробрасывает Set-Cookie из ответа наружу.

Обычно при SSR из соображений безопасности куки браузера в $fetch не попадают.

Если же на сервере вызывается useFetch с относительным URL, Nuxt подставляет useRequestFetch и проксирует заголовки и куки (кроме не предназначенных для проброса, например host).

Передача куки из серверных вызовов API в SSR-ответ

Если вы хотите передавать/проксировать куки в другом направлении - от внутреннего запроса обратно клиенту - вам нужно будет сделать это самостоятельно.

composables/fetch.ts
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'

export const fetchWithCookie = async (event: H3Event, url: string) => {
  /* Get the response from the server endpoint */
  const res = await $fetch.raw(url)
  /* Get the cookies from the response */
  const cookies = res.headers.getSetCookie()
  /* Attach each cookie to our incoming Request */
  for (const cookie of cookies) {
    appendResponseHeader(event, 'set-cookie', cookie)
  }
  /* Return the data of the response */
  return res._data
}
<script setup lang="ts">
// This composable will automatically pass cookies to the client
const event = useRequestEvent()

const { data: result } = await useAsyncData(() => fetchWithCookie(event!, '/api/with-cookie'))

onMounted(() => console.log(document.cookie))
</script>

Поддержка Options API

Nuxt предоставляет возможность выполнять asyncData в Options API. Для этого вы должны обернуть определение вашего компонента в defineNuxtComponent.

<script>
export default defineNuxtComponent({
  /* Use the fetchKey option to provide a unique key */
  fetchKey: 'hello',
  async asyncData () {
    return {
      hello: await $fetch('/api/hello'),
    }
  },
})
</script>
Рекомендуется объявлять компоненты Vue в Nuxt через <script setup> или <script setup lang="ts">.
Узнать больше Docs > 3 X > API > Utils > Define Nuxt Component.

Сериализация данных с сервера на клиент

При использовании useAsyncData и useLazyAsyncData для передачи данных, полученных на сервере, клиенту (а также всего остального, что использует полезную нагрузку Nuxt), полезная нагрузка сериализуется с devalue. Это позволяет нам передавать не только базовый JSON, но и сериализовывать и «оживить»/десериализовывать более сложные виды данных, такие как регулярные выражения, даты, Map и Set, ref, reactive, shallowRef, shallowReactive и NuxtError — и многое другое.

Также можно определить свой собственный сериализатор/десериализатор для типов, которые не поддерживаются Nuxt. Подробнее об этом можно прочитать в документации useNuxtApp.

Обратите внимание: это не относится к данным из ваших серверных маршрутов при запросах через $fetch или useFetch — подробности в следующем разделе.

Сериализация данных из маршрутов API

При получении данных из директории server ответ сериализуется с помощью JSON.stringify. Однако, поскольку сериализация ограничена только примитивными типами JavaScript, Nuxt делает все возможное, чтобы преобразовать возвращаемый тип $fetch и useFetch для соответствия реальному значению.

Узнайте больше об ограничениях JSON.stringify.

Пример

server/api/foo.ts
export default defineEventHandler(() => {
  return new Date()
})
app.vue
<script setup lang="ts">
// Type of `data` is inferred as string even though we returned a Date object
const { data } = await useFetch('/api/foo')
</script>

Пользовательская функция сериализатора

Чтобы настроить поведение сериализации, вы можете определить функцию toJSON для возвращаемого объекта. Если вы определите метод toJSON, Nuxt учтёт возвращаемый тип функции и не будет пытаться преобразовать типы.

server/api/bar.ts
export default defineEventHandler(() => {
  const data = {
    createdAt: new Date(),

    toJSON () {
      return {
        createdAt: {
          year: this.createdAt.getFullYear(),
          month: this.createdAt.getMonth(),
          day: this.createdAt.getDate(),
        },
      }
    },
  }
  return data
})
app.vue
<script setup lang="ts">
// Type of `data` is inferred as
// {
//   createdAt: {
//     year: number
//     month: number
//     day: number
//   }
// }
const { data } = await useFetch('/api/bar')
</script>

Использование альтернативного сериализатора

В настоящее время Nuxt не поддерживает сериализатор, альтернативный JSON.stringify. Однако вы можете возвращать полезную нагрузку в виде обычной строки и использовать метод toJSON для сохранения безопасности типов.

В примере ниже мы используем superjson в качестве сериализатора.

server/api/superjson.ts
import superjson from 'superjson'

export default defineEventHandler(() => {
  const data = {
    createdAt: new Date(),

    // Workaround the type conversion
    toJSON () {
      return this
    },
  }

  // Serialize the output to string, using superjson
  return superjson.stringify(data) as unknown as typeof data
})
app.vue
<script setup lang="ts">
import superjson from 'superjson'

// `date` is inferred as { createdAt: Date } and you can safely use the Date object methods
const { data } = await useFetch('/api/superjson', {
  transform: (value) => {
    return superjson.parse(value as unknown as string)
  },
})
</script>

Рецепты

Использование SSE (Server-Sent Events) через POST-запрос

Если вы используете SSE через GET-запрос, вы можете использовать EventSource или композабл VueUse useEventSource.

При использовании SSE через POST-запрос вам необходимо вручную обработать соединение. Вот как это можно сделать:

// Make a POST request to the SSE endpoint
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
  method: 'POST',
  body: {
    query: 'Hello AI, how are you?',
  },
  responseType: 'stream',
})

// Create a new ReadableStream from the response with TextDecoderStream to get the data as text
const reader = response.pipeThrough(new TextDecoderStream()).getReader()

// Read the chunk of data as we get it
while (true) {
  const { value, done } = await reader.read()

  if (done) { break }

  console.log('Received:', value)
}

Параллельные запросы

Если запросы не зависят друг от друга, их можно выполнить параллельно через Promise.all() и ускорить загрузку.

const { data } = await useAsyncData((_nuxtApp, { signal }) => {
  return Promise.all([
    $fetch('/api/comments/', { signal }),
    $fetch('/api/author/12', { signal }),
  ])
})

const comments = computed(() => data.value?.[0])
const author = computed(() => data.value?.[1])