Nuxt 4:$fetch、useFetch 和 useAsyncData - 完整数据获取指南
Nuxt 4 的数据获取有三个层级,每个工具干的活不同。搞混它们是 SSR 项目里最常见的坑——要么双重请求拖慢首屏,要么状态丢了导致水合报错。
三个工具的定位:$fetch 是底层 HTTP 客户端,纯粹发请求,不关心 SSR。useAsyncData 是核心状态原语,负责异步数据的生命周期管理,确保服务端拿到的数据能序列化进 payload 传给客户端。useFetch 是 useAsyncData 加 $fetch 的语法糖,针对单个 GET 端点做了封装。
说白了,useFetch 内部就是调用 useAsyncData + $fetch。
何时使用每个工具
选择标准取决于你在哪用、做什么操作。
1. useFetch:简单的初始数据检索(GET)
页面加载时要从单个端点拉数据,需要 SSR 支持,又不想手动管理状态传输——用 useFetch 就对了。写法最简洁。
典型场景:获取文章列表。
// app/pages/posts.vue
<script setup lang="ts">
// 负责在服务器上进行一次请求并传输状态。
const { data: posts } = await useFetch('/api/posts')
</script>2. useAsyncData:复杂的异步逻辑或非 HTTP 数据源
当你的异步操作不是简单的单端点 HTTP 调用时,useAsyncData 是唯一选择。它接受任意返回 Promise 的 handler,这才是真正的底层原语。
几个场景:
并行调用:需要同时发多个请求,拿到结果后组合返回。
const { data } = await useAsyncData("dashboard", async () => {
const [user, stats] = await Promise.all([$fetch("/api/user"), $fetch("/api/stats")]);
return { user, stats };
});第三方客户端:用 GraphQL 客户端、数据库 ORM 或其他不走 $fetch 的数据源。
// myGetFunction 是您的数据客户端中的自定义函数
const { data } = await useAsyncData("users", () => myGetFunction("users"));3. $fetch:突变和客户端/服务器驱动的操作
用户点击按钮、提交表单这类纯客户端操作,直接用 $fetch。这些请求不需要 SSR 状态同步,用 useAsyncData 反而是多余的。
突变(POST/PUT/DELETE):用 useFetch 发 POST 通常不划算,因为它依赖 SSR 状态水合,而突变根本不需要。
// 在事件处理程序内使用
async function submitForm() {
await $fetch("/api/contact", {
method: "POST",
body: formData.value
});
}内部服务器路由调用:在 Nuxt 的 server/api 路由内部,$fetch 不会真的发 HTTP 请求,而是直接调用处理函数。服务器内部通信零开销。
各自的优势和代价
$fetch 的好处是轻量和直接。内部调用 server/api 时没有 HTTP 往返。代价也明显:在 SSR 组件的 setup 里直接用,客户端水合时会重新发一次请求——经典的"双重获取"。
useAsyncData 控制力最强。它接受任何异步逻辑,保证数据只在服务端请求一次、通过 payload 传给客户端。返回值包括 data、pending、error、status,状态管理很完整。Nuxt 4 里 data 默认是 shallowRef,大型数据结构少了深层响应性的 CPU 开销。代价是需要手写 handler 和唯一 key,比 useFetch 啰嗦。
useFetch 胜在 DX。最简的 SSR 安全写法,自动从 URL 和选项生成 key,还能从服务端路由推断类型。但它只适合对单个端点的 GET 调用,拿来做 POST/PUT/DELETE 不合适。
何时从客户端 vs 服务器端使用
核心判断标准:这次请求的数据需不需要通过 payload 从服务端传到客户端?需要的话必须用支持状态传输的工具。
服务器端(SSR)和通用渲染
SSR 场景下,数据要出现在初始 HTML 中(SEO 和首屏性能的关键)。这时必须用走 payload 机制的工具:
页面或组件中的初始数据检索,用 useFetch 或 useAsyncData。它们确保数据只在服务端获取一次,客户端水合时直接从 payload 读取。
内部 API 路由之间的调用,用 $fetch。Nuxt 会拦截调用直接执行处理函数,跳过真正的 HTTP 请求。
踩坑预警:如果你在 SSR 组件的 <script setup> 里直接用 $fetch 获取初始数据,客户端水合时会重新发请求。双重获取就是这么来的。
客户端
水合后或用户交互触发的操作,不需要状态传输,用 $fetch 最合适。
用户事件触发的突变(POST/PUT/DELETE),直接 $fetch。状态不需要从服务端传过来,useAsyncData 的开销纯属浪费。
非 SEO 关键的数据(比如基于地理位置的内容),可以给 useFetch 或 useAsyncData 加 server: false,让数据获取推迟到客户端水合完成后。
需要手动刷新已有数据,用 useFetch/useAsyncData 返回的 refresh() 或 execute(),不必退回 $fetch。
打个比方:useFetch 和 useAsyncData 是从服务端寄到客户端的保险箱——服务端把数据装进去封好,客户端收到后直接开箱,不用再自己跑一趟。$fetch 就是一通电话,服务端打过一次,客户端还得再打一次才能拿到自己的副本。
高级实际示例
useAsyncData 的优化加载模式
// pages/dashboard.vue
<script setup lang="ts">
// 带有详细状态管理的优化加载
const {
data: dashboard,
pending,
error,
refresh
} = await useAsyncData('dashboard', async () => {
// 并行调用以获得更好的性能
const [user, stats, notifications] = await Promise.all([
$fetch('/api/user/profile'),
$fetch('/api/analytics/stats'),
$fetch('/api/notifications/unread')
])
return {
user,
stats,
notifications
}
}, {
// 高级选项
server: true, // 在服务器上执行
lazy: false, // 等待渲染前
default: () => ({ // 加载时的默认值
user: null,
stats: { views: 0, users: 0 },
notifications: []
})
})
// 带防抖的手动刷新
const debouncedRefresh = useDebounceFn(refresh, 300)
// 监视自动刷新
watch(() => route.path, debouncedRefresh)
</script>带错误处理的 $fetch 突变
// components/ContactForm.vue
<script setup lang="ts">
const formData = ref({
name: '',
email: '',
message: ''
})
const isSubmitting = ref(false)
const submitError = ref<string | null>(null)
async function submitForm() {
isSubmitting.value = true
submitError.value = null
try {
const response = await $fetch('/api/contact', {
method: 'POST',
body: formData.value,
// 额外选项
timeout: 10000,
retry: 2,
retryDelay: 1000
})
// 成功处理
await navigateTo('/thank-you')
} catch (error) {
if (error.data?.message) {
submitError.value = error.data.message
} else {
submitError.value = '提交表单时出错。请重试。'
}
} finally {
isSubmitting.value = false
}
}
</script>带数据转换的 useFetch
// pages/products/[id].vue
<script setup lang="ts">
interface Product {
id: number
name: string
price: number
description: string
category: string
}
interface ProductWithDetails extends Product {
formattedPrice: string
isInStock: boolean
relatedProducts: Product[]
}
const { data: product, pending } = await useFetch<ProductWithDetails>(
`/api/products/${route.params.id}`,
{
// 数据转换
transform: (data: Product) => ({
...data,
formattedPrice: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(data.price),
isInStock: data.price > 0,
relatedProducts: [] // 稍后填充
}),
// 高级缓存
server: true,
key: `product-${route.params.id}`,
// 缓存选项
getCachedData: (key) => {
// 自定义缓存逻辑
return nuxtApp.staticData[key] || nuxtApp.payload.data[key]
}
}
)
// 之后加载相关产品
watchEffect(async () => {
if (product.value?.category) {
const { data: related } = await $fetch<Product[]>(`/api/products/related/${product.value.category}`)
product.value.relatedProducts = related
}
})
</script>最佳实践和模式
1. 唯一键策略
useAsyncData 的 key 决定了 payload 里数据的唯一性。key 冲突会互相覆盖,key 不稳定会导致重复请求。
// ✅ 良好的键策略
const { data } = await useAsyncData(`user-${userId}`, () => $fetch(`/api/users/${userId}`));
// 对于依赖多个参数的数据
const { data } = await useAsyncData(`search-${searchTerm}-${page}-${category}`, () =>
$fetch("/api/search", {
query: { q: searchTerm, page, category }
})
);2. 加载状态管理
// components/DataLoader.vue
<script setup lang="ts">
interface AsyncDataState<T> {
data: Ref<T | null>
pending: Ref<boolean>
error: Ref<Error | null>
refresh: () => Promise<void>
}
// 可重用组合式函数
function useAsyncDataWithState<T>(
key: string,
handler: () => Promise<T>
): AsyncDataState<T> {
const { data, pending, error, refresh } = useAsyncData(key, handler)
return {
data,
pending,
error,
refresh
}
}
// 在组件中使用
const { data: posts, pending, error } = useAsyncDataWithState(
'posts',
() => $fetch('/api/posts')
)
</script>
<template>
<div>
<div v-if="pending" class="loading">
加载文章...
</div>
<div v-else-if="error" class="error">
错误:{{ error.message }}
<button @click="refresh">重试</button>
</div>
<div v-else-if="data" class="posts">
<PostCard
v-for="post in data"
:key="post.id"
:post="post"
/>
</div>
</div>
</template>3. useLazyFetch 的网络优化
非关键数据可以延后加载,别让它阻塞首屏渲染。
// 对于可以稍后加载的非关键数据
const { data: comments } = await useLazyFetch(`/api/posts/${postId}/comments`, {
server: false, // 仅客户端
lazy: true // 不阻塞渲染
});
// 对于频繁更新的数据
const { data: liveScore } = await useFetch("/api/live-score", {
server: false,
refresh: {
// 每 30 秒刷新
interval: 30000
}
});性能考虑
1. Nuxt 4 中的浅引用
Nuxt 4 默认把 useAsyncData 和 useFetch 返回的 data 做成 shallowRef。这意味着只有顶层引用变化才触发更新,深层属性改动不会。大型数据结构省了大量响应式追踪开销。
// 在 Nuxt 4 中,这默认是 shallowRef
const { data } = await useAsyncData("large-dataset", () => $fetch("/api/large-dataset"));
// 如果需要深度响应性(谨慎使用)
const { data } = await useAsyncData("deep-reactive", () => $fetch("/api/nested-data"), {
deep: true // 启用深度响应性
});2. 缓存策略
// 组件级缓存
const { data } = await useFetch("/api/config", {
key: "app-config",
// 缓存 5 分钟
server: true,
transform: data => {
// 数据将保留在缓存中
return data;
}
});
// 条件缓存
const { data } = await useFetch("/api/user-preferences", {
key: `user-prefs-${userId}`,
server: false,
// 仅在没有错误时缓存
getCachedData: key => {
const cached = nuxtApp.staticData[key] || nuxtApp.payload.data[key];
return cached && !cached.error ? cached : null;
}
});TypeScript 集成
useFetch 的强类型
// types/api.ts
export interface User {
id: number
name: string
email: string
avatar?: string
}
export interface ApiResponse<T> {
data: T
message: string
success: boolean
}
// pages/users/[id].vue
<script setup lang="ts">
const route = useRoute()
const { data: user } = await useFetch<ApiResponse<User>>(`/api/users/${route.params.id}`)
// 类型安全访问
if (user.value?.data) {
console.log(user.value.data.name) // TypeScript 知道类型
}
</script>useAsyncData 的类型
// composables/useDashboard.ts
export interface DashboardData {
user: User;
stats: {
views: number;
clicks: number;
conversions: number;
};
recentActivity: Activity[];
}
export function useDashboardData() {
return useAsyncData<DashboardData>("dashboard", async () => {
const [userResponse, statsResponse, activityResponse] = await Promise.all([$fetch<ApiResponse<User>>("/api/user/profile"), $fetch<ApiResponse<Stats>>("/api/analytics/stats"), $fetch<ApiResponse<Activity[]>>("/api/activity/recent")]);
return {
user: userResponse.data,
stats: statsResponse.data,
recentActivity: activityResponse.data
};
});
}调试和监控
调试工具
// 在开发中启用调试模式
const { data } = await useFetch("/api/debug-example", {
// 在控制台显示调试信息
onRequestError({ request, error }) {
console.error("请求错误:", { request, error });
},
onResponseError({ response }) {
console.error("响应错误:", response.status, response.statusText);
},
onResponse({ response }) {
console.log("收到响应:", response._data);
}
});性能监控
// 用于测量性能的组合式函数
function useTrackedFetch<T>(url: string, options = {}) {
const startTime = Date.now();
return useFetch<T>(url, {
...options,
onResponse() {
const duration = Date.now() - startTime;
console.log(`获取 ${url} 耗时 ${duration}ms`);
// 如果慢则发送到分析
if (duration > 1000) {
$fetch("/api/analytics/slow-request", {
method: "POST",
body: { url, duration }
});
}
}
});
}结论
一句话总结:$fetch 用于突变和直接客户端调用,useFetch 用于带 SSR 的简单数据获取,useAsyncData 用于复杂的异步逻辑和完全的状态控制。
选对工具的关键在于判断这次请求需不需要状态传输。理解了这一点,双重获取的坑就不会再踩,Nuxt 4 的通用渲染能力也能真正发挥出来。
参考来源是 Nuxt 官方文档 和社区实践。