指南 · 专家
版本基线 ofetch 1.x。深入内核与工程实践:createFetch 自定义底座、Node 代理 / dispatcher、timeout 与 abort 的重试边界、拦截器实现 401 刷新的死循环防护、同构 fetch 选择、依赖与解析机制。
一、createFetch:自定义底层 fetch / Headers
ofetch 是 createFetch() 用默认全局对象建出来的。需要注入自定义底座时直接用 createFetch:
import { createFetch } from 'ofetch'
const myFetch = createFetch({
fetch: customFetchImpl, // 自定义 fetch(如测试 mock、特定 polyfill)
Headers: CustomHeaders,
AbortController: CustomAbortController,
defaults: { baseURL: '/api', retry: 1 },
})
CreateFetchOptions含fetch/Headers/AbortController/defaults。ofetch.create(defaults)是它的便捷封装(沿用同一底座、只合并 defaults)。
二、Node 端代理 / 连接池:dispatcher 与 agent
import { ProxyAgent } from 'undici'
// Node ≥ 18 走 undici:传 dispatcher
await ofetch('/api', {
dispatcher: new ProxyAgent('http://127.0.0.1:7890'),
})
// 老 Node(node-fetch-native polyfill):传 agent
await ofetch('/api', { agent: someHttpAgent })ofetch 没有 axios 式的
proxy字符串选项。Node 端控制底层网络(代理、连接池、TLS)走dispatcher(undici)或agent(polyfill)。
三、timeout 与 abort 的重试边界(易错)
源码里判断是否「中止」的逻辑:isAbort = error.name === 'AbortError' && !options.timeout。结合重试流程:
| 场景 | 是否重试 |
|---|---|
手动 signal.abort()(未设 timeout) | 不重试(isAbort 为真) |
| timeout 到点导致的中断(设了 timeout) | 会重试(isAbort 为假,因 options.timeout 有值) |
// 这个 timeout 中断,仍可能被 retry 再试
await ofetch('/slow', { timeout: 1000, retry: 2 })
// 这个手动 abort,不会被重试
const c = new AbortController()
setTimeout(() => c.abort(), 500)
await ofetch('/slow', { signal: c.signal, retry: 2 }) // abort 后直接抛出直觉上「用户主动取消不该重试」——ofetch 正是这么设计的:纯手动 abort(无 timeout)被视为用户意图,跳过重试。
四、拦截器实现 401 刷新 token:防死循环
在 onResponseError 里刷新 token 并重发,是常见模式,但有两个必须处理的坑:
let refreshing = null // 并发去重
const api = ofetch.create({
baseURL: '/api',
async onResponseError({ request, response, options }) {
// 坑 1:加重试标记,防止刷新后仍 401 → 无限循环
if (response.status === 401 && !options._retried) {
// 坑 2:并发请求同时 401,只刷新一次
refreshing ??= refreshToken().finally(() => { refreshing = null })
await refreshing
// 重发本次请求(带标记,避免再次进入刷新)
return api(request, { ...options, _retried: true })
}
},
})两个必处理点
- 死循环:刷新后若仍 401(refresh 也失效),不加标记会无限刷新→重发。
- 并发去重:多个请求同时 401,应只触发一次刷新,其余等同一个 Promise。
五、在 onRequest 中止请求
ofetch 拦截器没有 return false 取消的约定。要在发出前中止,抛错或 abort signal:
const api = ofetch.create({
onRequest({ options }) {
if (!isOnline()) {
throw new Error('离线,已拦截请求') // 跳过实际 fetch,走错误流程
}
},
})抛出的错误会被
catch捕获,请求不会真正发出。return false/ 删context.request都不是受支持的取消方式。
六、同构 fetch 的选择逻辑
ofetch 默认用 globalThis.fetch,但 package.json 的条件导出对不同环境给了不同入口:
| 环境(exports 条件) | 入口 | 底层 fetch |
|---|---|---|
| browser / worker / deno / edge-light / netlify… | index.mjs | 平台原生 fetch |
| node(import/require) | node.mjs / node.cjs | undici(Node≥18),缺则 node-fetch-native polyfill |
现象:同一份业务代码在浏览器、Cloudflare Worker、Node 服务里都能跑
根因:ofetch 同构 —— 优先用平台原生 fetch,Node 端经 node.mjs 适配 undici,
老 Node 缺 fetch 时由 node-fetch-native 兜底
要点:你几乎不用关心环境差异,这正是 ofetch「Works on node, browser and workers」的实现七、解析与错误的内核细节
- 解析器:默认
destr,比JSON.parse鲁棒——合法 JSON(含标量42/true)解析为对应值,非法输入退回原始字符串,不抛错。 - 无体响应:
HEAD方法及nullBodyResponses = [101, 204, 205, 304]跳过 body 解析。 - FetchError 双命名:
status/statusCode同值(皆response.status)、statusText/statusMessage同值,兼容 fetch 风格与 Node/h3 风格。 - 错误 message:
createFetchError拼成[METHOD] "URL": status statusText,并Error.captureStackTrace精简栈。
八、辨析:ofetch 做了什么 vs 没做什么
| ofetch 替你做了(相对 fetch) | ofetch 没做(需自己) |
|---|---|
| 自动解析响应(destr) | 运行时类型校验(用 zod) |
| 自动序列化 JSON body | 上传/下载进度(用流自算) |
| 非 2xx 自动抛 FetchError | token 刷新逻辑(在拦截器写) |
| 内置 retry / timeout | 请求取消的高层封装(用 signal) |
| baseURL / query 拼接(ufo) | CORS / cookie 策略(由浏览器/服务端定) |
| 拦截器四钩子 | 缓存 / 去重(用 useFetch 或自建) |