项目:my-ts-axios | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
第 03 篇第 4.1 节给出过"新增配置项的五处触碰点"清单,本篇把它扩展成一份完整的扩展开发手册:六个最常见的扩展场景各配一份 SOP(标准操作流程),每份包含触碰点清单、代码骨架、验证方式与该场景特有的陷阱。所有 SOP 共享三个前置认知,先在此统一说明:
前置一:改动的影响半径由第 01 篇的三条依赖规则决定。 core/ 不 import axios.ts、适配器只依赖 helpers/cancel/settle/types、helpers/ 保持无状态——新增代码若需要违反其中任何一条,先停下来重新设计(例外边只有 helpers/headers.ts → core/AxiosHeaders 一处,属于历史决定而非许可)。
前置二:特性落地的判定口径是"四处齐全"。 第 00 篇第 1.2 节确立了口径:类型声明(src/types/index.ts)、默认值(src/defaults.ts,可选)、消费路径(src/ 中真实读取该配置的代码)、测试用例(test/)四处齐全才算特性落地;transitional.silentJSONParsing 这类"声明未消费"的半成品就是这个口径的反面教材。
前置三:验证基线是六连命令。 每个 SOP 的收尾都一样:npm run lint → npx tsc --noEmit → npm test(盯覆盖率四项门槛,见《09》第 4 节)→ npm run build → node scripts/smoke-dist.cjs → npm run dev 手工验证(涉及浏览器行为时)。覆盖率门槛意味着新增源码必须伴随测试,否则 npm test 直接失败。
场景:为新的运行时或特殊传输(如 HTTP/2、内存 mock、WebSocket 隧道)添加适配器。以假想的 mock 适配器为例。
触碰点清单(共 5 处):
src/core/adapters/mock.ts:实现签名 (config: AxiosRequestConfig) => AxiosPromise。最小骨架:import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from '../../types'
import settle from '../settle'
import { createError, AxiosErrorCodes } from '../../helpers/error'
export default function mockAdapter(config: AxiosRequestConfig): AxiosPromise {
return new Promise((resolve, reject) => {
// ① 环境守卫:依赖不可用时以 AxiosError + 明确错误码拒绝(参照 fetch.ts 第 46~55 行)
// ② 组装 response:config 字段必须引用传入的 config(数据不变量 3,见《04》第 5 节)
const response: AxiosResponse = {
data: {}, status: 200, statusText: 'OK',
headers: {}, config, request: null,
}
// ③ 唯一成功出口走 settle:validateStatus 判定、4xx/5xx 错误码、error.status 写入全部免费获得
settle(resolve, reject, response, config)
})
}三条硬性要求:取消双轨必须支持(订阅 config.cancelToken.promise 与 config.signal,触发时以 Cancel 实例 / CanceledError reject——数据不变量 4 要求 reject 值满足 isCancel());超时按 transitional.clarifyTimeoutError 定码(ECONNABORTED 或 ETIMEDOUT,参照 xhr.ts 第 91~101 行);headers 必须小写键(数据不变量 5)。URL/data/headers 的加工不要做——进入适配器的 config 已是加工形态(《04》不变量 2)。
2. 注册 src/core/getAdapter.ts:在 knownAdapters 加 mock: mockAdapter 一行。若适配器依赖 Node 内置模块,需同步做两件事:rollup.config.mjs 的 EXTERNAL 数组加模块名;src/shims/node-builtins.ts 补对应抛错导出(否则浏览器 UMD 构建期就缺符号)。若适配器仅特定环境可用,用与 xhr 相同的模式:typeof X !== 'undefined' ? adapter : null。
3. 类型(仅当引入新配置):如需新配置字段,按 SOP 三流程走;纯实现不新增配置时跳过。
4. 测试:test/core/mockAdapter.spec.ts,至少覆盖:成功路径走 settle、4xx/5xx 码与 status、cancelToken 与 signal 双轨取消、无 validateStatus 放行。参照 test/core/settle.spec.ts 与 fetchAdapter.spec.ts(304 行,是适配器测试的最全范本)。
5. 文档:更新第 00 篇特性覆盖表与《02-模块设计》第 5 节的横切对照表(新增一列)。
陷阱:适配器解析的回退语义——getAdapter 对命名适配器查不到时静默尝试下一个(getAdapter.ts 第 36~42 行),因此新适配器在浏览器里声明为不可用(null)是安全的;但若你把 mock 注册为无条件可用,adapter: ['mock', 'xhr'] 的回退行为会改变,需在 getAdapter.spec.ts 补用例锁定。
场景:这是调用方级扩展(不改库源码),但"如何写一个不踩坑的拦截器"值得一份 SOP。以"自动附加 token 并在 401 时刷新重试"为例。
请求拦截器(instance.interceptors.request.use(resolved, rejected?, options?)):
instance.interceptors.request.use(
(config) => {
config.headers = config.headers || {}
config.headers['Authorization'] = `Bearer ${getToken()}`
return config // 必须返回 config,链才能继续
},
undefined,
{
runWhen: (config) => !config.url?.includes('/auth'), // 条件执行:收集期剔除,零链路成本
synchronous: true, // 全部拦截器都标 true 时走同步快路径
},
)要点:①config 是合并形态的单一对象,直接改写即生效(《04》推论二);②runWhen 拿到的 config 已完成 mergeConfig 与 method 小写化,可安全读取 config.method;③标 synchronous 前确认所有其他请求拦截器也标了(every 全真才生效,混入一个未标记的整体退回异步);④同步路径下拦截器抛错是同步异常,调用方需要 try/catch 而 .catch 无效(《05》流程一边界)。
响应拦截器(按注册序执行,无同步概念):成功钩子做形态加工,失败钩子做错误恢复:
instance.interceptors.response.use(
(res) => res,
async (error) => {
if (error.response?.status === 401 && !error.config._retried) {
error.config._retried = true // 借宽松索引签名放标记,防止重试风暴
await refresh()
return instance.request(error.config) // 用加工后的 config 重放请求
}
throw error // 不恢复必须继续抛,否则错误被吞成 resolved
},
)要点:⑤error.config 是加工形态(url 已完整、headers 已拍平),直接重放不会重复拼 baseURL(combineURL 对绝对 URL 不动作),但 headers 已是单层对象、再经 flattenHeaders 也安全(无方法键);⑥响应拦截器失败钩子返回非 Promise 值会把错误恢复为成功——"不恢复就继续抛"是必须遵守的纪律。eject:use 返回的 id 长期有效,配合模块级单例管理避免重复注册(热更新场景的常见内存泄漏源)。
以假想的 retry: number 为例,五处触碰点(第 03 篇 4.1 节清单的展开):
src/types/index.ts 的 AxiosRequestConfig 增加字段声明与注释。得益于末尾 [propName: string]: any 索引签名,不做此步也能编译,但会失去类型提示——属必做项。defaultStrat 天然支持"本次优先、缺省回退");对象型且需与 defaults 深合并的,把键名加进 mergeConfig.ts 第 39 行的 stratKeysDeepMerge 数组;需要"永不继承"语义的加进第 18 行 stratKeysFromVal2。src/defaults.ts 直接加字段。注意 falsy 默认值的陷阱:mergeConfig 第二轮只对 config2 中 undefined 的键补合并,因此 defaults 里 retry: 0 与"不设置"对调用方可感知的行为一致,但 config2.retry = 0 是显式值、不会回落 defaults——两条路径都要在测试里覆盖。dispatchRequest.ts(如重试这类"围绕请求"的逻辑更适合在响应拦截器层实现,库内置则放 dispatchRequest 的失败分支);传输级语义放具体适配器——多端都要生效就得三处都写或下沉为共享 helper(withXSRFToken 的落地痕迹是标准参照:types → xhr/fetch 两适配器 → test/xsrf.spec.ts 与 advancedConfig.spec.ts 双覆盖)。src/axios.ts 挂载与 scripts/smoke-dist.cjs 的 15 项断言清单;行为用例进 test/ 根或 test/core/,纯函数用例进 test/helpers/,确认覆盖率达标。错误码是封闭集合(数据决定二:《04》第 1.3 节),新增流程:①src/helpers/error.ts 的 AxiosErrorCodes 常量表加一行(保持 as const,码名即值的大写风格);②在唯一的产生点使用它——记住第 01 篇的三段约定:"错误只在适配器内部与 settle 两处诞生",链路其他环节只透传不制造;若新码属于 HTTP 状态语义,优先复用 settle 的 4xx/5xx 二分而不是新造码;③构造调用统一走 createError 工厂(自动获得 config 携带、isAxiosError 标记、原型修复),带 HTTP 响应的第 5、6 参传 response 与 status;④测试进 test/helpers/error.spec.ts(工厂行为)与触发点的 spec(如新码在 http 适配器触发,进 httpAdapterHardening.spec.ts);⑤同步《03》错误码表与《04》错误来源表。反面约束:不要新增"占位码"——ERR_BAD_OPTION_VALUE/ERR_BAD_OPTION/ERR_DEPRECATED 三个已声明未使用的码是历史对齐产物(见《13》),新代码不应复制这个模式。
面向"想给这个库提 PR"的贡献者,从改动到合并的完整清单:
npm install(husky 经 prepare 脚本自动安装 hook),确认 npm test 全绿且覆盖率达标(这是你的改动基线)。core/、纯函数进 helpers/、环境差异进 adapters/);import 遵守第 1 节前置一;风格交给 lint-staged(pre-commit 自动 prettier + eslint --fix),不要手工调格式。test/core/settle.spec.ts 里"回归:http 适配器此前反转"的注释就是这个流程的化石证据。npm run lint → npx tsc --noEmit → npm test → npm run build → node scripts/smoke-dist.cjs → 涉及浏览器行为的 npm run dev 手工过一遍对应示例页(示例直接 import 源码,改完刷新即见效,见《06》第 7 节)。npm run commit 走 commitizen 生成 Conventional Commits 信息(feat:/fix:/docs:/refactor:/test:/chore:),commit-msg hook 会拒绝不合规信息;一次提交一个意图。.github/workflows/ci.yml 的 Node 20/22 双矩阵(lint → test → build → examples:build),本地全绿但 CI 失败时优先怀疑环境差异(Node 版本、锁文件漂移)。示例体系是"一示例一路由组"(《06》第 7 节),新增页面四步:①examples/<name>/ 下建 index.html + main.ts + App.vue(复制 examples/simple/ 起步,vite.config.ts 的多入口扫描会自动发现新目录);②examples/routes/<name>.js 写 Express 路由并在 routes/index.js 注册(路由文件导出 (router) => {...} 函数);③需要跨域/cookie 的场景用 8088 的 server2.js 或在 mock-api 中种 cookie(examples/plugins/mock-api.ts 预置 XSRF-TOKEN-D 的中间件是范本);④共享 UI 直接取用 examples/shared/ 的 useRequest.ts、ResponseView.vue 等。注意两点边界:示例直接 import 库源码(import axios from '../../src' 形态),验证的永远是源码形态;示例目录被 eslint 忽略、不进发布包,所以示例代码不需要过 lint,但路由若用 ES Module 语法要注意 mock-api 是以 require 加载的(examples/plugins/mock-api.ts 第 43 行),新路由文件保持 CommonJS。
| 你想做的事 | 应走 SOP | 关键约束 |
|---|---|---|
| 接入新运行时/新协议 | 一 | settle 唯一出口、取消双轨、headers 小写、不改 config 加工 |
| 附加 token / 刷新重试等调用方逻辑 | 二 | 拦截器必须返回值;失败钩子不恢复就继续抛 |
| 暴露新的可配置行为 | 三 | 类型、默认值、消费点、测试四处齐全 |
| 表达新的失败语义 | 四 | 只在适配器/settle 产生;走 createError;不加占位码 |
| 修 bug / 改实现 | 五 | 先红后绿的用例;六连验证;Conventional Commits |
| 写演示页面 | 六 | 一示例一路由组;示例只验源码形态 |
六个 SOP 的共同底座是同一个思想:这个库的每个扩展点都已经被架构"预留了接缝"——适配器有注册表、配置有策略表、错误有码表与工厂、拦截器有管理器——扩展工作本质上是"在正确的接缝插入正确的最小实现,然后让测试与门槛兜住你"。找不到接缝时,先怀疑自己理解错了架构(回头读《01》《02》),而不是硬开新口子。