Получение данных
Nuxt поставляется с двумя композаблами и встроенной библиотекой для получения данных в браузере или на сервере: useFetch, useAsyncData и $fetch.
В двух словах:
- $fetchis the simplest way to make a network request.
- useFetchis wrapper around- $fetchthat fetches data only once in universal rendering.
- useAsyncDatais similar to- useFetchbut offers more fine-grained control.
И useFetch, и useAsyncData имеют общий набор опций и паттернов, которые мы подробно рассмотрим в следующих разделах.
The need for useFetch and useAsyncData
Nuxt - это фреймворк, который может выполнять изоморфный (или универсальный) код как в серверном, так и в клиентском окружениях. Если функция $fetch используется для получения данных в функции setup компонента Vue, это может привести к тому, что данные будут получены дважды, один раз на сервере (чтобы отрендерить HTML) и еще раз на клиенте (когда HTML будет гидратирован). Именно поэтому Nuxt предлагает специальные композаблы для получения данных, чтобы данные запрашивались только один раз.
Nuxt is a framework which can run isomorphic (or universal) code in both server and client environments. If the $fetch function is used to perform data fetching in the setup function of a Vue component, this may cause data to be fetched twice, once on the server (to render the HTML) and once again on the client (when the HTML is hydrated). This can cause hydration issues, increase the time to interactivity and cause unpredictable behavior.
The useFetch and useAsyncData composables solve this problem by ensuring that if an API call is made on the server, the data is forwarded to the client in the payload.
Полезная нагрузка - это объект JavaScript, доступный через useNuxtApp().payload. Он используется на клиенте, чтобы избежать повторного запроса одних и тех же данных при выполнении кода в браузере во время гидратации.
<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>
In the example above, useFetch would make sure that the request would occur in the server and is properly forwarded to the browser. $fetch has no such mechanism and is a better option to use when the request is solely made from the browser.
Suspense
Nuxt uses Vue’s <Suspense> component under the hood to prevent navigation before every async data is available to the view. The data fetching composables can help you leverage this feature and use what suits best on a per-call basis.
<NuxtLoadingIndicator> to add a progress bar between page navigations.$fetch
Nuxt использует библиотеку ofetch, которая автоматически импортируется как псевдоним $fetch во всем приложении.
<script setup lang="ts">
async function addTodo() {
  const todo = await $fetch('/api/todos', {
    method: 'POST',
    body: {
      // todo данные
    }
  })
}
</script>
$fetch не обеспечит дедупликацию сетевых вызовов и предотвращение навигации. Рекомендуется использовать
$fetch для взаимодействия на стороне клиента (на основе событий) или в сочетании с useAsyncData при получении исходных данных компонента.Передача клиентских заголовков в API
Во время рендеринга на сервере, поскольку запрос $fetch выполняется "внутри" сервера, он не будет включать файлы cookie браузера пользователя.
Мы можем использовать useRequestHeaders для доступа и проксирования файлов cookie в API со стороны сервера.
В приведенном ниже примере заголовки запроса добавляются к изоморфному вызову $fetch, чтобы гарантировать, что эндпоинт API имеет доступ к тому же заголовку cookie, который изначально был отправлен пользователем.
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
async function getCurrentUser() {
  return await $fetch('/api/me', { headers: headers.value })
}
</script>
- host,- accept
- content-length,- content-md5,- content-type
- x-forwarded-host,- x-forwarded-port,- x-forwarded-proto
- cf-connecting-ip,- cf-ray
useRequestFetch для автоматической передачи заголовков вызову.useFetch
Композабл useFetch использует $fetch под капотом для выполнения SSR-безопасных сетевых вызовов в функции setup.
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>
<template>
  <p>Посещения страницы: {{ count }}</p>
</template>
Этот композабл представляет собой обертку вокруг композабла useAsyncData и утилиты $fetch.
useAsyncData
Композабл useAsyncData отвечает за обертывание асинхронной логики и возврат результата после его разрешения.
useFetch(url) почти эквивалентно useAsyncData(url, () => event.$fetch(url)). Это сахар для разработчиков для наиболее распространенных случаев использования. (You can find out more about
event.fetch at useRequestFetch.)Бывают случаи, когда использование композабла useFetch не подходит, например, когда CMS или сторонние разработчики предоставляют свой собственный слой запросов. В этом случае вы можете использовать useAsyncData, чтобы обернуть ваши вызовы и сохранить преимущества, предоставляемые композаблами.
<script setup lang="ts">
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))
// Так тоже можно:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>
useAsyncData - это уникальный ключ, используемый для кэширования ответа второго аргумента, функции запроса. Этот ключ можно игнорировать, передавая напрямую функцию запроса, ключ будет сгенерирован автоматически.
Поскольку авто-генерируемый ключ учитывает только файл и строку, в которой вызывается
useAsyncData, рекомендуется всегда создавать свой собственный ключ, чтобы избежать нежелательного поведения, например, при создании собственной обертки над useAsyncData.
Установка ключа может быть полезна для обмена одними и теми же данными между компонентами с помощью
useNuxtData (/docs/api/composables/use-nuxt-data) или для обновления специфичных данных.<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 () => {
  const [coupons, offers] = await Promise.all([
    $fetch('/cart/coupons'),
    $fetch('/cart/offers')
  ])
  return { coupons, offers }
})
// discounts.value.coupons
// discounts.value.offers
</script>
Возвращаемые значения
useFetch и useAsyncData имеют одинаковые возвращаемые значения, перечисленные ниже.
- data: результат работы переданной асинхронной функции.
- refresh/- execute: функция, которая может быть использована для обновления данных, возвращенных функцией- handler.
- clear: функция, которая может быть использована для установки- dataв- undefined, установки- errorв- null, установки- pendingв- false, установки- statusв- idleи пометки всех текущих запросов как отмененных.
- error: объект ошибки, если получение данных не удалось.
- status: строка, указывающая на статус запроса данных (- "idle",- "pending",- "success",- "error").
data, error и status - это Vue ref, доступные с помощью .value в <script setup>.По умолчанию Nuxt ждет, пока refresh не будет завершен, прежде чем его можно будет выполнить снова.
server: false), то данные не будут получены до завершения гидратации. Это означает, что даже если вы ожидаете useFetch на стороне клиента, data останется null внутри <script setup>.Параметры
useAsyncData и useFetch возвращают один и тот же тип объекта и принимают общий набор опций в качестве последнего аргумента. С их помощью можно управлять поведением композаблов, например, блокировкой навигации, кэшированием или выполнением.
Отложенная загрузка
По умолчанию композаблы, выполняющие получение данных, будут ждать разрешения своей асинхронной функции перед переходом на новую страницу с помощью Vue-шного Suspense. Эту возможность можно игнорировать при навигации на стороне клиента с помощью опции lazy. В этом случае вам придется вручную обрабатывать состояние загрузки, используя значение status.
<script setup lang="ts">
const { status, data: posts } = useFetch('/api/posts', {
  lazy: true
})
</script>
<template>
  <!-- вам нужно будет обрабатывать состояние загрузки -->
  <div v-if="status === 'pending'">
    Loading ...
  </div>
  <div v-else>
    <div v-for="post in posts">
      <!-- сделать что-нибудь -->
    </div>
  </div>
</template>
В качестве альтернативы вы можете использовать useLazyFetch и useLazyAsyncData как удобные методы для выполнения того же самого.
<script setup lang="ts">
const { status, data: posts } = useLazyFetch('/api/posts')
</script>
Получение данных только на клиенте
По умолчанию композаблы для получения данных будут выполнять свою асинхронную функцию как на клиенте, так и на сервере. Установите опцию server в значение false, чтобы выполнять вызов только на стороне клиента. При первоначальной загрузке данные не будут извлечены до завершения гидратации, поэтому вам придется обрабатывать состояние ожидания, хотя при последующей навигации на стороне клиента данные будут загружены перед загрузкой страницы.
В сочетании с опцией lazy это может быть полезно для данных, которые не нужны при первом рендере (например, данные, не относящиеся к SEO).
/* Этот вызов выполняется перед гидратацией */
const articles = await useFetch('/api/article')
/* Этот вызов будет выполнен только на клиенте */
const { status, data: comments } = useFetch('/api/comments', {
  lazy: true,
  server: false
})
Композабл useFetch предназначен для вызова в методе setup или непосредственно на верхнем уровне функции в хуках жизненного цикла, в противном случае следует использовать функцию $fetch.
Минимизация размера полезной нагрузки
Опция pick позволяет минимизировать размер полезной нагрузки, хранящейся в HTML-документе, выбирая только те поля, которые вы хотите вернуть из композаблов.
<script setup lang="ts">
/* выберите только те поля, которые используются в вашем шаблоне */
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.Обновить и выполнить
Если вы хотите получить или обновить данные вручную, воспользуйтесь функцией execute или refresh, предоставляемые композаблом.
<script setup lang="ts">
const { data, error, execute, refresh } = await useFetch('/api/users')
</script>
<template>
  <div>
    <p>{{ data }}</p>
    <button @click="() => refresh()">Обновить данные</button>
  </div>
</template>
Функция execute - это псевдоним для refresh, который работает точно так же, но является более семантичной для случаев, когда выборка происходит не немедленно.
refreshNuxtData и clearNuxtData.Очистка
Если вы хотите очистить предоставленные данные по какой-либо причине, не зная конкретного ключа, который нужно передать в 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', {
  /* Изменение идентификатора вызовет повторную загрузку */
  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.
Вычисляемый 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>
    <!-- отключаем инпут, пока данные запрашиваются -->
    <input v-model="id" type="number" :disabled="pending"/>
    <div v-if="status === 'idle'">
      Введите ID пользователя
    </div>
    <div v-else-if="pending">
      Загрузка ...
    </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">Получить данные</button>
  </div>
  <div v-else-if="status === 'pending'">
    Загружаем комментарии...
  </div>
  <div v-else>
    {{ data }}
  </div>
</template>
Для более точного контроля переменная status может быть:
- idle, когда получение данных еще не началось
- pending, когда получение данных началось, но еще не завершилось
- error, когда получение данных завершилось неудачно
- success, когда получение данных завершилось успешно
Передача заголовков и куки
When we call $fetch in the browser, user headers like cookie will be directly sent to the API. But during server-side-rendering, since the $fetch request takes place 'internally' within the server, it doesn't include the user's browser cookies, nor does it pass on cookies from the fetch response.
When we call $fetch in the browser, user headers like cookie will be directly sent to the API.
Normally, during server-side-rendering, since the $fetch request takes place 'internally' within the server, it wouldn't include the user's browser cookies, nor pass on cookies from the fetch response.
However, when calling useFetch on the server, Nuxt will use useRequestFetch to proxy headers and cookies (with the exception of headers not meant to be forwarded, like host).
Передача куки из вызовов API на стороне сервера в SSR ответе
Если вы хотите передавать/проксировать куки в другом направлении - от внутреннего запроса обратно клиенту - вам нужно будет сделать это самостоятельно.
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'
export const fetchWithCookie = async (event: H3Event, url: string) => {
  /* Получите ответ от эндпоинта сервера */
  const res = await $fetch.raw(url)
  /* Получите куки из ответа */
  const cookies = res.headers.getSetCookie()
  /* Прикрепите каждую куки к нашему входящему запросу */
  for (const cookie of cookies) {
    appendResponseHeader(event, 'set-cookie', cookie)
  }
  /* Верните данные из ответа */
  return res._data
}
<script setup lang="ts">
// Этот композабл будет автоматически передавать куки клиенту
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({
  /* Используйте опцию fetchKey, чтобы предоставить уникальный ключ. */
  fetchKey: 'hello',
  async asyncData () {
    return {
      hello: await $fetch('/api/hello')
    }
  }
})
</script>
<script setup> или <script setup lang="ts""> является рекомендуемым способом объявления компонентов Vue в Nuxt 3.Сериализация данных с сервера на клиент
При использовании useAsyncData и useLazyAsyncData для передачи данных, полученных на сервере, клиенту (а также всего остального, что использует Nuxt payload), полезная нагрузка сериализуется с devalue. Это позволяет нам передавать не только базовый JSON, но и сериализовывать и "оживить"/десериализовывать более сложные виды данных, такие как регулярные выражения, даты, Map и Set, ref, reactive, shallowRef, shallowReactive и NuxtError - и многое другое.
Также можно определить свой собственный сериализатор/десериализатор для типов, которые не поддерживаются Nuxt. Подробнее об этом можно прочитать в документации useNuxtApp.
$fetch или useFetch - см. следующий раздел для получения дополнительной информации.Сериализация данных из маршрутов API
При получении данных из директории server ответ сериализуется с помощью JSON.stringify. Однако, поскольку сериализация ограничена только примитивными типами JavaScript, Nuxt делает все возможное, чтобы преобразовать возвращаемый тип $fetch и useFetch для соответствия реальному значению.
Пример
export default defineEventHandler(() => {
  return new Date()
})
<script setup lang="ts">
// Тип `data` определяется как string, хотя мы вернули объект Date.
const { data } = await useFetch('/api/foo')
</script>
Пользовательская функция сериализатора
Чтобы настроить поведение сериализации, вы можете определить функцию toJSON для возвращаемого объекта. Если вы определите метод toJSON, Nuxt будет "уважать" возвращаемый тип функции и не будет пытаться преобразовать типы.
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
})
<script setup lang="ts">
// Тип `data` определяется как
// {
//   createdAt: {
//     year: number
//     month: number
//     day: number
//   }
// }
const { data } = await useFetch('/api/bar')
</script>
Использование альтернативного сериализатора
В настоящее время Nuxt не поддерживает сериализатор, альтернативный JSON.stringify. Однако вы можете возвращать полезную нагрузку в виде обычной строки и использовать метод toJSON для сохранения безопасности типов.
В примере ниже мы используем superjson в качестве сериализатора.
import superjson from 'superjson'
export default defineEventHandler(() => {
  const data = {
    createdAt: new Date(),
    // Играться с преобразованием типов тут
    toJSON() {
      return this
    }
  }
  // Сериализуйте вывод в строку, используя superjson
  return superjson.stringify(data) as unknown as typeof data
})
<script setup lang="ts">
import superjson from 'superjson'
// `data` определяется как { createdAt: Date }, и вы можете смело использовать методы объекта Date
const { data } = await useFetch('/api/superjson', {
  transform: (value) => {
    return superjson.parse(value as unknown as string)
  },
})
</script>
Рецепты
Использование SSE (Server Sent Events) через POST-запрос
EventSource или композабл из VueUse useEventSource.При использовании SSE через POST-запрос вам необходимо вручную обработать соединение. Вот как это можно сделать:
// Выполните POST-запрос к эндпоинту SSE:
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
  method: 'POST',
  body: {
    query: "Привет AI, как ты?",
  },
  responseType: 'stream',
})
// Создайте новый поток ReadableStream из ответа с помощью TextDecoderStream, чтобы получить данные в виде текста:
const reader = response.pipeThrough(new TextDecoderStream()).getReader()
// Прочитайте фрагмент данных, как только мы его получим:
while (true) {
  const { value, done } = await reader.read()
  if (done)
    break
  console.log('Получено:', value)
}