Nuxt Nation conference is coming. Join us on November 12-13.

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

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

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

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

И 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. Он используется на клиенте, чтобы избежать повторного запроса одних и тех же данных при выполнении кода в браузере во время гидратации.

Используйте 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>

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.

You can add the <NuxtLoadingIndicator> to add a progress bar between page navigations.

$fetch

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

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

useFetch

The useFetch composable uses $fetch under-the-hood to make SSR-safe network calls in the setup function.

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

This composable is a wrapper around the useAsyncData composable and $fetch utility.

Watch the video from Alexander Lichter to avoid using useFetch the wrong way!
Узнать больше Docs > API > Composables > Use Fetch.
Прочитайте и отредактируйте живой пример в Docs > Examples > Features > Data Fetching.

useAsyncData

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

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

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

pages/users.vue
<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) или для обновления специфичных данных.
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 () => {
  const [coupons, offers] = await Promise.all([
    $fetch('/cart/coupons'),
    $fetch('/cart/offers')
  ])

  return { coupons, offers }
})
// discounts.value.coupons
// discounts.value.offers
</script>
Узнайте больше о useAsyncData.

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

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.

app.vue
<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>
Узнайте больше о useLazyFetch.
Узнайте больше о useLazyAsyncData.

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

По умолчанию композаблы для получения данных будут выполнять свою асинхронную функцию как на клиенте, так и на сервере. Установите опцию 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, когда получение данных завершилось успешно

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

Когда мы вызываем $fetch в браузере, пользовательские заголовки, такие как cookie, будут напрямую отправлены в API. Но во время рендеринга на стороне сервера, поскольку запрос $fetch происходит «внутри» сервера, он не включает куки браузера пользователя и не передает куки из ответа fetch.

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

Мы можем использовать useRequestHeaders для доступа и проксирования куки к API со стороны сервера.

Пример ниже добавляет заголовки запроса к изоморфному вызову $fetch, чтобы гарантировать, что API эндпоинт имеет доступ к тому же заголовку cookie, который первоначально был отправлен пользователем.

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

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

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

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

composables/fetch.ts
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.
Узнать больше Docs > API > Utils > Define Nuxt Component.

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

При использовании 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 для соответствия реальному значению.

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

Пример

server/api/foo.ts
export default defineEventHandler(() => {
  return new Date()
})
app.vue
<script setup lang="ts">
// Тип `data` определяется как string, хотя мы вернули объект Date.
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">
// Тип `data` определяется как
// {
//   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(),

    // Играться с преобразованием типов тут
    toJSON() {
      return this
    }
  }

  // Сериализуйте вывод в строку, используя superjson
  return superjson.stringify(data) as unknown as typeof data
})
app.vue
<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-запрос

Если вы используете SSE через GET-запрос, вы можете использовать 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)
}