ofetch — Nuxt 默认的现代 fetch 包装
是什么
ofetch 是一个给原生 fetch 套一层”自动收拾东西”的外壳。日常类比:像外卖打包袋——网络层还是原生 fetch,外面那层帮你把饭盒摞好、忘记给筷子(Content-Type)时自动塞一双、送到一半摔了(5xx)再送一次。
你写:
import { ofetch } from "ofetch";const user = await ofetch<User>("/api/users/1");没 .json()、没手动 JSON.stringify(body)、没 try/catch 4xx——全是默认行为。它跨 Node 18+、浏览器、Deno、Bun、Cloudflare Worker 跑同一份代码。Nuxt 3 项目里那个全局 $fetch,就是 ofetch 的实例。
为什么重要
不理解 ofetch,下面这些事都没法解释:
- 为什么 Nuxt 文档几乎不再提 ofetch 这个名字——它被 framework 起了个别名
$fetch接管了 - 为什么大家不直接用原生 fetch——4xx 不抛错、没 retry、body 要手 stringify,业务代码最后都会自己写一个”半成品 ofetch”
- 为什么 axios 在 Cloudflare Worker 跑不起来——它依赖 Node 的 http 模块,edge runtime 没有
- 为什么 axios / ky / ofetch 三家在同一赛道却长得不像——选了不同的姿势包同一件事
核心要点
ofetch 干的事可以拆成 三件:
-
薄包装:网络层 100% 交给原生 fetch,自己只加胶水。bundle 大约 7 KB(min+gzip),相比 axios 的 17 KB。类比:不重造发动机,只装个方向盘套。
-
智能解析:拿到响应后看 Content-Type 自动 json / text / blob,JSON 用 destr 安全 parse——遇到不合法 JSON 不抛异常,回退到原始 text。类比:拆快递时先看箱子上的标签决定怎么开。
-
Hooks 而非 Interceptors:在
ofetch.create({ onRequest, onResponse, onRequestError, onResponseError })时声明四个钩子,运行期不变。类比:开店前贴好”进门必须洗手""离店必须刷卡”的牌子,比每个客人来了再口头交代更稳。
三件事加起来叫 ofetch 内核,剩下的 retry、baseURL、params、SSR cookie 透传都是这三件事的派生。
实践案例
案例 1:POST 一份 JSON 不需要手动 stringify
const created = await ofetch<User>("/api/users", { method: "POST", body: { name: "Alice" } // ← 直接传 object});ofetch 看到 body 是 plain object,自动 JSON.stringify + 自动加 Content-Type: application/json。要传 FormData / URLSearchParams 时,不动它原样直传。这就省掉了原生 fetch 那两句样板代码。
案例 2:建一个带 baseURL 和 retry 的实例
const api = ofetch.create({ baseURL: "https://api.example.com", retry: 3, retryStatusCodes: [408, 425, 429, 500, 502, 503, 504], onRequest({ options }) { options.headers = { ...options.headers, "X-Trace-ID": crypto.randomUUID() }; }});
const data = await api<User[]>("/users"); // 实际请求 https://api.example.com/usersretry 默认只对幂等方法(GET/HEAD/PUT/DELETE)+ 上面那串状态码生效,POST 失败默认不重试——避免重复扣款这种事故。重试间隔走指数退避:第 1 次失败立刻重试,第 2 次等 1s,第 3 次等 2s,避免短时间打爆 server。
案例 2.5:拿原始 Response 看 status / headers
const res = await ofetch.raw<User>("/api/users/1");console.log(res.status, res.headers.get("etag"), res._data);普通 ofetch(url) 直接解包成数据;ofetch.raw(url) 返回完整 FetchResponse,多了 status / statusText / headers / _data 字段。需要做 ETag 缓存、读 Set-Cookie、看 304 状态码时用这个。
案例 3:在 Nuxt 里用 $fetch
<script setup>const data = await $fetch<User[]>("/api/users");</script>$fetch 不是 Nuxt 自造的新 API,它就是 ofetch 实例 + 自动 baseURL(指向当前站自己的 server route) + SSR 阶段透传 cookie。Nuxt 的 useFetch / useAsyncData 又在 $fetch 之上包了响应式 + payload 复用——三层是同一条链。
踩过的坑
-
destr 的 fallback 会掩盖 bug:server 误返了 HTML 错误页(Content-Type: text/html),ofetch 按 text 分支拿到一个”看起来是字符串但其实是错误页”的东西,业务代码毫不知情。开发期想严格抛错,需要自己传
parseResponse: JSON.parse。 -
$fetch/useFetch/useAsyncData三层容易搞混:$fetch立即发请求,useFetch在 SSR 阶段把结果塞进 payload、client hydrate 时跳过重发。新人常以为useFetch每次 client 渲染都会重发,结果时间敏感数据不更新。 -
SSR cookie 透传不在 ofetch 里:ofetch 只暴露
onRequesthook,cookie / x-forwarded-for 这些”当前 SSR 请求上下文”的拼装放在 Nuxt。换 SvelteKit / Astro 时这套胶水得自己重写一遍。 -
没有官方 mock 方案:axios 有 axios-mock-adapter(拦 XHR 内部),ofetch 走原生 fetch 没法这么做,只能上 playwright 或 msw 走 service worker 拦截。
-
timeout 不是默认的:原生 fetch 没有 timeout 概念(要用 AbortController),ofetch 加了
timeoutoption 但默认不开。生产环境忘记设 timeout,下游 API 卡死会拖垮 SSR 进程——和”以为有默认 timeout”踩坑的人不少。
适用 vs 不适用场景
适用:
- Nuxt 3 / Nitro / 任何基于 h3 的 server——开箱即用,零配置
- edge runtime(Cloudflare Worker / Vercel Edge / Deno Deploy)——只用标准 fetch,不依赖 Node 内建模块
- 想要 axios 那种 DX 但不想引入 17 KB bundle
不适用:
- 维护中的老 axios 项目——interceptor → hooks 是结构性重写,不是 rename
- React / Next.js 项目——社区惯性是 native fetch + react-query 或 axios + react-query
- Node 后端密集流式下载——用 got,它的
got.stream(url).pipe(...)这条路 ofetch 没暴露
历史小故事(可跳过)
- 2022-08:UnJS 的 Pooya Parsa(@pi0,Nuxt core team lead)发布 v0.x,最早叫 ohmyfetch
- 2022-10:改名 ofetch,统一 UnJS 命名(h3 / ufo / destr / ohash 都是这种短名)
- 2023:1.x 稳定,跟着 Nuxt 3 正式版被默认推
- 2026:weekly downloads 4M+,stars 4k+——比起 axios 50M / 105k stars 还差一个量级,但增速快
学到什么
- 框架默认 = 独立库——Nuxt 不重造 HTTP 客户端,只给 ofetch 起别名
$fetch+ 注入 SSR 上下文。这种拆法让 ofetch 在非 Nuxt 项目里也能用,Nuxt 用户也能在需要时摸到底层 - 依赖按职责切——ufo 管 URL 拼接、destr 管 JSON 安全 parse、node-fetch-native 管 polyfill。代价是用户要”理解 N 个小包”
- Hooks > Interceptors——配置式 hooks 比 axios 的
.use()命令式更适合 SSR / 不可变配置 - bundle size 是产品决策——7 KB / 4 KB / 17 KB 看起来都”很小”,但对 edge runtime(CF Worker 1MB cap)和首屏加载都有可衡量影响
- fetch wrapper 是没有终点的赛道——superagent → axios → ky → ofetch,每隔几年都会出”更现代”的,因为 runtime 在变(XHR → fetch → 未来 WebTransport)、框架在变、业务模式在变
延伸阅读
- 文档:unjs.io/packages/ofetch(API 速查 + 例子)
- 源码:github.com/unjs/ofetch(src/ 加起来不到 600 行 TS,半天能读完)
- ky —— 同赛道竞品,零依赖 + 链式 lazy
- axios —— 上一代代表,interceptor 模式 vs hooks 模式对照
- destr —— ofetch 内部依赖,安全 JSON parse
关联
- ky —— 同赛道竞品;ofetch 多了框架集成,少了”零依赖”
- axios —— 老牌 HTTP 客户端;interceptor 模式 vs hooks 模式
- destr —— ofetch 用它做安全 JSON parse
- h3 —— Nuxt 服务端 router;和 ofetch 是 Nitro 的两大支柱
- playwright —— 缺 mock 时的兜底方案之一
反向链接
- axios —— axios — 浏览器和 Node 都能用的 HTTP 客户端
- got —— got — Node 端 HTTP 客户端的瑞士军刀
- ky —— ky — 把浏览器自带的 fetch 包成顺手工具
- playwright —— Playwright — 跨浏览器自动化测试