Nuxt 4:$fetch、useFetch 和 useAsyncData - 完整数据获取指南
Nuxt 中的数据获取管理分为 三个层级,每个工具都有不同的架构角色:
| 工具 | 架构角色 | 主要目的 | SSR 状态传输(水合) |
|---|---|---|---|
| $fetch | 低级实用程序(原始 HTTP 客户端) | 执行纯 HTTP 网络请求。 | 否。 如果在 SSR 组件中直接使用,会导致双重 获取。 |
| useAsyncData | 状态原语(核心原语) | 管理异步状态的生命周期并保证服务器和客户端之间的数据传输。 | 是。 捕获结果,将其序列化到 Nuxt 的 payload 中,并避免双重请求。 |
| useFetch | 便利包装器(开发者包装器) | 简化从单个 HTTP 端点 获取数据的最常见用例。 | 是。 在内部与 useAsyncData 一起工作,继承其功能。 |
useFetch 本质上是实现 useAsyncData 与 $fetch 的语法糖(快捷方式)。
何时使用每个工具
选择从根本上取决于操作的架构上下文:
1. useFetch:简单的初始数据检索(GET)
建议对通过 GET 请求从单个 端点 进行初始数据检索使用 useFetch,特别是当您需要服务器端渲染(SSR)支持并希望避免双重数据获取时。它是语法最简洁的选项。
清晰示例: 获取页面的文章列表。
// app/pages/posts.vue
<script setup lang="ts">
// 负责在服务器上进行一次请求并传输状态。
const { data: posts } = await useFetch('/api/posts')
</script>2. useAsyncData:复杂的异步逻辑或非 HTTP 数据源
当异步操作不是来自单个 端点 的简单 HTTP 调用时,必须使用 useAsyncData。它是包装任何异步 promise 的主要工具。
清晰示例:
-
并行调用: 协调多个同时的
$fetch调用并组合它们。const { data } = await useAsyncData("dashboard", async () => { const [user, stats] = await Promise.all([$fetch("/api/user"), $fetch("/api/stats")]); return { user, stats }; }); -
使用第三方客户端: 集成不使用
$fetch的客户端,如 GraphQL 客户端或数据库 ORM。// myGetFunction 是您的数据客户端中的自定义函数 const { data } = await useAsyncData("users", () => myGetFunction("users"));
3. $fetch:突变和客户端/服务器驱动的操作
明确建议对从根本上是 客户端 的交互使用 $fetch,这些交互由用户事件(如点击或表单提交)驱动。它也是不需要 SSR 状态同步的低级实用程序调用的最佳选项。
清晰示例:
-
突变(POST/PUT/DELETE): 发送表单数据。对突变使用
useFetch通常效率低下,因为它不会从状态水合中受益。// 在事件处理程序内使用 async function submitForm() { await $fetch("/api/contact", { method: "POST", body: formData.value }); } -
内部服务器路由调用: 在 Nuxt 的 API 路由内(例如
server/api),$fetch避免 HTTP 请求并直接调用处理程序函数,优化服务器端速度。
优势和劣势
| 工具 | 优势 | 劣势 |
|---|---|---|
| $fetch | 内部调用优化: 调用内部服务器路由而不进行 HTTP 往返(延迟节省)。 适合突变: 直接和简单的客户端操作使用。 | 双重 获取 风险: 如果在没有 useAsyncData 的组件中使用,客户端会在水合期间再次执行请求。 |
| useAsyncData | 完全控制: 允许包装 任何 异步逻辑(不仅仅是 HTTP)。 SSR 友好: 保证数据仅在服务器上获取一次并传输到客户端(防止 双重获取)。 完整响应状态: 返回 data、pending、error 和 status(指示状态的字符串:"idle"、"pending"、"success"、"error")。 Nuxt 4 优化: data 值默认为 shallowRef(浅响应),减少大型数据结构的 CPU 开销,提高性能。 | 更冗长: 需要显式定义处理程序和唯一键。 |
| useFetch | 简单语法(DX): 最简单的 SSR 安全请求方式。 自动 键 机制: 根据 URL 和选项自动生成键。 强类型: 可以从服务器路由推断响应类型和 URL 类型。 | 有限: 仅适用于对单个 HTTP 端点 的调用。不建议在 setup 阶段对突变(POST、PUT、DELETE)使用。 |
何时从客户端 vs 服务器端使用
这里的关键区别是 状态传输 的需要,用于通用渲染(SSR),这可以防止"双重数据获取"。
服务器端(SSR)和通用渲染
当使用 Nuxt 进行服务器端渲染并且您需要数据出现在初始 HTML 中(对 SEO 和性能至关重要)时,您必须使用通过 payload 处理状态传输的函数:
| 上下文 | 推荐工具 | 原因 |
|---|---|---|
| 初始数据检索(GET) 在页面/组件中。 | useFetch 或 useAsyncData | 确保数据仅在服务器上获取一次并在客户端水合,避免双重请求。 |
| 内部 API 路由调用(来自服务器代码)。 | $fetch | Nuxt 拦截调用并直接执行,不进行真正的 HTTP 请求,速度更快。 |
警告: 如果您仅对服务器端渲染组件(<script setup>)中的初始数据获取使用 $fetch,客户端将在水合期间再次执行网络请求,导致双重 获取。
客户端
在水合后或响应用户交互执行的操作应使用低级实用程序:
| 上下文 | 推荐工具 | 原因 |
|---|---|---|
| 突变(POST、PUT、DELETE) 由用户事件触发。 | $fetch | 这是进行直接客户端 HTTP 调用的首选方式。状态不需要从服务器传输,因此 useAsyncData 开销是不必要的。 |
| 非 SEO 关键数据 或客户端依赖数据(例如地理位置)。 | useFetch/useAsyncData 带 server: false | 这禁用服务器上的处理程序执行,推迟数据获取直到客户端水合完成。 |
| 刷新现有数据 或执行延迟函数。 | refresh()/execute()(由 useFetch/useAsyncData 返回) | 允许手动重新执行数据获取逻辑,无需使用 $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. 唯一键策略
// ✅ 良好的键策略
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 通过默认对数据使用 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 和 useAsyncData 之间做出正确选择对于构建高效且可维护的 Nuxt 4 应用程序至关重要:
$fetch用于突变和直接客户端调用useFetch用于带 SSR 的简单数据获取useAsyncData用于复杂的异步逻辑和完全的状态控制
理解这些差异不仅可以防止双重获取等常见问题,还允许您充分利用 Nuxt 4 的通用渲染功能,构建更快、更高效且用户体验更好的应用程序。
本指南基于官方 Nuxt 4 文档和社区建立的最佳实践。要深入了解,请查阅 官方 Nuxt 文档 和 Nuxt 仓库中的示例。