Получение данных
Nuxt поставляется с двумя композаблами и встроенной библиотекой для получения данных в браузере или на сервере: useFetch
, useAsyncData
и $fetch
.
В двух словах:
useFetch
- это самый простой способ получениях данных в функцииsetup
компонента.$fetch
отлично подходит для выполнения сетевых запросов на основе взаимодействия с пользователем.useAsyncData
в сочетании с$fetch
предлагает больший контроль.
И useFetch
, и useAsyncData
имеют общий набор опций и паттернов, которые мы подробно рассмотрим в последних разделах.
Но прежде необходимо знать, зачем вообще существуют эти композаблы.
Зачем использовать специальные композаблы для получения данных?
Nuxt - это фреймворк, который может выполнять изоморфный (или универсальный) код как в серверном, так и в клиентском окружениях. Если функция $fetch
используется для получения данных в функции setup
компонента Vue, это может привести к тому, что данные будут получены дважды, один раз на сервере (чтобы отрендерить HTML) и еще раз на клиенте (когда HTML будет гидрирован). Именно поэтому Nuxt предлагает специальные композаблы для получения данных, чтобы данные запрашивались только один раз.
Дублирование сетевых вызовов
Композаблы useFetch
и useAsyncData
гарантируют, что после выполнения вызова API на сервере данные будут правильно переданы клиенту в полезной нагрузке.
Полезная нагрузка - это объект JavaScript, доступный через useNuxtApp().payload
. Он используется на клиенте, чтобы избежать повторного запроса одних и тех же данных при выполнении кода в браузере во время гидратации.
Suspense
Nuxt использует компонент Vue <Suspense>
под капотом, чтобы предотвратить навигацию до того, как все асинхронные данные будут доступны для просмотра. Композаблы для получения данных могут помочь вам использовать эту функцию и использовать то, что лучше всего подходит для каждого вызова.
<NuxtLoadingIndicator>
, чтобы добавить индикатор прогресса между переходами по странице.useFetch
Композабл useFetch
является наиболее простым способом получения данных.
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>
<template>
<p>Количество просмотров страницы: {{ count }}</p>
</template>
Этот композабл представляет собой обертку вокруг композабла useAsyncData
и утилиты $fetch
.
$fetch
Nuxt использует библиотеку ofetch, которая автоматически импортируется как псевдоним $fetch
во всем приложении. Это то, что useFetch
использует за кулисами.
<script setup lang="ts">
async function addTodo() {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// todo данные
}
})
}
</script>
$fetch
не обеспечит дедупликацию сетевых вызовов и предотвращение навигации. Рекомендуется использовать
$fetch
для взаимодействия на стороне клиента (на основе событий) или в сочетании с useAsyncData
при получении исходных данных компонента.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)
}