ofetch — 以 Fetch 为底座的跨运行时请求包装
已复核ofetch 是一个建立在 Fetch API 上的 HTTP 客户端。日常类比:运输车仍由运行时提供,ofetch 负责整理地址、包装 JSON、解析回包、执行 hook,并按明确规则决定是否再发一次。
你写:
import { ofetch } from "ofetch";const user = await ofetch<User>("/api/users/1");普通调用直接返回解析后的 _data;需要状态码与 headers 时使用 ofetch.raw()。固定版本按导出条件区分浏览器、worker 与 Node 入口,Node 入口在缺少全局 Fetch 时会使用 node-fetch-native。
不理解 ofetch,下面这些事都没法解释:
- 为什么普通调用返回 body,而
.raw()返回带_data的 Response - 为什么 GET 默认可能重试一次,POST/PUT/PATCH/DELETE 默认不重试
- 为什么显式设置
retry后,payload method 也会进入重试 - 为什么 SSR cookie、用户身份和请求去重不属于 ofetch 核心合同
ofetch 的执行链可以拆成五步:
-
合并 options:instance defaults 与本次请求合并,headers、query/params 被单独规范化。
-
运行
onRequest并准备请求:拼接baseURL/query;payload method 的普通对象会被 stringify,并补 JSON headers。 -
调用运行时 Fetch:网络失败先进入
onRequestError;固定版本仅在没有既有signal时用AbortController实现timeout。 -
解析 response body:根据 Content-Type 或
responseType选择 JSON/text/blob/arrayBuffer/stream;JSON 默认使用destr。 -
运行 response hook 并处理错误/retry:
onResponse在状态判断前运行;4xx/5xx 再进入onResponseError和 retry 规则,最终形成FetchError。
案例 1:POST 一份 JSON 不需要手动 stringify
Section titled “案例 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 的实例
Section titled “案例 2:建一个带 baseURL 和 retry 的实例”const api = ofetch.create({ baseURL: "https://api.example.com", retry: 2, retryDelay: 500, retryStatusCodes: [408, 425, 429, 500, 502, 503, 504], onRequest({ options }) { options.headers.set("X-Trace-ID", crypto.randomUUID()); }});
const data = await api<User[]>("/users"); // 实际请求 https://api.example.com/users不显式配置时,非 payload method 默认重试一次,POST/PUT/PATCH/DELETE 默认零次,默认 delay 为 0。这里显式写 retry: 2 后,所有 method 都可能按状态码重试;调用方必须证明副作用可去重。
案例 2.5:拿原始 Response 看 status / headers
Section titled “案例 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:严格解析 JSON
Section titled “案例 3:严格解析 JSON”const data = await ofetch<User>("/api/users/1", { responseType: "json", parseResponse: JSON.parse});固定 1.5.0 默认使用 destr,对部分非标准 JSON 输入会容错。可信边界需要严格 JSON 时,应显式提供 parser;TypeScript 泛型本身不验证数据。
-
把默认 retry 说成幂等 method allowlist:固定 1.5.0 实际按 payload method 分组;PUT/DELETE 默认不重试,显式数值则会覆盖这个默认。
-
以为 retry 默认指数退避:默认
retryDelay是 0。需要退避、jitter 或尊重业务预算时必须显式实现。 -
同时设置 signal 与 timeout:固定实现只在没有
signal时创建 timeout controller;已有 signal 时不能假定timeout仍会生效。 -
把
destr容错当 schema validation:它是 parser,不会证明字段、类型或业务约束;外部数据仍需 zod/valibot 等 runtime schema。 -
把 Nuxt SSR 行为归给 ofetch:cookie 透传、payload hydration 和响应式缓存来自上层框架,不是本库单独保证。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- Nuxt 3 / Nitro / 任何基于 h3 的 server——开箱即用,零配置
- 具有标准 Fetch 的浏览器、worker 与现代运行时
- 需要自动 body 处理、response parsing、hook 与轻量 retry policy
不适用:
- 维护中的老 axios 项目——interceptor → hooks 是结构性重写,不是 rename
- 需要阶段 timeout、复杂 retry budget 或 Node Duplex stream 的服务
- 需要框架级身份透传、缓存或去重,但没有上层 integration
- 不能接受固定 1.5.0 的依赖与 parser 语义
固定版本边界
Section titled “固定版本边界”- 本文绑定
unjs/ofetch@47fe8079...,tag 与 package 均为1.5.0。 - npm 把 1.5.1 标为 latest,但其
gitHead在 canonical GitHub 仓库不可达,GitHubv1.5.1tag 又指向自报 2.0 alpha 的提交;升级前需重新建立可复查 provenance。 - 固定版本依赖
ufo、destr、node-fetch-native;条件 exports 为不同运行时选择入口。 - 本文未安装依赖、运行上游测试、发送请求或测量 bundle,状态保持
UNVERIFIED。
- 框架默认 = 独立库——Nuxt 不重造 HTTP 客户端,只给 ofetch 起别名
$fetch+ 注入 SSR 上下文。这种拆法让 ofetch 在非 Nuxt 项目里也能用,Nuxt 用户也能在需要时摸到底层 - 依赖按职责切——ufo 管 URL 拼接、destr 管 JSON 安全 parse、node-fetch-native 管 polyfill。代价是用户要”理解 N 个小包”
- 默认策略必须按版本读源码——retry method、delay 与 signal/timeout 组合都不能靠同类库经验外推。
- parsed 不等于 validated——自动解包改善调用体验,却没有提高外部数据可信度。
- 未显式配置 retry 的 PUT 请求收到 503,会自动重试吗?
- POST 配置
retry: 2后,是否仍因“payload method”而禁止重试? - 已传入
AbortSignal,同时设置timeout: 1000。固定 1.5.0 是否一定创建 timeout controller?
检查点:
- 不会;PUT 属于 payload method,默认 retry 为 0。
- 不会禁止。显式数值覆盖默认,副作用安全需由调用方保证。
- 不一定;固定实现只在没有既有 signal 时安装 timeout。
- 文档:unjs.io/packages/ofetch(API 速查 + 例子)
- 固定源码:unjs/ofetch —— 本文绑定提交
47fe80799e23406dd0fb1c504bb493b6a6d0a5af - ky —— 同赛道竞品,ResponsePromise 与 retry/timeout 合同不同
- 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 时的兜底方案之一