项目:my-ts-axios | 版本:v1.2 | 生成日期:2026-10-06 | 依据:源码分析
本篇是库的使用侧总集:把每一个核心特性写成一段完整可运行的示例,每段示例固定三个部分——代码(可直接保存运行)、行为说明(运行后你会观察到什么,以及为什么)、对应源码锚点(该行为落在 src/ 的哪些位置,便于从"用法"回溯到"实现")。与前序篇目的分工:《03-接口设计》与《15-接口全量参考》是字典(字段与签名),本篇是剧本(调用、发生什么、为什么);示例只引用《00-项目总览》第 4 节导览中存在消费路径的特性,凡"声明未消费"的配置项(如 transitional.silentJSONParsing)不进入示例并在相关处标注。
可运行性约定:示例默认在 Node.js(>=14,库运行时要求)下运行,入口写法为 const axios = require('ts-axios-zhangzhengyang')(对应产物 dist/node/axios.cjs,互操作细节见《12-FAQ与故障排查》2.2 节);标注【浏览器】的段落以 <script src="dist/browser/axios.umd.js"> 引入后使用全局 axios,或在打包器中 import axios from 'ts-axios-zhangzhengyang'。需要服务端的示例统一假设本地 3000 端口有一个与 examples/routes/*.js 等价的 mock 路由(npm run dev 时由 examples/plugins/mock-api.ts 自动提供)。文中所有行为结论均以源码为准;其中拦截器执行顺序一项经过实际运行验证(加载 dist/node/axios.cjs、自定义适配器回放),并在 2.2 节给出验证记录与一处示例页描述的出入说明。
const axios = require('ts-axios-zhangzhengyang')
// 形态一:直接传配置对象(method 缺省为 'get')
axios({ url: 'http://localhost:3000/base/get', params: { a: 1, b: ['x', 'y'] } })
.then(print)
// 形态二:url 与 config 分离(config.url 会被写为该 url)
axios.get('http://localhost:3000/base/get', { params: { a: 1 } }).then(print)
// 形态三:带体请求——普通对象自动 JSON 序列化并补 Content-Type
axios.post('http://localhost:3000/base/post', { firstName: 'Yee', lastName: 'Huang' })
.then(print)
// 形态四:baseURL 拼接 + getUri 预览最终地址
const instance = axios.create({ baseURL: 'http://localhost:3000', timeout: 5000 })
console.log(instance.getUri({ url: '/base/get', params: { a: 1 } }))
// => http://localhost:3000/base/get?a=1
instance.get('/base/get').then(print)
function print(res) {
// 响应契约:data / status / statusText / headers / config / request
console.log(res.status, res.statusText, res.data)
console.log('实际发送的最终配置 method:', res.config.method) // 已统一 toLowerCase
}行为说明。四个形态都 resolve 同一契约的 AxiosResponse:data(已过 transformResponse,JSON 字符串被解析为对象)、status/statusText、headers(小写键普通对象)、config(mergeConfig 后的最终配置,method 已小写)、request(适配器内部对象,xhr 为 XHR 实例、http 为 ClientRequest)。params 数组默认序列化为 b[]=x&b[]=y(indexes 三态见《15-接口全量参考》3 节);普通对象体被 JSON.stringify 并自动补 Content-Type: application/json;charset=utf-8;post/put/patch 的默认头是 application/x-www-form-urlencoded,但转换器按需补写 JSON 头的两段式设计让最终生效的是后者(《02-模块设计》第 10 节)。getUri 只做 mergeConfig + transformURL,不发请求、不加工 data/headers。注意 allowAbsoluteUrls: false 可以禁止绝对 URL 绕过 baseURL。
源码锚点:方法族与双签名归一在 src/core/Axios.ts 第 4152、107180 行;mergeConfig(this.defaults, config) 在第 51 行;transformURL 的 baseURL 拼接在 src/core/dispatchRequest.ts 第 3037 行;params 序列化在 85 行);JSON 序列化与补头在 src/helpers/url.ts 的 buildURL(第 16src/defaults.ts 第 37~55 行与 src/helpers/data.ts、src/helpers/headers.ts 的 processHeaders。
const instance = axios.create()
// 请求拦截器:A、B 两个(后注册者先执行)
const idA = instance.interceptors.request.use(config => {
config.headers['X-Seq'] = (config.headers['X-Seq'] || '') + 'A'
return config
})
instance.interceptors.request.use(config => {
config.headers['X-Seq'] = (config.headers['X-Seq'] || '') + 'B'
return config
})
instance.interceptors.request.eject(idA) // A 被移除,最终只追加 B
// 响应拦截器:成功钩子按注册序执行;rejected 钩子可捕获适配器错误并恢复
instance.interceptors.response.use(
res => { res.data.via = 'ok-hook'; return res },
err => {
if (axios.isCancel(err)) throw err // 取消不恢复,继续上抛
return { data: 'fallback', status: 200, statusText: 'OK',
headers: {}, config: err.config, request: null } // 其他错误可降级恢复
}
)
// runWhen:只对 /renew 开头的请求注册生效
instance.interceptors.request.use(config => {
config.headers['X-Renew'] = '1'
return config
}, undefined, { runWhen: config => String(config.url).startsWith('/renew') })
// synchronous:全部声明时走同步快路径(少 n 个 microtask)
instance.interceptors.request.use(config => {
config.headers['X-Sync'] = '1'
return config
}, undefined, { synchronous: true })
// 发起请求(自定义适配器回放,便于离线观察链路)
const adapter = cfg => Promise.resolve({
data: 'body', status: 200, statusText: 'OK', headers: {}, config: cfg, request: null
})
instance({ url: '/other/get', headers: {}, adapter }).then(res =>
console.log('X-Seq:', res.config.headers['X-Seq'])) // => 'B'行为说明与验证记录。请求拦截器后注册先执行:链构造时按注册序 unshift(Axios.ts 第 89~91 行),因此上面的 X-Seq 只会是 'B'(A 已被 eject;若 A 未被移除,注册序 A→B 的执行序是 B→A,值为 'BA')。响应拦截器按注册序执行,rejected 钩子位于 dispatchRequest 之后,所以能捕获适配器与 settle 的错误并选择恢复(返回正常值即 resolve)或继续上抛。runWhen 返回 false 的拦截器连链都不进——上面的 X-Renew 在 /other/get 上不会出现,且它不参与 synchronous 的 every 判定;synchronous 是 all-or-nothing:只要有一个请求拦截器未声明,整体回退异步链。还有一个边界要牢记:同步快路径中拦截器抛出的异常会从 request() 同步抛出(加工发生在 Promise.resolve(conf) 之前的同一调用栈),只写 .catch 的调用方会漏接,需要 try/catch。
⚠️ 待确认 说明一处出入:示例页 examples/interceptor/App.vue 的描述文案写的是"请求拦截器按添加顺序执行……请求头 test 为 '123'",而库的实际行为是反序(同页三个拦截器依次追加 1/2/3,实测结果为 '321')。本篇以源码与实测为准——验证方法即本节示例(Node 加载 dist/node/axios.cjs,自定义适配器回放,async 与 synchronous 两路径均得到 '321')。示例页的可视化结果应以实际运行为准,文案疑为笔误,建议随《13》阶段一的一致性收敛一并修正。
源码锚点:收集与 runWhen 过滤 src/core/Axios.ts 第 5468 行;80 行;unshift 链构造与 syncRequest 判定与快路径第 71while (chain.length) 第 8297 行;响应拦截器追加第 100102 行;eject 的槽位置 null 在 src/core/InterceptorManager.ts 第 4146 行;6 行(注意它尚未进公共类型面,见《13》2.6 节)。时序断言的测试范本见 InterceptorOptions(runWhen/synchronous)在同文件第 3test/core/interceptorRunWhen.spec.ts。
// 形态一:函数适配器——最小可用的 (config) => AxiosPromise
const mockAdapter = (config) => new Promise((resolve, reject) => {
if (config.url === '/mock/ok') {
resolve({ data: { hello: 'mock' }, status: 200, statusText: 'OK',
headers: {}, config, request: { mock: true } })
} else {
// 状态错误也应走 settle 的语义:4xx/5xx → AxiosError
const { createError, AxiosErrorCodes } = require('ts-axios-zhangzhengyang')
reject(Object.assign(
new Error('Request failed with status code 404'),
{ config, code: AxiosErrorCodes.ERR_BAD_REQUEST, response: {
data: 'not found', status: 404, statusText: 'Not Found',
headers: {}, config, request: null }, status: 404 }))
}
})
axios.get('/mock/ok', { adapter: mockAdapter }).then(res => console.log(res.data))
axios.get('/mock/miss',{ adapter: mockAdapter }).catch(e => console.log(e.code, e.status))
// => ERR_BAD_REQUEST 404(注意 error.response.data 已被补跑 transformResponse)
// 形态二:命名适配器 + 回退数组(先试 fetch,失败换 xhr;仅浏览器有意义)
axios.get('/probe', { adapter: ['fetch', 'xhr'] })
// 形态三:注册进注册表(利用暴露的 knownAdapters 机制,getAdapter 已作为静态 API 暴露)
console.log(axios.getAdapter({})) // 默认解析:环境可用适配器之一
console.log(axios.getAdapter({ adapter: 'http' }).name || 'httpAdapter')行为说明。config.adapter 三种形态都会被归一为数组后线性扫描(getAdapter.ts 第 19~29 行):函数直接返回;字符串查内置注册表(xhr/http/fetch,其中 xhr 仅在存在 XMLHttpRequest 的环境注册,fetch 无条件注册但运行时无全局 fetch 时以 ERR_NOT_SUPPORT 拒绝);数组依次回退,命名适配器不可用时静默尝试下一个,全部失败抛普通 Error 并列出当前环境可用名。自定义函数适配器要想让错误体验与内置一致,应遵循同一契约——错误对象带 config、code 与 response,且 response.data 最终会被 dispatchRequest 失败分支补跑转换(本例 e.status 与 e.response.status 都是 404)。注册表本身未直接导出,但 getAdapter 已挂到静态 API(axios.ts 第 48 行),可用于探测当前环境的可用适配器。
源码锚点:注册表与解析在 src/core/getAdapter.ts 第 10~53 行;适配器签名约定在 src/types/index.ts 第 163 行(AxiosAdapter);三内置适配器对 settle 的共享调用见《07-模块详解》第 4 节;新增适配器的完整 SOP(含测试与注册步骤)见《11-扩展开发指南》SOP 一。
// —— 轨道一:CancelToken(旧式,仍完全兼容)——
const source = axios.CancelToken.source()
axios.get('http://localhost:3000/cancel/get', { cancelToken: source.token })
.catch(e => {
console.log(axios.isCancel(e)) // => true
console.log(e instanceof axios.Cancel, e.message) // => true '用户主动取消'
})
setTimeout(() => source.cancel('用户主动取消'), 200)
// 一个 token 取消多个并发请求(Promise 多订阅语义)
const shared = axios.CancelToken.source()
axios.get('/a', { cancelToken: shared.token }).catch(e => console.log('a:', axios.isCancel(e)))
axios.get('/b', { cancelToken: shared.token }).catch(e => console.log('b:', axios.isCancel(e)))
shared.cancel('batch stop') // a、b 同时进入取消分支
// —— 轨道二:AbortController / AbortSignal(Web 标准,推荐)——
const controller = new AbortController()
axios.get('http://localhost:3000/cancel/get', { signal: controller.signal })
.catch(e => {
console.log(axios.isCancel(e)) // => true
console.log(e instanceof axios.AxiosError, e.code) // => true 'ERR_CANCELED'
})
controller.abort('用户主动取消') // 也可传 reason
// 自动超时信号:AbortSignal.timeout(浏览器/Node 17.3+)
axios.get('http://localhost:3000/error/timeout', { signal: AbortSignal.timeout(1000) })
.catch(e => console.log('canceled by timeout signal:', axios.isCancel(e)))行为说明。两轨的判定统一收口在 axios.isCancel:CancelToken 轨的 reject 值是 Cancel 实例(只有 message 字段),signal 轨与"取消先于请求"场景的 reject 值是 CanceledError(AxiosError 子类,code: 'ERR_CANCELED',name: 'CanceledError')——instanceof 行为两轨不同,用 isCancel 判定才是稳妥写法。CancelToken 的 cancel 幂等(重复取消只生效一次,CancelToken.ts 第 17~23 行先判 this.reason);一个 token 可被任意多个请求共享,因为取消本质是 resolve 一个 promise,多请求各自 .then 订阅。signal 轨在三端的行为差异是:xhr/http 用事件监听 + abort,fetch 适配器把 config.signal、cancelToken、timeout 三个中断源收敛进内部 AbortController;超时与取消的区分——timeout 触发的是 ECONNABORTED/ETIMEDOUT(受 transitional.clarifyTimeoutError 控制),不是取消错误。已 aborted 的 signal 在 dispatch 预检阶段即被拒绝(请求不会发出)。
源码锚点:src/cancel/CancelToken.ts 全文(executor/promise/throwIfRequested/source);src/cancel/Cancel.ts 第 1214 行的 58 行;三端各自的 signal 处理:isCancel 双轨判定;src/cancel/CanceledError.ts(ERR_CANCELED 与原型修复);预检双查在 src/core/dispatchRequest.ts 第 50xhr.ts 的 processSignal(第 179198 行)、115 行一带)、http.ts 的 abortCleanups 机制(第 78fetch.ts 的中断源收敛(第 72~135 行)。完整流程图见《05-核心流程》流程四。
// 下载进度(三端均支持;fetch 端为流式读取增量)
axios.get('http://localhost:3000/base/download', {
onDownloadProgress: e => {
// AxiosProgressEvent:loaded/total/progress/bytes/lengthComputable...
console.log('下载', Math.round((e.loaded / (e.total || 1)) * 100) + '%')
}
})
// 上传进度(xhr 端为原生 upload.onprogress;http 端由限速流统计;fetch 无上传进度)
const formData = new FormData()
formData.append('file', fileInput.files[0]) // 浏览器场景
axios.post('http://localhost:3000/base/upload', formData, {
onUploadProgress: e => console.log('上传', e.loaded, '/', e.total)
})
// postForm:普通对象自动转 FormData(headers 前置 multipart 声明)
axios.postForm('http://localhost:3000/base/upload', {
name: 'avatar.png',
file: fileBlob, // Blob/File 值按文件附加
}, {
formSerializer: { dots: true, indexes: true }, // 路径键 a.b 与 foo[0] 形态
onUploadProgress: e => console.log('form 上传', e.progress)
})
// Node 端:上行/下行限速([upload, download] 字节/秒)+ 流式响应
axios.get('http://localhost:3000/base/download', {
responseType: 'stream',
maxRate: [100 * 1024, 200 * 1024],
onDownloadProgress: e => console.log('rate:', e.rate, 'est:', e.estimated)
}).then(res => res.data.pipe(process.stdout))行为说明。进度事件的契约是官方 v1 对齐的 AxiosProgressEvent:loaded/total/progress(0~1)/bytes(本段增量)/rate/estimated(仅 Node 端由流包装器计算)/upload/download 方向标记/lengthComputable。xhr 端用 makeProgressEvent 维护 lastLoaded 计算增量;http 端在解压前的原始响应流上包 createProgressStream(2 秒滑动窗口计算瞬时速率),total 取 content-length;fetch 端用 body.getReader() 的 for 循环增量读取——没有上传进度(fetch API 限制,《13》2.6 节登记)。postForm 的行为链是:headers 前置 multipart/form-data(用户显式同名头优先)→ toFormData(data, undefined, formSerializer) 预转换 → 常规 request;formSerializer 的 dots/indexes/visitor 已实现并有测试,metaTokens/maxDepth 声明未消费(⚠️ 待确认 是否有意,见《13》条目 2/3)。maxRate 是 Node 专属:数字或 [上行, 下行] 数组,0 视为不限速,实现为"累计字节/期望速率 − 实际耗时"的延迟放行。
源码锚点:xhr 进度在 src/core/adapters/xhr.ts 第 106124 行(279 行;makeProgressHandler);流包装与限速在 src/helpers/progressStream.ts 全文(createProgressStream/resolveMaxRates);fetch 下载进度在 src/core/adapters/fetch.ts 第 246postForm 折算在 src/core/Axios.ts 第 182~196 行(_requestForm);toFormData 的路径生成规则在 src/helpers/formData.ts。上传示例的服务端配套(multipart 解析)见 examples/plugins/mock-api.ts。
const axios = require('ts-axios-zhangzhengyang')
axios.get('http://localhost:3000/error/get404').catch(e => {
if (axios.isCancel(e)) return console.log('已取消,交由取消分支处理')
if (axios.isAxiosError(e)) {
// 状态错误:4xx → ERR_BAD_REQUEST,5xx → ERR_BAD_RESPONSE(settle 判定)
console.log(e.code, e.status, e.response && e.response.data)
// e.response.data 已是解析后的对象——dispatchRequest 失败分支补跑了 transformResponse
} else {
throw e
}
})
// 超时:ECONNABORTED;transitional.clarifyTimeoutError 开启后为 ETIMEDOUT
axios.get('http://localhost:3000/error/timeout', {
timeout: 300,
transitional: { clarifyTimeoutError: true }
}).catch(e => console.log('timeout code:', e.code)) // => ETIMEDOUT
// 自定义合法状态码:不再把 4xx/5xx 当失败
axios.get('http://localhost:3000/error/get404', {
validateStatus: s => s < 500
}).then(res => console.log('404 也算成功,status =', res.status))
// 网络层失败:ERR_NETWORK(浏览器 onerror / Node 连接错误)
axios.get('http://127.0.0.1:9/nope').catch(e => console.log('network:', e.code))行为说明。错误体系的第一分流永远是 isCancel → isAxiosError → 兜底。状态错误的错误码以 500 为界(settle.ts 第 20~21 行),数值状态同时写入 error.status;error.response.data 在到达 catch 之前已被响应转换管道加工。超时错误码默认 ECONNABORTED(对齐官方历史行为),transitional.clarifyTimeoutError: true 时改为更准确的 ETIMEDOUT——这是 transitional 三项中第二个有消费点的配置。validateStatus 是"什么算成功"的唯一裁决者,置为恒真函数可拿到任意状态码的响应对象;settle 还有两个放行条件(status 为 0、未配置 validateStatus),完整语义见《07-模块详解》3.7 节。网络失败统一 ERR_NETWORK;Node 端另有 ERR_INVALID_URL、ERR_FR_TOO_MANY_REDIRECTS、ERR_PROXY(本项目专有码,官方不存在,见《13》条目 7)与 maxContentLength 超限(code 为 null 的普通 Error)。全部错误码的产生点对照表在《15-接口全量参考》4.4 节。
源码锚点:src/core/settle.ts 全文;src/helpers/error.ts 的 AxiosError(第 1959 行,含 ES5 原型修复)与 13 个错误码常量(第 317 行);超时错误码开关在三个适配器各自的 timeout 处理中(xhr 第 63~133 行一带、http 的 setTimeout 段、fetch 的中断源收敛段)。
const axios = require('ts-axios-zhangzhengyang')
// axios.all 即 Promise.all;axios.spread 把数组参数展开为逐个回调参数
axios.all([
axios.get('http://localhost:3000/base/get', { params: { id: 1 } }),
axios.get('http://localhost:3000/base/get', { params: { id: 2 } }),
])
.then(axios.spread((user1, user2) => {
console.log(user1.data, user2.data) // 两个 AxiosResponse 按提交顺序排列
}))
// 现代写法等价(async/await + 解构);并发失败任一即整体 reject
async function demo() {
const [r1, r2] = await Promise.all([
axios.get('/base/get', { params: { id: 1 } }),
axios.get('/base/get', { params: { id: 2 } }),
])
console.log(r1.status, r2.status)
}
demo()行为说明。axios.all 在源码中就是 Promise.all 的别名(axios.ts 第 3436 行),语义与原生完全一致:全部成功返回按顺序排列的响应数组,任一失败立即 reject——注意这与"部分失败容错"无关,需要后者应自行加 41 行),解决"回调函数签名的参数数量与数组长度耦合"的旧式书写问题;现代 JS 的解构语法已覆盖其价值,两者并存纯为 API 兼容。与取消机制组合时有一个实用推论:给多个并发请求传同一个 CancelToken 的 token(见 2.4 节),即可实现"整批取消",等价于官方文档推荐的组合用法。.catch(e => e) 或使用 Promise.allSettled。axios.spread 是数组展开糖(第 37
源码锚点:src/axios.ts 第 3441 行(all/spread 挂载);类型面在 226 行(AxiosStatic 的 all/spread 签名)。src/types/index.ts 第 224
把七段示例与仓库内的两套"活示例"对应起来,可以形成"文档示例 → 源码锚点 → 仓库示例页 → mock 路由"的完整回查链:基本请求对应 examples/base/(路由 examples/routes/base.js);拦截器对应 examples/interceptor/(examples/routes/interceptor.js,注意其描述文案与实际执行顺序的出入已在 2.2 节说明);取消对应 examples/cancel/ 与 examples/abort/(examples/routes/cancel.js,服务端延迟 1s 给手动取消留窗口);错误处理对应 examples/error/(examples/routes/error.js,含 404/timeout 路由);配置与序列化对应 examples/config/、表单对应 examples/form/、综合进阶对应 examples/more/(examples/routes/more.js,含 Basic Auth——那里用了 atob,见《17-依赖全景》2.4 节)。运行方式统一是 npm run dev 后访问 http://localhost:5173,mock 路由由 examples/plugins/mock-api.ts 挂载、8088 端口的 examples/server2.js 提供跨域场景(见《14-配置与环境详解》第 7 节)。若某段示例的行为与本文描述不符,优先怀疑三件事:服务端未启动、Node 版本低于 AbortSignal.timeout 的要求(17.3+)、或消费产物形态不对(CJS/ESM 互操作,《12-FAQ与故障排查》2.2 节)——按此顺序排查即可覆盖绝大多数"示例跑不通"的场景。
想离线验证任意一段示例的行为(不起服务、不发网络请求),仓库的测试体系提供了两条现成通道。第一条是自定义适配器回放(即 2.2、2.3 节示例的写法):把 adapter 注入为返回伪造 AxiosResponse 的函数,请求链上除网络层之外的全部环节(合并、拦截器、加工、settle 语义、响应转换)都会真实执行,这是观察链路行为最快的方式。第二条是直接读测试:test/core/interceptorRunWhen.spec.ts(时序断言)、test/core/settle.spec.ts(成败判定)、test/cancel/(双轨取消)、test/progress.spec.ts(进度事件)各自对应本篇一节示例的机器可执行版本——文档示例与测试用例描述的是同一套行为契约,两边互相印证时若出现不一致,以测试通过的实际行为为准并回查《15-接口全量参考》的字段定义。这也是使用本库时最值得养成的习惯:对行为的疑问先转译为"这个行为由哪个源码锚点产生",再用适配器回放或测试用例求证,而不是反复试错配置组合。