Nuxt 服务端数据获取完全指南
概述
数据获取是 Nuxt 应用的核心功能。Nuxt 3 提供了 useFetch、useAsyncData 和 $fetch 三种数据获取方式,它们在服务端和客户端的行为有所不同。正确使用这些工具可以实现高效的 SSR 数据预取,避免水合不匹配问题。
核心概念
Nuxt 数据获取体系
Nuxt 3 数据获取工具对比
useFetch
├── 封装了 useAsyncData + $fetch
├── 适合:获取 API 数据
├── 自动处理:SSR、缓存、去重
└── 最常用的数据获取方式
useAsyncData
├── 通用的异步数据获取
├── 适合:非 HTTP 请求的异步操作
├── 可自定义:获取逻辑
└── 更灵活的控制
$fetch
├── 基于 ofetch 的 HTTP 客户端
├── 适合:事件处理器、非组件代码
├── 无 SSR 集成:不会自动传输到客户端
└── 轻量级请求
服务端渲染中的数据流
SSR 数据获取流程
服务端
├── 1. 接收请求
├── 2. 执行 useFetch/useAsyncData
├── 3. 等待数据返回
├── 4. 使用数据渲染 HTML
├── 5. 将数据序列化到 payload
└── 6. 发送 HTML + payload
客户端(水合)
├── 1. 接收 HTML
├── 2. 解析 payload
├── 3. useFetch/useAsyncData 直接使用 payload
├── 4. 无需重复请求
└── 5. 完成水合
useFetch 详解
基本用法
<script setup>
// 基础用法
const { data, pending, error, refresh } = await useFetch('/api/users')
// 带参数
const { data: user } = await useFetch(`/api/users/${userId}`)
// 完整配置
const { data, pending, error, refresh, status } = await useFetch('/api/posts', {
// 请求方法
method: 'GET',
// 查询参数
query: {
page: 1,
limit: 10
},
// 请求头
headers: {
'Authorization': `Bearer ${token}`
},
// 请求体(POST/PUT)
body: {
title: 'New Post'
},
// 缓存键(用于去重和缓存)
key: 'posts-page-1',
// 只在服务端获取
server: true,
// 懒加载(不阻塞导航)
lazy: false,
// 立即执行(默认 true)
immediate: true,
// 数据转换
transform: (data) => data.items,
// 默认值
default: () => []
})
</script>
<template>
<div>
<div v-if="pending">加载中...</div>
<div v-else-if="error">错误: {{ error.message }}</div>
<ul v-else>
<li v-for="post in data" :key="post.id">{{ post.title }}</li>
</ul>
</div>
</template>
响应式参数
<script setup>
const page = ref(1)
const category = ref('all')
// 方法 1:使用函数返回 URL
const { data: posts } = await useFetch(() => `/api/posts?page=${page.value}`)
// 方法 2:使用响应式 query
const { data: articles } = await useFetch('/api/articles', {
query: {
page, // 直接使用 ref
category, // 会自动追踪变化
limit: 10
}
})
// 方法 3:使用计算属性
const apiUrl = computed(() => `/api/users/${userId.value}/posts`)
const { data: userPosts } = await useFetch(apiUrl)
// 当参数变化时,会自动重新获取数据
function nextPage() {
page.value++ // 自动触发请求
}
</script>
请求拦截
<script setup>
const { data } = await useFetch('/api/protected', {
// 请求前拦截
onRequest({ request, options }) {
// 添加认证头
options.headers = options.headers || {}
options.headers.Authorization = `Bearer ${useAuthToken().value}`
console.log('发起请求:', request)
},
// 请求错误拦截
onRequestError({ request, error }) {
console.error('请求失败:', error)
},
// 响应拦截
onResponse({ response }) {
// 处理响应
console.log('响应状态:', response.status)
},
// 响应错误拦截
onResponseError({ response }) {
if (response.status === 401) {
// 跳转到登录页
navigateTo('/login')
}
}
})
</script>
条件获取
<script setup>
const userId = ref(null)
// 只有当 userId 存在时才获取
const { data: user } = await useFetch(() => `/api/users/${userId.value}`, {
// 控制是否立即执行
immediate: !!userId.value,
// 使用 watch 选项控制
watch: [userId]
})
// 或使用 watch: false 完全禁用自动获取
const { data: profile, execute } = await useFetch('/api/profile', {
immediate: false,
watch: false
})
// 手动触发
function loadProfile() {
execute()
}
</script>
useAsyncData 详解
基本用法
<script setup>
// 基础用法
const { data, pending, error, refresh } = await useAsyncData(
'users', // 唯一键
() => $fetch('/api/users') // 异步函数
)
// 完整配置
const { data, pending, error, refresh, status } = await useAsyncData(
'posts',
async () => {
const posts = await $fetch('/api/posts')
const comments = await $fetch('/api/comments')
return { posts, comments }
},
{
// 缓存键(可选,默认使用第一个参数)
key: 'posts-with-comments',
// 只在服务端执行
server: true,
// 懒加载
lazy: false,
// 立即执行
immediate: true,
// 数据转换
transform: (result) => result.posts,
// 选择性获取(优化 payload 大小)
pick: ['id', 'title', 'summary'],
// 默认值
default: () => ({ posts: [], comments: [] }),
// 监听依赖
watch: [someRef]
}
)
</script>
复杂数据获取场景
<script setup>
// 场景 1:聚合多个 API
const { data: dashboard } = await useAsyncData('dashboard', async () => {
const [stats, recentOrders, topProducts] = await Promise.all([
$fetch('/api/stats'),
$fetch('/api/orders/recent'),
$fetch('/api/products/top')
])
return { stats, recentOrders, topProducts }
})
// 场景 2:依赖其他数据
const { data: user } = await useFetch('/api/user')
const { data: userPosts } = await useAsyncData(
'user-posts',
() => $fetch(`/api/users/${user.value.id}/posts`),
{
watch: [user],
immediate: !!user.value
}
)
// 场景 3:非 HTTP 异步操作
const { data: config } = await useAsyncData('config', async () => {
// 从本地存储或其他来源获取
if (process.server) {
return await readServerConfig()
}
return loadClientConfig()
})
</script>
缓存策略
<script setup>
// getCachedData 允许自定义缓存行为
const { data } = await useAsyncData('cached-data',
() => $fetch('/api/data'),
{
getCachedData(key) {
// 返回 undefined 表示需要重新获取
// 返回数据表示使用缓存
const nuxtApp = useNuxtApp()
const cachedData = nuxtApp.payload.data[key] || nuxtApp.static.data[key]
if (!cachedData) {
return undefined
}
// 检查缓存是否过期(示例:1 小时)
const expirationTime = 60 * 60 * 1000
const now = Date.now()
if (cachedData._fetchedAt && now - cachedData._fetchedAt > expirationTime) {
return undefined
}
return cachedData
},
transform(data) {
return {
...data,
_fetchedAt: Date.now()
}
}
}
)
</script>
useFetch vs useAsyncData
选择指南
// 选择决策树
const chooseDataFetchMethod = {
// 使用 useFetch 的场景
useFetch: [
'获取 REST API 数据',
'简单的 CRUD 操作',
'需要自动处理 baseURL',
'大多数标准数据获取场景'
],
// 使用 useAsyncData 的场景
useAsyncData: [
'需要聚合多个请求',
'非 HTTP 异步操作',
'需要自定义获取逻辑',
'需要更细粒度的控制'
],
// 使用 $fetch 的场景
'$fetch': [
'事件处理器中的请求',
'不需要 SSR 的客户端请求',
'非组件代码(utils、stores)',
'服务端 API 路由中'
]
}
代码对比
<script setup>
// 场景:获取用户列表
// 方式 1:useFetch(推荐)
const { data: users } = await useFetch('/api/users')
// 方式 2:useAsyncData + $fetch(等效)
const { data: users2 } = await useAsyncData(
'users',
() => $fetch('/api/users')
)
// 场景:复杂数据获取
// 方式 1:useAsyncData(更适合)
const { data: dashboard } = await useAsyncData('dashboard', async () => {
const [users, orders, stats] = await Promise.all([
$fetch('/api/users'),
$fetch('/api/orders'),
$fetch('/api/stats')
])
return { users, orders, stats }
})
// 方式 2:多个 useFetch(并行但独立)
const { data: users } = useFetch('/api/users')
const { data: orders } = useFetch('/api/orders')
const { data: stats } = useFetch('/api/stats')
// 需要等待所有完成
await Promise.all([users, orders, stats])
</script>
$fetch 使用场景
事件处理中使用
<script setup>
const loading = ref(false)
// 事件处理器中使用 $fetch
async function submitForm(formData) {
loading.value = true
try {
const result = await $fetch('/api/submit', {
method: 'POST',
body: formData
})
// 处理成功
console.log('提交成功:', result)
} catch (error) {
// 处理错误
console.error('提交失败:', error)
} finally {
loading.value = false
}
}
// 删除操作
async function deleteItem(id) {
await $fetch(`/api/items/${id}`, {
method: 'DELETE'
})
// 刷新列表
await refreshItems()
}
</script>
服务端 API 路由中使用
// server/api/aggregate.ts
export default defineEventHandler(async (event) => {
// 在服务端 API 中使用 $fetch 请求其他服务
const [users, products] = await Promise.all([
$fetch('https://api.example.com/users'),
$fetch('https://api.example.com/products')
])
return {
users,
products,
timestamp: Date.now()
}
})
工具函数中使用
// utils/api.ts
export const api = {
async getUsers(params?: { page?: number; limit?: number }) {
return await $fetch('/api/users', {
query: params
})
},
async createUser(userData: CreateUserInput) {
return await $fetch('/api/users', {
method: 'POST',
body: userData
})
},
async updateUser(id: string, userData: UpdateUserInput) {
return await $fetch(`/api/users/${id}`, {
method: 'PUT',
body: userData
})
},
async deleteUser(id: string) {
return await $fetch(`/api/users/${id}`, {
method: 'DELETE'
})
}
}
// 在组件中使用
// const users = await api.getUsers({ page: 1 })
高级用法
数据刷新策略
<script setup>
// 基础刷新
const { data, refresh, pending } = await useFetch('/api/data')
// 手动刷新
async function handleRefresh() {
await refresh()
}
// 定时刷新
const refreshInterval = ref(null)
onMounted(() => {
refreshInterval.value = setInterval(() => {
refresh()
}, 30000) // 每 30 秒刷新
})
onUnmounted(() => {
clearInterval(refreshInterval.value)
})
// 条件刷新
const shouldRefresh = ref(false)
watch(shouldRefresh, (newVal) => {
if (newVal) {
refresh()
shouldRefresh.value = false
}
})
</script>
乐观更新
<script setup>
const { data: todos, refresh } = await useFetch('/api/todos')
async function toggleTodo(todo) {
// 乐观更新:先更新 UI
const originalStatus = todo.completed
todo.completed = !todo.completed
try {
// 发送请求
await $fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
body: { completed: todo.completed }
})
} catch (error) {
// 失败时回滚
todo.completed = originalStatus
console.error('更新失败:', error)
}
}
async function addTodo(title) {
// 创建临时项
const tempId = `temp-${Date.now()}`
const newTodo = { id: tempId, title, completed: false }
// 乐观添加
todos.value.push(newTodo)
try {
const created = await $fetch('/api/todos', {
method: 'POST',
body: { title }
})
// 替换临时 ID
const index = todos.value.findIndex(t => t.id === tempId)
todos.value[index] = created
} catch (error) {
// 失败时移除
todos.value = todos.value.filter(t => t.id !== tempId)
}
}
</script>
分页与无限滚动
<script setup>
const page = ref(1)
const allPosts = ref([])
const { data, pending } = await useFetch('/api/posts', {
query: {
page,
limit: 10
},
transform: (response) => response.data
})
// 监听数据变化,累积结果
watch(data, (newPosts) => {
if (newPosts) {
if (page.value === 1) {
allPosts.value = newPosts
} else {
allPosts.value = [...allPosts.value, ...newPosts]
}
}
})
// 无限滚动实现
function loadMore() {
page.value++
}
// 使用 Intersection Observer
const loadMoreRef = ref(null)
onMounted(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !pending.value) {
loadMore()
}
})
if (loadMoreRef.value) {
observer.observe(loadMoreRef.value)
}
})
</script>
<template>
<div>
<div v-for="post in allPosts" :key="post.id">
{{ post.title }}
</div>
<div ref="loadMoreRef">
<span v-if="pending">加载中...</span>
</div>
</div>
</template>
错误处理
<script setup>
const { data, error, refresh } = await useFetch('/api/data', {
// 自定义错误处理
onResponseError({ response }) {
const statusCode = response.status
switch (statusCode) {
case 401:
navigateTo('/login')
break
case 403:
throw createError({
statusCode: 403,
message: '没有权限访问此资源'
})
case 404:
throw createError({
statusCode: 404,
message: '资源不存在'
})
case 500:
// 记录错误
console.error('服务器错误:', response._data)
break
}
}
})
// 全局错误处理(通过插件)
// plugins/fetch-error.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('app:error', (error) => {
console.error('应用错误:', error)
// 可以上报到错误监控服务
})
})
</script>
<template>
<div>
<template v-if="error">
<div class="error-container">
<h2>出错了</h2>
<p>{{ error.message }}</p>
<button @click="refresh">重试</button>
</div>
</template>
<template v-else>
<!-- 正常内容 -->
</template>
</div>
</template>
性能优化
减少 Payload 大小
<script setup>
// 使用 pick 只选择需要的字段
const { data: posts } = await useFetch('/api/posts', {
pick: ['id', 'title', 'summary', 'createdAt']
// 完整的 post 对象可能有 20+ 个字段
// 只传输需要的字段可以显著减少 payload
})
// 使用 transform 处理数据
const { data: users } = await useFetch('/api/users', {
transform: (users) => users.map(u => ({
id: u.id,
name: u.name,
avatar: u.avatar
}))
})
</script>
并行请求优化
<script setup>
// ❌ 串行请求(慢)
const { data: user } = await useFetch('/api/user')
const { data: posts } = await useFetch('/api/posts')
const { data: comments } = await useFetch('/api/comments')
// 总时间 = user + posts + comments
// ✅ 并行请求(快)
const [
{ data: user },
{ data: posts },
{ data: comments }
] = await Promise.all([
useFetch('/api/user'),
useFetch('/api/posts'),
useFetch('/api/comments')
])
// 总时间 = max(user, posts, comments)
// ✅ 或使用 useAsyncData 聚合
const { data } = await useAsyncData('page-data', async () => {
const [user, posts, comments] = await Promise.all([
$fetch('/api/user'),
$fetch('/api/posts'),
$fetch('/api/comments')
])
return { user, posts, comments }
})
</script>
懒加载非关键数据
<script setup>
// 关键数据:阻塞渲染(SSR)
const { data: mainContent } = await useFetch('/api/main')
// 非关键数据:懒加载(不阻塞)
const { data: relatedPosts, pending: loadingRelated } = useFetch('/api/related', {
lazy: true // 不阻塞页面渲染
})
const { data: comments, pending: loadingComments } = useFetch('/api/comments', {
lazy: true,
server: false // 完全在客户端获取
})
</script>
<template>
<div>
<!-- 主要内容立即显示 -->
<main>{{ mainContent }}</main>
<!-- 相关文章懒加载 -->
<aside>
<div v-if="loadingRelated">加载相关文章...</div>
<RelatedPosts v-else :posts="relatedPosts" />
</aside>
<!-- 评论客户端加载 -->
<section>
<div v-if="loadingComments">加载评论...</div>
<Comments v-else :comments="comments" />
</section>
</div>
</template>
常见问题解答
Q: useFetch 和 useAsyncData 的主要区别是什么?
A: useFetch 是 useAsyncData + $fetch 的封装,专门用于 HTTP 请求。useAsyncData 更通用,可以用于任何异步操作。
Q: 为什么不能在事件处理器中使用 useFetch?
A: useFetch 需要在组件 setup 阶段调用,以便进行 SSR 数据传输。事件处理器在用户交互时触发,此时应使用 $fetch。
Q: 如何避免数据重复请求? A: Nuxt 会自动根据 key 去重。确保使用一致的 key,或让 Nuxt 自动生成(基于 URL 和参数)。
Q: 如何处理认证 token? A: 使用请求拦截器(onRequest)动态添加 Authorization 头,或创建自定义的 $fetch 实例。
总结
Nuxt 3 的数据获取体系强大而灵活:
- useFetch - 大多数 API 请求的首选
- useAsyncData - 复杂异步场景的选择
- $fetch - 事件处理和非组件代码
最佳实践:
- 使用响应式参数实现自动刷新
- 合理使用 lazy 和 server 选项
- 通过 pick/transform 减少 payload
- 使用 Promise.all 并行请求
- 实现恰当的错误处理


