useFetch
useFetch 是 Nuxt 中最常用的数据获取组合式函数,提供了 SSR 友好的数据获取方式。
为什么用 useFetch 而不是直接用 $fetch 或 axios?
| 问题 | $fetch / axios | useFetch |
|---|---|---|
| SSR 时数据不会传给客户端 | ❌ 客户端重复请求 | ✅ 自动通过 Payload 传递 |
| 数据不是响应式的 | ❌ 需要手动 ref 包裹 | ✅ 返回 Ref,自动响应式 |
| 多组件重复请求同一数据 | ❌ 各请求各的 | ✅ 同 key 自动共享 |
| 加载状态管理 | ❌ 手动管理 pending | ✅ 自动提供 status/pending |
| 错误处理 | ❌ 手动 try/catch | ✅ 自动提供 error ref |
useFetch 做了什么?
- SSR 时:在服务端发起请求,获取数据用于渲染 HTML
- 将数据序列化到 Payload 中(嵌入 HTML)
- 客户端 Hydration 时:直接使用 Payload 数据,不再重复请求
- 后续客户端导航时:在客户端发起新请求
这就是"SSR 友好"的含义——同一个 useFetch 调用,在服务端和客户端都能正确工作。
基本用法
<script setup>
const { data, status, error, refresh, clear } = await useFetch('/api/users')
</script>
<template>
<div v-if="status === 'pending'">加载中...</div>
<div v-else-if="error">{{ error.message }}</div>
<div v-else>
<ul>
<li v-for="user in data" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>await 在这里做了什么?
- 在 SSR 时:等待数据返回后渲染 HTML(阻塞渲染,但确保页面有数据)
- 在客户端首次加载时:从 Payload 读取数据(不阻塞)
- 在客户端导航时:等待数据返回
status 的 4 个值
idle:还未开始(immediate: false时)pending:正在加载success:加载成功error:加载失败
返回值
| 属性 | 类型 | 说明 | 使用场景 |
|---|---|---|---|
data | Ref<T | undefined> | 响应数据 | 模板中显示数据 |
status | Ref<string> | 状态:idle/pending/success/error | 显示加载/错误状态 |
error | Ref<Error | undefined> | 错误信息 | 显示错误消息 |
pending | Ref<boolean> | 是否正在加载 | 等价于 status === 'pending' |
refresh | (opts?) => Promise<void> | 刷新数据 | 用户手动刷新 |
execute | (opts?) => Promise<void> | refresh 的别名 | 同上 |
clear | () => void | 清除数据 | 重置数据状态 |
data 是 Ref
模板中自动解包 在 <template> 中直接用 data,在 <script> 中要用 data.value。
INFO
️ 这是最常见的初学者错误
| 位置 | 正确写法 | 错误写法 | 结果 |
|---|---|---|---|
<template> | {{ data?.message }} | {{ data.value?.message }} | .value 多余但不会报错 |
<script> | data.value.message | data.message | ❌ 返回 undefined,因为 data 是 Ref 对象 |
原因:Vue 在模板中会自动解包 ref(data → data.value),但在 <script> 中不会。如果你在 <script> 中写 data.message,实际上访问的是 Ref 对象上的 message 属性(不存在),而不是响应式数据的 message 字段。
常用选项
method / query / body
// GET 请求带查询参数
const { data } = await useFetch('/api/users', {
query: { page: 1, size: 20 },
})
// POST 请求带请求体
const { data } = await useFetch('/api/users', {
method: 'POST',
body: { name: 'Alice', email: 'alice@example.com' },
})query vs params
query→ URL 查询参数,如/api/users?page=1&size=20body→ 请求体,如 POST 的 JSON 数据- 没有
params选项——路由参数直接写在 URL 中
headers
const { data } = await useFetch('/api/me', {
headers: {
Authorization: `Bearer ${token.value}`,
},
})INFO
️ 注意:headers 中的值在 SSR 时也会被发送 如果 token 是用户特定的,确保 SSR 时也能正确获取
baseURL
const { data } = await useFetch('/users', {
baseURL: 'https://api.example.com',
})推荐用 createUseFetch 代替手动设置 baseURL
这样所有请求都自动带上。
lazy
不阻塞导航,数据加载完后才显示:
const { data } = await useFetch('/api/heavy-data', {
lazy: true,
})或使用 useLazyFetch:
const { data, pending } = useLazyFetch('/api/heavy-data')lazy: true 的效果
页面立刻显示,data 初始为 undefined,需要用 pending 显示加载状态。适合非关键数据。
server
是否在服务端获取数据:
// 仅在客户端获取(SSR 时不请求)
const { data } = await useFetch('/api/client-only', {
server: false,
})什么时候用 server: false?
- 不需要 SEO 的数据(用户个人信息、后台数据)
- 依赖浏览器 API 的数据
- 用户特定数据(SSR 时不知道是哪个用户)
INFO
️ server: false 时 data 在 SSR 时为 undefined 需要处理这种情况或用 default 设置默认值
immediate
是否立即执行(默认 true):
const { data, execute } = await useFetch('/api/search', {
immediate: false, // 不立即执行
})
// 手动触发
function search() {
execute()
}典型场景
搜索功能——页面加载时不搜索,用户输入关键词后点击"搜索"才执行。
watch
监听响应式源的变化,自动重新获取:
const searchQuery = ref('')
const { data } = await useFetch('/api/search', {
query: { q: searchQuery },
watch: [searchQuery], // searchQuery 变化时自动刷新
})watch 的注意事项
- 只有
watch数组中的 ref 变化才会触发重新请求 query中的 ref 不会自动触发——必须显式加入watch- 可以传
false禁用自动监听
常见模式
分页 + 搜索
const page = ref(1)
const keyword = ref('')
const { data } = await useFetch('/api/users', {
query: { page, q: keyword },
watch: [page, keyword], // 任一变化都刷新
})transform
转换响应数据:
const { data } = await useFetch('/api/users', {
transform: (users) => {
return users.map(u => ({ ...u, fullName: `${u.firstName} ${u.lastName}` }))
},
})transform vs 在模板中处理
transform:在数据传递到 Payload 前处理,减少 Payload 大小- 模板中处理:数据全量传递,在客户端处理
如果数据量很大
推荐用 transform 或 pick 减少传递的数据 。
pick
只提取部分字段:
const { data } = await useFetch('/api/user/1', {
pick: ['name', 'email'], // data 只包含 name 和 email
})pick 的好处
API 返回了所有字段(包括密码哈希等),但客户端只需要 name 和 email。pick 确保只有这些字段被传递到 Payload 中,减少数据量和安全风险。
default
设置默认值(数据加载前):
const { data } = await useFetch('/api/users', {
default: () => [], // data 默认为空数组
})为什么需要 default?
data 初始值是 undefined,模板中 v-for="user in data" 会报错。设置 default: () => [] 后,data 初始为空数组,不会报错。
dedupe
避免重复请求:
const { data } = await useFetch('/api/users', {
dedupe: 'cancel', // 取消之前的请求(默认)
// dedupe: 'defer', // 等待之前的请求完成,共享结果
})cancel vs defer
cancel:新请求到来时,取消上一个未完成的请求。适合数据可能频繁变化的场景。defer:如果已有相同 key 的请求在进行中,等待它完成并共享结果。适合数据不常变化的场景。
响应式 URL
使用计算属性或 ref 作为 URL,URL 变化时自动重新获取:
const route = useRoute()
const id = computed(() => route.params.id as string)
const { data: post } = await useFetch(() => `/api/posts/${id.value}`)为什么用函数形式?
直接写 /api/posts/${id.value} 时,URL 是静态字符串(只计算一次)。用函数形式 () => ... 时,URL 会在每次请求时重新计算,响应式变量的变化能被捕获。
类型化请求
interface User {
id: number
name: string
}
// 自动类型推断(基于 API 路由的返回值)
const { data } = await useFetch('/api/users')
// 手动指定类型(API 路由推断不了时)
const { data } = await useFetch<User[]>('/api/users')
// 泛型 + transform
const { data } = await useFetch<User[], string[]>({
url: '/api/users',
transform: (users) => users.map(u => u.name),
})拦截器
const { data } = await useFetch('/api/login', {
onRequest({ request, options }) {
options.headers.set('Authorization', `Bearer ${token.value}`)
},
onRequestError({ request, options, error }) {
console.error('请求错误', error)
},
onResponse({ request, response, options }) {
console.log('响应状态', response.status)
},
onResponseError({ request, response, options }) {
console.error('响应错误', response.status)
},
})拦截器 vs createUseFetch
拦截器适合单个请求的定制。如果你有多个请求需要相同的拦截逻辑(如添加鉴权头),推荐用 createUseFetch 创建自定义实例。
createUseFetch(v4.4+)
创建自定义的 useFetch 实例,预设默认选项:
// app/composables/useApiFetch.ts
export const useApiFetch = createUseFetch((options) => {
const config = useRuntimeConfig()
return {
...options,
baseURL: options.baseURL ?? config.public.apiBase,
headers: {
...options.headers,
Authorization: `Bearer ${useCookie('token').value}`,
},
}
})使用方式和 useFetch 完全一样:
const { data } = await useApiFetch('/users')createUseFetch 解决了什么问题?
- 每次请求都手动传
baseURL和headers?→ 创建自定义实例,预设这些选项 - 所有请求都要鉴权头?→ 在自定义实例中统一添加
- 401 自动跳转登录?→ 在自定义实例中统一处理
刷新数据
const { data, refresh } = await useFetch('/api/users')
// 手动刷新
await refresh()
// 强制刷新(忽略缓存)
await refresh({ dedupe: false })什么时候需要手动刷新?
- 用户执行了操作后(如删除了一篇文章,需要刷新列表)
- 轮询更新数据
- 用户点击"刷新"按钮
常见问题
数据为 null?
如果 server: false,在 Hydration 完成前 data 为 null:
const { data } = await useFetch('/api/data', { server: false })
// 在服务端:data.value 为 undefined
// 在客户端 Hydration 后:data.value 才有值TIP
解决方案:用 default: () => [] 设置默认值 或用 pending 显示加载状态
Hydration 不匹配?
避免在 useFetch 的 URL 中使用动态时间或随机值。
与 VueUse 的 useFetch 冲突?
确保不要 import { useFetch } from '@vueuse/core',Nuxt 的 useFetch 会自动导入。
知识脉络
视图与布局 → 你在这里:useFetch
│
├─→ 下一步:useAsyncData
│
└─→ 相关:$fetch(非响应式请求)