学习目标
理解为什么不能直接使用
fetch(),useFetch()解决了哪些问题,以及它与useAsyncData、$fetch的分工。学完本章,你应该能够:
- 解释首屏为什么不会请求两次
- 说出
useFetch常见 options 的作用- 知道什么时候该用
$fetch而不是useFetch
一、这个知识解决什么问题?
JavaScript 原生已经提供了 fetch('/api/user'),Vue 项目里也一直这样写。
那么 Nuxt 为什么还要重新设计 useFetch()?如果只是换了名字,那完全没有必要。所以:
useFetch 一定解决了 fetch 解决不了的问题。
(以下基于 Nuxt 3.x 的 API,Nuxt 4 中同名 API 行为一致。)
二、一起推导
假设页面这样写:
const user = await fetch('/api/user')
根据前面几章我们已经知道(浏览器第一次访问到底发生了什么):第一次访问页面时,服务器会执行这段代码,请求 /api/user(第一次)。
但是浏览器下载 JavaScript 后,组件还会执行一次。浏览器又执行了 fetch('/api/user')(第二次)。
同一个接口,请求了两次。
原生 fetch 最大的问题:
它不知道自己正运行在 Nuxt 的 SSR 生命周期里。
如果把请求次数摊开看:
服务器执行组件 → fetch('/api/user') ← 第 1 次
浏览器 Hydration → fetch('/api/user') ← 第 2 次
两次请求拿到的是同一份数据,第二次完全是浪费。
三、useFetch 做了什么?
Nuxt 在 fetch 外面包了一层,形成 useFetch()。除了请求接口,还做了四件事:
第一:支持 SSR
同一份代码,服务器和浏览器都能执行,不需要额外处理。
第二:支持 Payload
服务器请求到的数据,会被放进 Payload 一起发给浏览器。浏览器 Hydration 时直接读取,不再重复请求。
第三:自动管理状态
fetch() 需要自己管理 loading、error、data;useFetch() 直接提供:
const { data, pending, error } = useFetch(...)
第四:自动响应路由变化
当请求参数来自响应式路由参数时,useFetch 可以监听依赖并重新获取数据:
const route = useRoute()
const { data } = useFetch('/api/product', {
query: { id: route.params.id },
watch: [() => route.params.id]
})
四、useFetch、useAsyncData、$fetch 的分工
这三个名字容易混,其实它们是三层:
$fetch(基于 ofetch):负责"发出请求",不管理状态。适合事件处理、表单提交等一次性调用。useAsyncData:负责"管理异步数据的生命周期",不负责发请求,可以包装任何 Promise。useFetch=$fetch+useAsyncData:两者合体,是页面数据获取的默认选择。
// 等价关系
const { data } = await useFetch('/api/user')
// ≈
const { data } = await useAsyncData('user', () => $fetch('/api/user'))
| 能力 | fetch | $fetch | useFetch |
|---|---|---|---|
| HTTP 请求 | ✅ | ✅ | ✅ |
| SSR 支持 | ❌ 自己处理 | ✅ 可调用 | ✅ 自动 |
| 写入 Payload 避免重复请求 | ❌ | ❌ | ✅ |
| pending / error 状态 | ❌ | ❌ | ✅ |
| 响应式 data | ❌ | ❌ | ✅ |
| 跟随参数变化重新请求 | ❌ | ❌ | ✅ |
五、最常用的 options
const { data, pending, error } = await useFetch('/api/user', {
key: 'user-profile', // 自定义 key,影响 Payload 恢复
server: true, // 默认 true:SSR 阶段也请求
lazy: false, // 默认 false:阻塞导航等待数据
default: () => [], // 数据到达前的初始值
transform: (res) => res.items, // 只保留需要的字段,Payload 更小
watch: [() => route.params.id] // 参数变化时自动重新请求
})
关键理解:
key是 Payload 里数据的"身份证"。同一 key 的数据在 SSR 和 Hydration 之间传递。参数变化时,key 不变但watch触发重新请求。lazy: true适合不阻塞首屏的数据,配合pending显示加载状态。transform不只影响页面显示,还会让放进 Payload 的数据变小,是控制 Payload 体积的手段之一。
useFetch 返回的 data 是响应式引用,如果你对 ref 的转换规则还不熟,可参考 Vue 3 响应式:ref、reactive 和 toRefs 怎么选。
六、错误处理
useFetch 遇到 4xx/5xx 不会抛出异常,而是放进 error.value,data.value 为 null:
<script setup>
const { data, error } = await useFetch('/api/user')
</script>
<template>
<div v-if="error">加载失败:{{ error.message }}</div>
<div v-else>{{ data }}</div>
</template>
如果你需要拿到 HTTP 状态码(比如判断 404),用 $fetch.raw:
const res = await $fetch.raw('/api/user')
// res.status → 404
七、什么时候用原生 fetch 反而更合理?
useFetch 的价值在于"和 SSR/Payload 生命周期配合"。以下场景它没有优势:
- 事件处理器里的一次性调用:点击按钮后提交表单,不需要状态管理,用
$fetch或fetch都行。 - 客户端专属逻辑:只在
onMounted里拉取的数据,用$fetch更直接。 - 上传进度等特殊需求:原生
fetch更容易控制。
不是"必须用 useFetch",而是"页面级数据用 useFetch,一次性动作用 $fetch"。
八、脑图
读图时不要只比较"是否能发送 HTTP 请求",而要看完整生命周期:
- 原生
fetch只返回一次网络响应,不知道服务器数据应该如何交给 Hydration useFetch在服务端请求数据后,将结果以稳定 key 写入 Nuxt Payload- 客户端执行到同一个
useFetch时,先用 key 查找 Payload,命中后直接恢复响应式状态 - 后续刷新、响应式参数变化等场景,才会重新执行请求
九、代码实验
创建页面:
<script setup>
const { data, pending, error } = await useFetch('/api/user')
</script>
<template>
<div v-if="pending">加载中</div>
<div v-else-if="error">出错了</div>
<pre v-else>{{ data }}</pre>
</template>
打开 Chrome DevTools → Network,过滤 /api/user:
- 刷新页面:通常只有一次请求。第二次被 Payload 顶掉了。
- 把
server: false加进 options:首屏 Network 里没有请求,页面先显示"加载中",浏览器端才发起请求。 - 试试
watch参数版本:切换路由参数时,Network 里会出现新的请求。
如果看不到效果,先确认你看到的是刷新整页而不是 Nuxt 的客户端导航。想理解这些数据随后如何进入 Payload,见下一章(Payload Cache 到底缓存了什么)。
十、容易踩坑
❌ useFetch 就是 fetch
不是。增加了 SSR、Payload、Cache、响应式、生命周期,已经完全不是一个层级。
❌ useFetch 不会执行两次
实际上 Server 和 Client 都会执行,但 Client 读取的是 Payload,所以不会重复请求网络。
❌ fetch 在 Nuxt 不能用
可以用,只是无法享受 SSR、Payload、Hydration 等配套能力。事件处理里用 $fetch 反而更合适。
❌ 拿不到 HTTP 状态码
useFetch 把错误放进 error.value;要状态码用 $fetch.raw。
❌ 页面数据一定要用 useFetch
页面级数据用它;一次性动作用 $fetch。
一句话记忆
fetch 只会请求数据;useFetch 管理的是整个 Nuxt 数据获取生命周期;$fetch 负责发出请求本身。