Получение данных
Nuxt поставляется с двумя композаблами и встроенной библиотекой для получения данных в браузере или на сервере: useFetch
, useAsyncData
и $fetch
.
В двух словах:
$fetch
is the simplest way to make a network request.useFetch
is wrapper around$fetch
that fetches data only once in universal rendering.useAsyncData
is similar touseFetch
but 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
при получении исходных данных компонента.useFetch
The useFetch
composable uses $fetch
under-the-hood to make SSR-safe network calls in the setup function.
<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.
useAsyncData
Композабл useAsyncData
отвечает за обертывание асинхронной логики и возврат результата после его разрешения.
useFetch(url)
почти эквивалентно useAsyncData(url, () => $fetch(url))
. Это сахар для разработчиков для наиболее распространенных случаев использования.
Бывают случаи, когда использование композабла 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
, когда получение данных завершилось успешно
Передача заголовков и куки
Когда мы вызываем $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>
host
,accept
content-length
,content-md5
,content-type
x-forwarded-host
,x-forwarded-port
,x-forwarded-proto
cf-connecting-ip
,cf-ray
Передача куки из вызовов 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)
}