数据获取
Maschinelle Übersetzung ·0 % geprüft
Nuxt 内置了两个 composables 和一个库,可在浏览器或服务器环境中执行数据获取:useFetch、useAsyncData 和 $fetch。
简而言之:
$fetch是发起网络请求最简单的方式。useFetch是$fetch的封装,在通用渲染中只获取一次数据。useAsyncData与useFetch类似,但提供了更精细的控制。
useFetch 和 useAsyncData 都共享一组通用的选项和模式,我们将在最后几节详细说明。
useFetch 和 useAsyncData 的必要性
Nuxt 是一个框架,可以在服务器和客户端环境中运行同构(或通用)代码。如果在 Vue 组件的 setup 函数中使用 $fetch 函数执行数据获取,可能会导致数据被获取两次:一次在服务器上(用于渲染 HTML),另一次在客户端上(HTML hydration 时)。这可能导致 hydration 问题、增加达到可交互状态所需的时间,并引发不可预测的行为。
useFetch 和 useAsyncData composables 通过确保在服务器上发起的 API 调用会将数据转发到客户端的 payload 中,解决了这个问题。
payload 是一个 JavaScript 对象,可通过 useNuxtApp().payload 访问。它会在客户端使用,以避免在浏览器中执行代码时进行 hydration而重新获取相同的数据。
<script setup lang="ts">
const { data } = await useFetch('/api/data')
async function handleFormSubmit () {
const res = await $fetch('/api/submit', {
method: 'POST',
body: {
// 我的表单数据
},
})
}
</script>
<template>
<div v-if="data == undefined">
无数据
</div>
<div v-else>
<form @submit="handleFormSubmit">
<!-- 表单输入标签 -->
</form>
</div>
</template>在上面的示例中,useFetch 会确保请求在服务器上执行,并正确转发到浏览器。$fetch 没有这样的机制,因此当请求完全由浏览器发起时,使用它是更好的选择。
Suspense
Nuxt 在内部使用 Vue 的 <Suspense> 组件,以防止在所有异步数据可用于视图之前进行导航。数据获取的 composables 可以帮助您利用此功能,并可按调用情况选择最适合的方案。
<NuxtLoadingIndicator>,在页面导航之间显示进度条。关于 await 的说明
本文档中的示例通常会 await 调用 useFetch 和 useAsyncData,但这并非总是必需的。
await 不会改变服务器渲染的 HTML。在服务器渲染期间,Nuxt 无论如何都会在序列化页面之前等待请求完成(底层依赖 <Suspense> 和 onServerPrefetch),因此完整填充的数据结果总是会发送到浏览器。
await 真正 改变的是在你自己的 <script setup> 中接下来会发生什么,以及客户端导航的行为:
- 使用
await时,执行会暂停,直到数据准备就绪,因此调用之后的任何代码都可以依赖已填充的data。在客户端导航时,这会阻塞导航,直到数据解析完成:用户会停留在当前页面上(可以选择显示<NuxtLoadingIndicator>),然后进入数据已完整填充的页面。这是默认行为。 - 不使用
await时,执行会立即继续,请求则在后台运行,因此data起初为其默认值,并在请求解析后填充。在客户端导航时,导航会立即发生,你需要自行处理加载和错误状态,通常通过返回的status和errorrefs 来实现。
没有一种方式在所有场景下都更好;正确的选择取决于你希望该路由呈现怎样的体验。
不使用 await 的用户体验与 lazy 选项类似(不会阻塞导航,且需要自行处理加载状态),但两者并不完全相同:lazy 是一个显式标志,会将请求延迟到组件挂载后执行;而仅仅不使用 await 则会在 setup 期间启动请求。如果你希望采用非阻塞行为,建议使用 lazy(或 useLazyFetch / useLazyAsyncData),因为这样可以明确表达意图。
await 和 lazy 是独立的,在客户端等待一个 lazy 函数不会产生你期望的效果。如果你 await 一个 lazy 调用(例如 await useLazyFetch(...) 或 await useFetch(..., { lazy: true })),它仍然会像往常一样阻塞服务器渲染,但在 客户端导航 中,await 会立即解析,而不会等待请求完成。data 在 await 之后仍然会保持其默认值,你必须通过 status 处理加载状态。如果你确实想让导航等待数据,请去掉 lazy 选项,而不是依赖 await。$fetch
Nuxt 包含 ofetch 库,并在应用中全局自动以 $fetch 别名导入。
<script setup lang="ts">
async function addTodo () {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// 我的待办数据
},
})
}
</script>$fetch 不会提供网络调用去重和导航阻止功能。:br
建议在客户端交互(基于事件)中使用 $fetch,或在获取组件的初始数据时将它与 useAsyncData 结合使用。为持有 $fetch 的值指定类型
要为持有 $fetch 本身的值指定类型(例如传入插件或通过 provide/inject 提供的 $fetch 实例),请使用 $Fetch 类型(TypedFetch 的别名):
import type { $Fetch } from '#app'
function createClient (fetcher: $Fetch) {
return {
todos: () => fetcher('/api/todos'),
}
}将客户端请求头传递给 API
在服务器上调用 useFetch 时,Nuxt 会使用 useRequestFetch 代理客户端请求头和 cookies(不包括不应转发的请求头,例如 host)。
<script setup lang="ts">
const { data } = await useFetch('/api/echo')
</script>// /api/echo.ts
export default defineEventHandler(event => parseCookies(event))另外,下面的示例展示了如何使用 useRequestHeaders 在服务端请求(由客户端发起)中访问 cookies,并将其发送给 API。通过使用同构的 $fetch 调用,我们可以确保 API 端点能够访问用户浏览器最初发送的同一个 cookie 请求头。只有在未使用 useFetch 时才需要这样做。
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
async function getCurrentUser () {
return await $fetch('/api/me', { headers })
}
</script>useRequestFetch 自动代理请求头。在将头代理到外部 API 之前请务必谨慎,仅包含您需要的头。并非所有头都可以安全地被绕过,可能会引入不期望的行为。以下是一些通常不应被代理的常见头列表:
host,acceptcontent-length,content-md5,content-typex-forwarded-host,x-forwarded-port,x-forwarded-protocf-connecting-ip,cf-ray
useFetch
useFetch composable 在底层使用 $fetch,用于在 setup 函数中发起 SSR 安全的网络调用。
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>
<template>
<p>页面访问次数: {{ count }}</p>
</template>这个 composable 是对 useAsyncData composable 和 $fetch 工具的封装。
useAsyncData
useAsyncData 组合式函数负责包装异步逻辑,并在其解析后返回结果。
useFetch(url) 几乎等同于 useAsyncData(url, () => event.$fetch(url))。:br
这是针对最常见用例提供的开发体验语法糖。(有关 event.fetch 的更多信息,请参阅 useRequestFetch。)在某些情况下,使用 useFetch composable 并不合适,例如 CMS 或第三方服务提供了自己的查询层。在这种情况下,您可以使用 useAsyncData 封装调用,同时保留该 composable 提供的优势。
<script setup lang="ts">
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))
// 也可以这样写:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>useAsyncData 的第一个参数是一个唯一键,用于缓存第二个参数(查询函数)的响应。直接传入查询函数即可忽略此键,系统会自动生成键。
由于自动生成的键只会考虑调用
useAsyncData 的位置,因此建议始终创建您自己的键,以避免不必要的行为,例如当您创建自己的自定义组合式函数来包装 useAsyncData 时。
设置键有助于使用
useNuxtData 在组件间共享相同数据,或刷新特定数据。<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 actions),因为这可能导致使用 nullish 值重复执行等意外行为。如果您需要触发副作用,请使用 callOnce 工具。
<script setup lang="ts">
const offersStore = useOffersStore()
// 你不能这样做
await useAsyncData(() => offersStore.getOffer(route.params.slug))
</script>返回值
useFetch 和 useAsyncData 具有相同的返回值,列举如下。
data:传入的异步函数返回的结果。refresh/execute:用于刷新由handler函数返回的数据的函数。clear:用于将data设置为undefined(或如果提供了options.default()则为该值)、将error设置为undefined、将status设置为idle,并将任何当前挂起的请求标记为已取消的函数。error:如果数据获取失败,则为错误对象。status:指示数据请求状态的字符串("idle"、"pending"、"success"、"error")。
data、error 和 status 是 Vue 的 refs,在 <script setup> 中可通过 .value 访问。默认情况下,Nuxt 会等待 refresh 完成后才允许再次执行。
server: false),那么在 hydration 完成之前不会获取数据。这意味着即使你在客户端 await useFetch,在 <script setup> 中 data 仍然会保持为 undefined。选项
useAsyncData 和 useFetch 返回相同的对象类型,并接受一组通用选项作为最后一个参数。这些选项可以帮助您控制 composables 的行为,例如是否阻止导航、缓存或执行。
延迟(Lazy)
默认情况下,数据获取的 composables 会在其异步函数解析完成之前使用 Vue 的 Suspense 等待,从而阻止导航。在客户端导航时,可以通过 lazy 选项忽略此功能。在这种情况下,您需要使用 status 值手动处理加载状态。
<script setup lang="ts">
const { status, data: posts } = useFetch('/api/posts', {
lazy: true,
})
</script>
<template>
<!-- 你需要自己处理加载状态 -->
<div v-if="status === 'pending'">
加载中 ...
</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>仅客户端获取
默认情况下,数据获取的 composables 会在客户端和服务器环境中执行它们的异步函数。将 server 选项设置为 false 可仅在客户端执行调用。在初次加载时,数据不会在 hydration 完成之前被获取,因此您必须处理挂起(pending)状态;不过在随后的客户端导航中,数据会在加载页面之前被等待。
与 lazy 选项结合使用时,这对首次渲染时不需要的数据(例如非 SEO 相关数据)非常有用。
/* 这个调用会在 hydration 之前执行 */
const articles = await useFetch('/api/article')
/* 这个调用只会在客户端执行 */
const { status, data: comments } = useFetch('/api/comments', {
lazy: true,
server: false,
})useFetch composable 应在 setup 方法中调用,或在生命周期钩子中直接于函数顶层调用;否则,您应该使用 $fetch 方法。
最小化 payload 大小
pick 选项可以帮助您最小化存储在 HTML 文档中的 payload 大小,只选择您希望从 composables 返回的字段。
<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 都不会阻止初始不想要的数据被获取。但它们会防止不想要的数据被添加到从服务器转发到客户端的 payload 中。缓存与重新获取
键(Keys)
useFetch 和 useAsyncData 使用键来避免重新获取相同的数据。
useFetch会根据 URL、获取选项以及源代码中调用的位置生成键。这意味着,不同组件中两个 URL 相同的useFetch调用会使用不同的键,并各自发起请求。要在多个组件间共享相同的数据,请在作为最后一个参数传入的options对象中提供相同的显式key。useAsyncData会在第一个参数为字符串时将其用作键。如果第一个参数是执行查询的 handler 函数,则会根据源代码中useAsyncData调用的位置为您生成唯一的键。
useNuxtData。共享状态与选项一致性
当多个组件对相同键使用 useAsyncData 或 useFetch 时,它们将共享相同的 data、error 和 status refs。这确保了组件间的一致性,但要求某些选项保持一致。
以下选项在使用相同键的所有调用中必须保持一致:
handler函数deep选项transform函数pick数组getCachedData函数default值
// ❌ 这会触发开发时警告
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 })以下选项可以安全地不同而不会触发警告:
serverlazyimmediatededupewatch
// ✅ 这是允许的
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 })如果您需要独立的实例,请使用不同的键:
// 这些是完全独立的实例
const { data: users1 } = useAsyncData('users-1', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))
const { data: users2 } = useAsyncData('users-2', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))响应式键
您可以使用计算 ref、普通 ref 或 getter 函数作为键,从而实现基于依赖变化自动更新的数据获取:
// 使用计算属性作为键
const userId = ref('123')
const { data: user } = useAsyncData(
computed(() => `user-${userId.value}`),
() => fetchUser(userId.value),
)
// 当 userId 改变时,数据会自动重新获取
// 并且如果没有其他组件使用它,旧数据会被清理
userId.value = '456'refresh 与 execute
如果您想手动获取或刷新数据,请使用 composables 提供的 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 的别名,工作方式完全相同,但在获取非立即执行的情况下语义更明确。
clearNuxtData 和 refreshNuxtData。清除(Clear)
如果您想清除所提供的数据(出于任何原因),而不需要知道传递给 clearNuxtData 的特定键,可以使用 composables 提供的 clear 函数。
<script setup lang="ts">
const { data, clear } = await useFetch('/api/users')
const route = useRoute()
watch(() => route.path, (path) => {
if (path === '/') {
clear()
}
})
</script>监听(Watch)
要在应用中的其他响应式值发生变化时重新执行获取函数,请使用 watch 选项。您可以将其用于一个或多个可监听元素。
<script setup lang="ts">
const id = ref(1)
const { data, error, refresh } = await useFetch('/api/users', {
/* 更改 id 会触发重新获取 */
watch: [id],
})
</script>请注意,监听响应式值不会更改被获取的 URL。例如,下面的代码会一直获取初始用户 ID 对应的相同 URL,因为 URL 在函数被调用时就已构建。
<script setup lang="ts">
const id = ref(1)
const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
watch: [id],
})
</script>如果您需要根据响应式值更改 URL,可以考虑使用计算 URL。
提供响应式获取选项时,它们会被自动监听并触发重新获取。在某些情况下,您可能希望通过指定 watch: false 来退出此行为。
const id = ref(1)
// 当 id 改变时不会自动重新获取
const { data, execute } = await useFetch('/api/users', {
query: { id }, // id 默认会被监听
watch: false, // 禁用对 id 的自动监听
})
// 不会触发重新获取
id.value = 2计算 URL(Computed URL)
有时您可能需要基于响应式值构建 URL,并在这些值每次变化时刷新数据。您可以将每个参数作为响应式值附加,Nuxt 会自动使用该响应式值并在其变化时重新获取。
<script setup lang="ts">
const id = ref(null)
const { data, status } = useLazyFetch('/api/user', {
query: {
user_id: id,
},
})
</script>在更复杂的 URL 构建场景中,您可以使用回调作为计算 getter,返回 URL 字符串。
每当依赖项发生变化时,都会使用新构建的 URL 获取数据。将其与非立即执行结合使用,即可等到响应式元素发生变化后再获取数据。
<script setup lang="ts">
const id = ref(null)
const { data, status } = useLazyFetch(() => `/api/users/${id.value}`, {
immediate: false,
})
</script>
<template>
<div>
<!-- 在获取时禁用输入 -->
<input
v-model="id"
type="number"
:disabled="status === 'pending'"
>
<div v-if="status === 'idle'">
输入用户 ID
</div>
<div v-else-if="status === 'pending'">
加载中 ...
</div>
<div v-else>
{{ data }}
</div>
</div>
</template>如果您需要在其他响应式值发生变化时强制刷新,也可以监听其他值。
非立即执行(Not immediate)
useFetch composable 会在调用的瞬间开始获取数据。您可以通过设置 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:获取成功完成
传递头和 Cookie
当我们在浏览器中调用 $fetch 时,用户的头信息如 cookie 会直接发送到 API。
通常,在服务端渲染期间,出于安全考虑,$fetch 不会包含用户浏览器的 cookies,也不会将 fetch 响应中的 cookie 转发回客户端。
但是,在服务器上使用相对 URL 调用 useFetch 时,Nuxt 会使用 useRequestFetch 代理请求头和 cookies(不包括不应转发的请求头,例如 host)。
在 SSR 响应中将服务器端 API 调用的 Cookie 传回客户端
如果您想将 cookie 从内部请求传回/代理到客户端,您需要自行处理。
import { appendResponseHeader } from 'h3'
import type { H3Event } from 'h3'
export const fetchWithCookie = async (event: H3Event, url: string) => {
/* 获取来自服务器端点的响应 */
const res = await $fetch.raw(url)
/* 获取响应中的 cookies */
const cookies = res.headers.getSetCookie()
/* 将每个 cookie 附加到我们的传入请求 */
for (const cookie of cookies) {
appendResponseHeader(event, 'set-cookie', cookie)
}
/* 返回响应数据 */
return res._data
}<script setup lang="ts">
// 该 composable 会自动将 cookies 传递到客户端
const event = useRequestEvent()
const { data: result } = await useAsyncData(() => fetchWithCookie(event!, '/api/with-cookie'))
onMounted(() => console.log(document.cookie))
</script>Options API 支持
Nuxt 提供了一种在 Options API 中执行 asyncData 获取的方法。为使其工作,您必须将组件定义包装在 defineNuxtComponent 中。
<script>
export default defineNuxtComponent({
/* 使用 fetchKey 选项提供唯一键 */
fetchKey: 'hello',
async asyncData () {
return {
hello: await $fetch('/api/hello'),
}
},
})
</script><script setup> 或 <script setup lang="ts"> 来声明 Vue 组件。将数据从服务器序列化到客户端
使用 useAsyncData 和 useLazyAsyncData 将服务器上获取的数据传输到客户端时(以及使用Nuxt payload的其他场景),payload 会使用 devalue 序列化。这样不仅可以传输基本的 JSON,还可以序列化并还原/反序列化更高级的数据类型,例如正则表达式、Date、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">
// 虽然我们返回了 Date 对象,但 `data` 类型推断为 string
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 之外的方案。不过,您可以将 payload 作为普通字符串返回,并利用 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>参考用例(Recipes)
通过 POST 请求使用 SSE(Server-Sent Events)
EventSource 或 VueUse 的 composable useEventSource。当通过 POST 请求消费 SSE 时,您需要手动处理连接。如下所示:
// 向 SSE 端点发起 POST 请求
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
method: 'POST',
body: {
query: '你好,AI,你好吗?',
},
responseType: 'stream',
})
// 使用 TextDecoderStream 创建新的 ReadableStream,以文本形式获取数据
const reader = response.pipeThrough(new TextDecoderStream()).getReader()
// 读取接收到的数据块
while (true) {
const { value, done } = await reader.read()
if (done) { break }
console.log('已接收:', 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])