数据获取
Nuxt 配备了两个组合式函数和一个内置库,用于在浏览器或服务器环境中执行数据获取:useFetch
、useAsyncData
和 $fetch
。
简而言之:
$fetch
是发起网络请求的最简单方式。useFetch
是$fetch
的封装,确保在通用渲染过程中只获取一次数据。useAsyncData
类似于useFetch
,但提供了更细粒度的控制。
useFetch
和 useAsyncData
共享一套通用的选项和模式,我们将在最后的章节详细介绍。
useFetch
和 useAsyncData
为什么需要
Nuxt 是一个能够在服务器和客户端环境中运行同构(或通用)代码的框架。如果在 Vue 组件的 setup 函数中直接使用 $fetch
函数 来执行数据获取,可能会导致数据被获取两次:一次在服务器端(用于渲染 HTML),一次在客户端(HTML 水合时)。这会引发水合问题,增加交互时间,且可能导致不可预测的行为。
useFetch
和 useAsyncData
通过确保如果在服务器上调用了 API,则数据会通过负载转发给客户端,从而解决了这个问题。
负载是一个 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: {
// 我的表单数据
}
})
}
</script>
<template>
<div v-if="data == null">
无数据
</div>
<div v-else>
<form @submit="handleFormSubmit">
<!-- 表单输入标签 -->
</form>
</div>
</template>
在上面的示例中,useFetch
会确保请求发生在服务器端,并正确转发到浏览器。$fetch
没有此机制,更适合仅在浏览器端发起请求的场景。
Suspense
Nuxt 底层使用 Vue 的 <Suspense>
组件阻止导航,直到所有异步数据可用于视图。数据获取组合函数可以帮助你利用此功能,并根据单次调用情况选择最合适的方式。
<NuxtLoadingIndicator>
来在页面导航间显示进度条。$fetch
Nuxt 包含了 ofetch 库,并在全局自动导入成 $fetch
别名。
<script setup lang="ts">
async function addTodo() {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// 我的待办数据
}
})
}
</script>
向 API 传递客户端请求头
服务端调用 useFetch
时,Nuxt 会使用 useRequestFetch
代理客户端请求头和 Cookie(除了不应转发的请求头,如 host
)。
<script setup lang="ts">
const { data } = await useFetch('/api/echo');
</script>
// /api/echo.ts
export default defineEventHandler(event => parseCookies(event))
另外,下面示例展示如何使用 useRequestHeaders
访问并传递服务端请求中的 Cookie(来自客户端请求)。通过同构的 $fetch
调用,我们确保 API 端点可以访问用户浏览器原始发送的 cookie
头。这仅在不使用 useFetch
时才需要。
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
async function getCurrentUser() {
return await $fetch('/api/me', { headers })
}
</script>
useRequestFetch
来自动代理请求头。host
,accept
content-length
,content-md5
,content-type
x-forwarded-host
,x-forwarded-port
,x-forwarded-proto
cf-connecting-ip
,cf-ray
useFetch
useFetch
组合函数基于 $fetch
,用于在 setup 函数中安全地执行 SSR 网络请求。
<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))
。:br
它是最常用场景的开发体验简化版。(你可以在useRequestFetch
了解更多关于 event.fetch
的信息。)有些场景不适合使用 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
共享同一数据或刷新特定数据。<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
用于获取和缓存数据,而非触发副作用(如调用 Pinia action),因为这可能导致重复执行且产生 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
,error
设为null
,status
设为idle
,并将当前所有待定请求标记为取消。error
: 数据获取失败时的错误对象。status
: 表示数据请求状态的字符串("idle"
、"pending"
、"success"
、"error"
)。
data
、error
和 status
是 Vue 的 ref,在 <script setup>
中用 .value
访问。默认情况下,Nuxt 会等待一次 refresh
执行完成后,才允许再次执行。
server: false
),则数据不会在水合完成前被获取。这意味着即便你在客户端等待 useFetch
,data
在 <script setup>
中依然是 null。选项
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'">
加载中...
</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 作为键。也可通过最后一个参数的key
选项提供自定义键。useAsyncData
如果第一个参数是字符串,则作为键。若第一个参数是查询函数,则自动基于文件名与行号生成唯一键。
useNuxtData
通过键获取缓存数据。共享状态与选项一致性
多个组件若使用相同的 useAsyncData
或 useFetch
键,将共享 data
、error
和 status
refs,保证状态一致。但这要求某些选项保持一致。
以下选项必须在所有同键调用中保持一致:
handler
函数deep
选项transform
函数pick
数组getCachedData
函数default
值
// ❌ 会触发开发警告
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { deep: false })
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { deep: true })
以下选项可安全不同且不会触发警告:
server
lazy
immediate
dedupe
watch
// ✅ 允许
const { data: users1 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: true })
const { data: users2 } = useAsyncData('users', () => $fetch('/api/users'), { immediate: false })
若需要独立实例,请使用不同键:
// 彼此完全独立的实例
const { data: users1 } = useAsyncData('users-1', () => $fetch('/api/users'))
const { data: users2 } = useAsyncData('users-2', () => $fetch('/api/users'))
响应式键
你可以使用计算属性 ref、普通 ref 或 getter 函数作为键,实现依赖变化时自动重新获取数据:
// 使用计算属性作为键
const userId = ref('123')
const { data: user } = useAsyncData(
computed(() => `user-${userId.value}`),
() => fetchUser(userId.value)
)
// 当 userId 变化时数据会自动刷新,且若无其他组件使用旧数据则被清理
userId.value = '456'
refresh 和 execute
如果想手动获取或刷新数据,可使用组合函数提供的 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
。清理
若想清理已有数据而不必使用特定键调用 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', {
/* id 变化会触发重新获取 */
watch: [id]
})
</script>
注意:监听响应式值不会改变被获取的 URL。例如,下面代码会一直请求初始的 user id:
<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 逻辑复杂,也可以使用回调函数作为计算 getter返回 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
:请求成功完成
传递请求头和 Cookie
在浏览器执行 $fetch
时,用户的请求头(如 cookie)会直接发送给 API。
通常在服务器端渲染时,出于安全原因,$fetch
不会携带用户浏览器的 Cookie,也不会传递响应中的 Cookie。
但在服务器上调用相对 URL 的 useFetch
时,Nuxt 会使用 useRequestFetch
代理请求头和 Cookie(除不应转发的头,如 host
)。
服务端 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">
// 该组合函数会自动将 Cookie 传递给客户端
const event = useRequestEvent()
const { data: result } = await useAsyncData(() => fetchWithCookie(event!, '/api/with-cookie'))
onMounted(() => console.log(document.cookie))
</script>
选项 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 负载的场景)时,负载会通过 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">
// 尽管返回的是 Date 对象,`data` 推断类型为 string
const { data } = await useFetch('/api/foo')
</script>
自定义序列化函数
要自定义序列化行为,可以在返回对象中定义 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 },可安全使用日期对象方法
const { data } = await useFetch('/api/superjson', {
transform: (value) => {
return superjson.parse(value as unknown as string)
},
})
</script>
使用示例
通过 POST 请求消费 SSE(服务器推送事件)
EventSource
或 VueUse 组合式函数 useEventSource
。通过 POST 请求消费 SSE 时,需要手动处理连接。示例代码如下:
// 向 SSE 端点发起 POST 请求
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
method: 'POST',
body: {
query: "Hello AI, how are you?",
},
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)
}
并行请求
当多个请求彼此独立时,可以使用 Promise.all()
并行执行,提升性能。
const { data } = await useAsyncData(() => {
return Promise.all([
$fetch("/api/comments/"),
$fetch("/api/author/12")
]);
});
const comments = computed(() => data.value?.[0]);
const author = computed(() => data.value?.[1]);