08 / 09

为什么 useFetch 不是 fetch?

理解 useFetch 如何连接服务端请求、Payload 与客户端状态,并掌握 useFetch、useAsyncData、$fetch 的分工。

为什么 useFetch 不是 fetch?

学习目标

理解为什么不能直接使用 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() 需要自己管理 loadingerrordatauseFetch() 直接提供:

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] // 参数变化时自动重新请求
})

关键理解:

  1. key 是 Payload 里数据的"身份证"。同一 key 的数据在 SSR 和 Hydration 之间传递。参数变化时,key 不变但 watch 触发重新请求。
  2. lazy: true 适合不阻塞首屏的数据,配合 pending 显示加载状态。
  3. transform 不只影响页面显示,还会让放进 Payload 的数据变小,是控制 Payload 体积的手段之一。

useFetch 返回的 data 是响应式引用,如果你对 ref 的转换规则还不熟,可参考 Vue 3 响应式:ref、reactive 和 toRefs 怎么选


六、错误处理

useFetch 遇到 4xx/5xx 不会抛出异常,而是放进 error.valuedata.valuenull

<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 生命周期配合"。以下场景它没有优势:

  • 事件处理器里的一次性调用:点击按钮后提交表单,不需要状态管理,用 $fetchfetch 都行。
  • 客户端专属逻辑:只在 onMounted 里拉取的数据,用 $fetch 更直接。
  • 上传进度等特殊需求:原生 fetch 更容易控制。

不是"必须用 useFetch",而是"页面级数据用 useFetch,一次性动作用 $fetch"。


八、脑图

fetch 与 useFetch 在 Nuxt SSR 中的数据流对比脑图

读图时不要只比较"是否能发送 HTTP 请求",而要看完整生命周期:

  1. 原生 fetch 只返回一次网络响应,不知道服务器数据应该如何交给 Hydration
  2. useFetch 在服务端请求数据后,将结果以稳定 key 写入 Nuxt Payload
  3. 客户端执行到同一个 useFetch 时,先用 key 查找 Payload,命中后直接恢复响应式状态
  4. 后续刷新、响应式参数变化等场景,才会重新执行请求

九、代码实验

创建页面:

<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

  1. 刷新页面:通常只有一次请求。第二次被 Payload 顶掉了。
  2. server: false 加进 options:首屏 Network 里没有请求,页面先显示"加载中",浏览器端才发起请求。
  3. 试试 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 负责发出请求本身。