导读:本文基于 Axios 官方源码(v1.x系列),结合 ECMA-262 规范与 MDN 文档,系统讲解 Axios 的底层实现原理、拦截器机制、请求取消等核心技术。文章包含完整可运行的示例代码,涵盖从基础使用到源码级自定义实现的完整路径,适合希望深入理解 HTTP 库设计的前端工程师阅读。
目录
- 零、导读与学习价值
- [0.1 示例覆盖清单](#0.1 示例覆盖清单)
- [0.2 核心名词速查](#0.2 核心名词速查)
- [0.3 为什么要学本篇](#0.3 为什么要学本篇)
- [0.4 建议练习路线(八步递进)](#0.4 建议练习路线(八步递进))
- [一、Axios 源码架构分析](#一、Axios 源码架构分析)
- [1.1 名词解释](#1.1 名词解释)
- [1.2 概念与底层原理](#1.2 概念与底层原理)
- [1.3 源码目录结构](#1.3 源码目录结构)
- [1.4 Axios 运行流程](#1.4 Axios 运行流程)
- [1.5 官方入口与入门实现的差异](#1.5 官方入口与入门实现的差异)
- [1.6 建议精读的核心文件](#1.6 建议精读的核心文件)
- [二、自定义实现 Axios 核心](#二、自定义实现 Axios 核心)
- [2.1 创建 axios 函数](#2.1 创建 axios 函数)
- [2.2 实现请求发送与 Promise 封装](#2.2 实现请求发送与 Promise 封装)
- [2.3 请求配置项深度解析](#2.3 请求配置项深度解析)
- [2.4 响应结果处理机制](#2.4 响应结果处理机制)
- [2.5 超时设置与错误处理](#2.5 超时设置与错误处理)
- [2.6 请求取消机制](#2.6 请求取消机制)
- [2.7 便捷方法实现](#2.7 便捷方法实现)
- 三、拦截器机制深度剖析
- [3.1 拦截器核心概念](#3.1 拦截器核心概念)
- [3.2 拦截器实现原理](#3.2 拦截器实现原理)
- [3.3 拦截器实战应用](#3.3 拦截器实战应用)
- 四、总结
- [4.1 知识点回顾](#4.1 知识点回顾)
- [4.2 高频面试题速查](#4.2 高频面试题速查)
- [4.3 学习建议](#4.3 学习建议)
零、导读与学习价值
0.1 示例覆盖清单
本文完整覆盖以下知识点与可运行示例:
| 示例 | 核心知识点 | 本文章节 |
|---|---|---|
| Axios 源码架构剖析 | 目录结构、createInstance + utils.extend |
§1 |
| 核心源码精读 | Axios.js 的 request 链、dispatchRequest、settle |
§1 |
| 创建 axios 函数示例 | bind 委托、axios 本质不是 Axios 实例 |
§2.1 |
| 请求发送与 Promise 封装示例 | dispatchRequest + XMLHttpRequest |
§2.2 |
| 请求配置项示例 | baseURL、params、headers、data |
§2.3 |
| 响应结果处理示例 | 响应对象结构、4xx/5xx 转 reject | §2.4 |
| 超时设置示例 | xhr.timeout、ECONNABORTED 错误码 |
§2.5 |
| 请求取消示例 | CancelToken + xhr.abort |
§2.6 |
| 便捷方法示例 | get / post 挂载到 instance |
§2.7 |
| 拦截器示例 | InterceptorManager、Promise 链 |
§3 |
| 拦截器实战封装示例 | token、loading、统一错误处理组合 | §3.3 |
0.2 核心名词速查
| 术语 | 一句话解释 |
|---|---|
| 适配器模式 | Axios 通过适配器屏蔽浏览器(XMLHttpRequest)与 Node.js(http 模块)的差异 |
| 拦截器 | 在请求发送前/响应接收后插入自定义逻辑的中间件机制 |
| CancelToken | 基于 Promise 状态控制实现请求取消的机制 |
| 责任链模式 | 拦截器采用的设计模式,让多个处理器依次处理请求/响应 |
| Promise 链 | Axios 通过 Promise 链实现拦截器的异步串联调用 |
0.3 为什么要学本篇
- 工程实践价值:掌握 Axios 设计模式,可应用到其他 HTTP 库或中间件系统的设计
- 面试加分项:源码级理解是高级前端工程师的必备能力,面试中能深入讨论实现细节
- 架构设计能力:学习 Axios 的适配器设计、拦截器机制,提升系统架构思维
- 调试能力:理解底层原理能快速定位请求问题,提升开发效率
与上一章的衔接 :若你已会用 Axios(axios(config)、create、拦截器、all),本篇回答 为什么这样设计 ,并带你 从零手写迷你版。建议先能独立发 GET/POST、写拦截器,再按 §0.4 的八步路线递进实现。
0.4 建议练习路线(八步递进)
#mermaid-svg-KJxV5tXfTJr02PMw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KJxV5tXfTJr02PMw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KJxV5tXfTJr02PMw .error-icon{fill:#552222;}#mermaid-svg-KJxV5tXfTJr02PMw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KJxV5tXfTJr02PMw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KJxV5tXfTJr02PMw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KJxV5tXfTJr02PMw .marker.cross{stroke:#333333;}#mermaid-svg-KJxV5tXfTJr02PMw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KJxV5tXfTJr02PMw p{margin:0;}#mermaid-svg-KJxV5tXfTJr02PMw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KJxV5tXfTJr02PMw .cluster-label text{fill:#333;}#mermaid-svg-KJxV5tXfTJr02PMw .cluster-label span{color:#333;}#mermaid-svg-KJxV5tXfTJr02PMw .cluster-label span p{background-color:transparent;}#mermaid-svg-KJxV5tXfTJr02PMw .label text,#mermaid-svg-KJxV5tXfTJr02PMw span{fill:#333;color:#333;}#mermaid-svg-KJxV5tXfTJr02PMw .node rect,#mermaid-svg-KJxV5tXfTJr02PMw .node circle,#mermaid-svg-KJxV5tXfTJr02PMw .node ellipse,#mermaid-svg-KJxV5tXfTJr02PMw .node polygon,#mermaid-svg-KJxV5tXfTJr02PMw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KJxV5tXfTJr02PMw .rough-node .label text,#mermaid-svg-KJxV5tXfTJr02PMw .node .label text,#mermaid-svg-KJxV5tXfTJr02PMw .image-shape .label,#mermaid-svg-KJxV5tXfTJr02PMw .icon-shape .label{text-anchor:middle;}#mermaid-svg-KJxV5tXfTJr02PMw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KJxV5tXfTJr02PMw .rough-node .label,#mermaid-svg-KJxV5tXfTJr02PMw .node .label,#mermaid-svg-KJxV5tXfTJr02PMw .image-shape .label,#mermaid-svg-KJxV5tXfTJr02PMw .icon-shape .label{text-align:center;}#mermaid-svg-KJxV5tXfTJr02PMw .node.clickable{cursor:pointer;}#mermaid-svg-KJxV5tXfTJr02PMw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KJxV5tXfTJr02PMw .arrowheadPath{fill:#333333;}#mermaid-svg-KJxV5tXfTJr02PMw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KJxV5tXfTJr02PMw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KJxV5tXfTJr02PMw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KJxV5tXfTJr02PMw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KJxV5tXfTJr02PMw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KJxV5tXfTJr02PMw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KJxV5tXfTJr02PMw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KJxV5tXfTJr02PMw .cluster text{fill:#333;}#mermaid-svg-KJxV5tXfTJr02PMw .cluster span{color:#333;}#mermaid-svg-KJxV5tXfTJr02PMw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KJxV5tXfTJr02PMw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KJxV5tXfTJr02PMw rect.text{fill:none;stroke-width:0;}#mermaid-svg-KJxV5tXfTJr02PMw .icon-shape,#mermaid-svg-KJxV5tXfTJr02PMw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KJxV5tXfTJr02PMw .icon-shape p,#mermaid-svg-KJxV5tXfTJr02PMw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KJxV5tXfTJr02PMw .icon-shape .label rect,#mermaid-svg-KJxV5tXfTJr02PMw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KJxV5tXfTJr02PMw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KJxV5tXfTJr02PMw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KJxV5tXfTJr02PMw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 01 创建函数
02 XHR+Promise
03 配置合并
04 响应对象
05 超时
06 取消
07 get/post
08 拦截器
对照官方 lib 源码
【代码注释】
- 每一步只扩展当前版本的
axios.js,用配套 HTML 在浏览器里验证;跳步会导致 bug 难定位。 - 步骤 01 完成后,可再引入官方打包的
axios.js,对比axios与axios.defaults的对象结构。 - 无模块化构建时,用 IIFE 把迷你 axios 挂到
window.axios,在控制台直接调试。 - 步骤 08 做完后,打开官方
lib/core/InterceptorManager.js与Axios.js#request,对照 Promise 链如何拼出来。
| 步骤 | 本步新增能力 | 与官方源码大致对应 |
|---|---|---|
| 01 | createInstance + bind |
lib/axios.js |
| 02 | dispatchRequest、XHR |
adapters/xhr.js + dispatchRequest.js |
| 03 | 配置合并(入门用 Object.assign) |
mergeConfig.js |
| 04 | resolve/reject 响应结构 | settle.js + 响应体组装 |
| 05 | ontimeout |
xhr 适配器内 |
| 06 | CancelToken | cancel/CancelToken.js |
| 07 | get/post 挂到 instance |
Axios.js 原型方法 |
| 08 | 拦截器链 | InterceptorManager.js + Axios.js request |
【实战要点】
- 入门版
createInstance往往只做到bind,尚未extend原型方法;§2.1 的完整版用for...in与getOwnPropertyNames补全,更接近官方utils.extend。 - 读源码时用 IDE「跳转到定义」从
axios(config)跟到Axios.prototype.request→ 拦截器链 →dispatchRequest。
【面试考点】
- 手写 axios 最小要实现哪几个函数/类?(
createInstance、Axios#request、dispatchRequest、XHR 适配器、可选InterceptorManager) - 官方
createInstance与「只做 bind」的入门版差在哪?(utils.extend复制原型方法与实例属性、instance.create递归合并默认配置)
一、Axios 源码架构分析
1.1 名词解释
- axios 实例 :对外暴露的「可调用函数」,内部
this绑定到Axios上下文,拥有defaults、interceptors与get/post等方法。 - 适配器(adapter) :真正发 HTTP 的模块;浏览器为
xhr.js,Node 为http.js,由dispatchRequest按环境选择。 - dispatchRequest :拦截器链执行完毕后,调用适配器并走
transformRequest/transformResponse的函数。 - mergeConfig :把「实例默认配置」与「本次请求配置」按字段策略深度合并,避免
headers被浅拷贝覆盖丢字段。 - settle :根据 HTTP
status决定 Promise 走resolve还是reject(2xx 为成功,其余为AxiosError)。 - InterceptorManager :维护
handlers数组,use注册、eject置空,供Axios#request拼 Promise 链。
Axios 本质不是「直接 new 出来的 axios」 ,而是一个通过 bind 委托了 Axios.prototype.request 的函数。调用 axios(config) 时,执行的是实例上的 request,this 指向持有 defaults 与 interceptors 的 Axios 对象。
1.2 概念与底层原理
官方一次请求的完整生命周期(见 axios 仓库 AGENTS.md)可概括为:
- 用户调用
axios(url)或axios(config); request内mergeConfig(this.defaults, config),再校验transitional、paramsSerializer等;- 用
buildFullPath+buildURL拼出最终 URL; - 请求拦截器 (LIFO)依次改写
config; dispatchRequest选适配器 →transformRequest→ 发网络请求;- 适配器返回后
settle根据状态码 resolve/reject; - 响应拦截器 (FIFO)处理
response或error; - 最外层 Promise 交给业务
then/catch或await。
为何 axios 是函数而不是 class 实例?
JavaScript 允许函数带属性。createInstance 用 bind(Axios.prototype.request, context) 得到可调用的 instance,再用 utils.extend 把 get/post、defaults、interceptors 挂到同一对象上,于是 API 同时支持 axios({ url }) 与 axios.get(url),且 axios.create() 可派生多实例(多 baseURL、多超时策略)。
mergeConfig 在 v1.x 的分层策略 (Configuration Merging):不同字段用不同合并规则------例如 url、method 以本次请求为准;baseURL、timeout、transformRequest 等默认「请求级覆盖实例级」;headers 常做深度合并。手写版用 Object.assign 是简化版,生产库必须处理 headers.common / headers.post 等嵌套,否则 POST 会丢全局 Content-Type。
xhr/http 适配器 dispatchRequest 拦截器链 Axios 业务代码 xhr/http 适配器 dispatchRequest 拦截器链 Axios 业务代码 #mermaid-svg-RpLKFVCbezen8fte{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RpLKFVCbezen8fte .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RpLKFVCbezen8fte .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RpLKFVCbezen8fte .error-icon{fill:#552222;}#mermaid-svg-RpLKFVCbezen8fte .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RpLKFVCbezen8fte .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RpLKFVCbezen8fte .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RpLKFVCbezen8fte .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RpLKFVCbezen8fte .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RpLKFVCbezen8fte .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RpLKFVCbezen8fte .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RpLKFVCbezen8fte .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RpLKFVCbezen8fte .marker.cross{stroke:#333333;}#mermaid-svg-RpLKFVCbezen8fte svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RpLKFVCbezen8fte p{margin:0;}#mermaid-svg-RpLKFVCbezen8fte .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RpLKFVCbezen8fte text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-RpLKFVCbezen8fte .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RpLKFVCbezen8fte .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-RpLKFVCbezen8fte .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-RpLKFVCbezen8fte .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-RpLKFVCbezen8fte #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-RpLKFVCbezen8fte .sequenceNumber{fill:white;}#mermaid-svg-RpLKFVCbezen8fte #sequencenumber{fill:#333;}#mermaid-svg-RpLKFVCbezen8fte #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-RpLKFVCbezen8fte .messageText{fill:#333;stroke:none;}#mermaid-svg-RpLKFVCbezen8fte .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RpLKFVCbezen8fte .labelText,#mermaid-svg-RpLKFVCbezen8fte .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-RpLKFVCbezen8fte .loopText,#mermaid-svg-RpLKFVCbezen8fte .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-RpLKFVCbezen8fte .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RpLKFVCbezen8fte .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-RpLKFVCbezen8fte .noteText,#mermaid-svg-RpLKFVCbezen8fte .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-RpLKFVCbezen8fte .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RpLKFVCbezen8fte .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RpLKFVCbezen8fte .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RpLKFVCbezen8fte .actorPopupMenu{position:absolute;}#mermaid-svg-RpLKFVCbezen8fte .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-RpLKFVCbezen8fte .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RpLKFVCbezen8fte .actor-man circle,#mermaid-svg-RpLKFVCbezen8fte line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-RpLKFVCbezen8fte :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} axios(config) mergeConfig 请求拦截器 LIFO config adapter(config) response 响应拦截器 FIFO Promise
【代码注释】
序列图把「配置合并 → 拦截器 → 适配器」串成一条线。追问「axios.get 和 axios(config) 走同一套吗」时答:便捷方法最终仍调用 request,只是预先填好 method 与 data/params 的位置。
1.3 源码目录结构
text
axios/
├── /dist/ # 项目输出目录
├── /lib/ # 项目源码目录
│ ├── adapters/ # 定义请求的适配器 xhr、http
│ │ ├── http.js # 实现 http 适配器(包装 http 包)
│ │ └── xhr.js # 实现 xhr 适配器(包装 xhr 对象)
│ ├── cancel/ # 定义取消功能
│ │ ├── CancelToken.js # 取消令牌实现
│ │ ├── CanceledError.js # 取消错误类
│ │ └── isCancel.js # 判断是否为取消错误
│ ├── core/ # 一些核心功能
│ │ ├── Axios.js # axios 的核心主类
│ │ ├── dispatchRequest.js # 用来调用 http 请求适配器方法发送请求的函数
│ │ ├── InterceptorManager.js # 拦截器的管理器
│ │ ├── settle.js # 根据 http 响应状态,改变 Promise 的状态
│ │ └── mergeConfig.js # 配置合并
│ ├── helpers/ # 一些辅助方法
│ │ ├── bind.js # 函数绑定工具
│ │ ├── buildURL.js # URL 参数构建
│ │ └── cookies.js # Cookie 处理
│ ├── defaults/ # axios 的默认配置
│ ├── axios.js # 对外暴露接口
│ └── utils.js # 公用工具
├── package.json # 项目信息
├── index.d.ts # 配置 TypeScript 的声明文件
└── index.js # 入口文件
【代码注释】 这是 Axios 完整的源码目录结构,核心模块包括:adapters/(适配器层,实现跨环境)、core/(核心逻辑,Axios 类与拦截器)、cancel/(请求取消机制)。理解目录结构有助于快速定位源码位置,如修改拦截器逻辑看 InterceptorManager.js,了解适配器差异对比 xhr.js 与 http.js。
1.4 Axios 运行流程
#mermaid-svg-iKAePAbtyuPBGgWt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iKAePAbtyuPBGgWt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iKAePAbtyuPBGgWt .error-icon{fill:#552222;}#mermaid-svg-iKAePAbtyuPBGgWt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iKAePAbtyuPBGgWt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iKAePAbtyuPBGgWt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iKAePAbtyuPBGgWt .marker.cross{stroke:#333333;}#mermaid-svg-iKAePAbtyuPBGgWt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iKAePAbtyuPBGgWt p{margin:0;}#mermaid-svg-iKAePAbtyuPBGgWt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iKAePAbtyuPBGgWt .cluster-label text{fill:#333;}#mermaid-svg-iKAePAbtyuPBGgWt .cluster-label span{color:#333;}#mermaid-svg-iKAePAbtyuPBGgWt .cluster-label span p{background-color:transparent;}#mermaid-svg-iKAePAbtyuPBGgWt .label text,#mermaid-svg-iKAePAbtyuPBGgWt span{fill:#333;color:#333;}#mermaid-svg-iKAePAbtyuPBGgWt .node rect,#mermaid-svg-iKAePAbtyuPBGgWt .node circle,#mermaid-svg-iKAePAbtyuPBGgWt .node ellipse,#mermaid-svg-iKAePAbtyuPBGgWt .node polygon,#mermaid-svg-iKAePAbtyuPBGgWt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iKAePAbtyuPBGgWt .rough-node .label text,#mermaid-svg-iKAePAbtyuPBGgWt .node .label text,#mermaid-svg-iKAePAbtyuPBGgWt .image-shape .label,#mermaid-svg-iKAePAbtyuPBGgWt .icon-shape .label{text-anchor:middle;}#mermaid-svg-iKAePAbtyuPBGgWt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iKAePAbtyuPBGgWt .rough-node .label,#mermaid-svg-iKAePAbtyuPBGgWt .node .label,#mermaid-svg-iKAePAbtyuPBGgWt .image-shape .label,#mermaid-svg-iKAePAbtyuPBGgWt .icon-shape .label{text-align:center;}#mermaid-svg-iKAePAbtyuPBGgWt .node.clickable{cursor:pointer;}#mermaid-svg-iKAePAbtyuPBGgWt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iKAePAbtyuPBGgWt .arrowheadPath{fill:#333333;}#mermaid-svg-iKAePAbtyuPBGgWt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iKAePAbtyuPBGgWt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iKAePAbtyuPBGgWt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iKAePAbtyuPBGgWt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iKAePAbtyuPBGgWt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iKAePAbtyuPBGgWt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iKAePAbtyuPBGgWt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iKAePAbtyuPBGgWt .cluster text{fill:#333;}#mermaid-svg-iKAePAbtyuPBGgWt .cluster span{color:#333;}#mermaid-svg-iKAePAbtyuPBGgWt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iKAePAbtyuPBGgWt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iKAePAbtyuPBGgWt rect.text{fill:none;stroke-width:0;}#mermaid-svg-iKAePAbtyuPBGgWt .icon-shape,#mermaid-svg-iKAePAbtyuPBGgWt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iKAePAbtyuPBGgWt .icon-shape p,#mermaid-svg-iKAePAbtyuPBGgWt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iKAePAbtyuPBGgWt .icon-shape .label rect,#mermaid-svg-iKAePAbtyuPBGgWt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iKAePAbtyuPBGgWt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iKAePAbtyuPBGgWt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iKAePAbtyuPBGgWt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 调用 axios/config
合并配置项
defaults + config
请求拦截器
后进先出 LIFO
分发请求
dispatchRequest
适配器选择
xhr/http
发送网络请求
响应拦截器
先进先出 FIFO
返回响应数据
【代码注释】 此流程图展示了一个完整请求的生命周期:从调用 axios 函数开始,经过配置合并、请求拦截器(LIFO顺序)、请求分发、适配器选择(浏览器用XHR、Node用http模块)、网络请求、响应拦截器(FIFO顺序),最终返回响应数据。拦截器的执行顺序是关键------请求拦截器后添加的先执行,响应拦截器先添加的先执行。
流程说明:
- 配置合并:将用户配置与默认配置合并
- 请求拦截器:按照后进先出(LIFO)顺序执行
- 请求分发 :调用
dispatchRequest选择适配器 - 网络请求:底层使用 XMLHttpRequest 或 http 模块
- 响应拦截器:按照先进先出(FIFO)顺序执行
- 数据返回:返回格式化后的响应数据
【实战要点】
- 断点建议:
Axios.js的request、dispatchRequest、xhr.js的onload。 - 请求拦截器里改
config.url要return config,否则后续拿到undefined。
【面试考点】
- 画出从
axios(config)到 XHRsend的调用栈(至少 4 层)。 - 为何响应拦截器用 FIFO、请求拦截器用 LIFO?
1.5 官方入口与入门实现的差异
官方 createInstance(lib/axios.js):
javascript
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = bind(Axios.prototype.request, context);
utils.extend(instance, Axios.prototype, context, { allOwnKeys: true });
utils.extend(instance, context, null, { allOwnKeys: true });
instance.create = function (instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
const axios = createInstance(defaults);
【代码注释】
- 入门版 (§2.1)通常只做到
bind返回 instance,尚未extend原型方法与get/post。 instance.create对应业务侧的axios.create,内部mergeConfig(defaultConfig, instanceConfig)合并默认配置后createInstance。- 导出时还挂载
CancelToken、isAxiosError、all、spread等静态工具(见文件后半段)。 - 无模块化时可将
createInstance包在 IIFE 里挂到window.axios,便于在 HTML 里直接调试。
1.6 建议精读的核心文件
| 文件 | 作用 |
|---|---|
core/Axios.js |
request:合并配置 → 走拦截器链 → dispatchRequest |
core/dispatchRequest.js |
选适配器(xhr/http),发真实请求 |
core/mergeConfig.js |
深度合并 defaults 与本次 config |
core/settle.js |
根据 status 决定 resolve 还是 reject |
core/InterceptorManager.js |
use / eject,handlers 数组 |
helpers/buildURL.js |
baseURL + url + params 拼完整地址 |
helpers/combineURLs.js、isAbsoluteURL.js |
相对路径与绝对路径拼接规则 |
adapters/xhr.js |
浏览器端 XHR 实现(与 §2.2 手写高度相似) |
【代码注释】
transformData.js、AxiosHeaders.js负责请求/响应头与体的转换,进阶阅读即可。dist/axios.min.js为打包产物,调试原理请读lib/源码。- 不必通读
helpers/下每一个流式/压缩相关文件,按表内顺序即可建立全局图。
【本章小结】
| 模块 | 职责 | 记忆点 |
|---|---|---|
lib/axios.js |
createInstance、导出 create |
axios 是 bind 出来的函数 |
core/Axios.js |
request、拦截器链 |
不直接发请求 |
dispatchRequest + adapters |
选 xhr/http、转换数据 | 跨环境关键 |
mergeConfig / settle |
合并配置、定 Promise 态 | 4xx 在 onload 里 reject |
InterceptorManager |
use/eject | 请求 LIFO、响应 FIFO |
记忆口诀 :"函数门面 request 心,合并配置再拦截;适配器里真发请求,settle 定成败。"
【面试考点】
Q1:从 axios(config) 到网络发出,至少经过哪四层?
A:createInstance 得到的函数 → Axios#request(mergeConfig)→ 请求拦截器链 → dispatchRequest 选适配器并 xhr.send。响应方向再走响应拦截器。答不出 mergeConfig 或 dispatchRequest 说明只背了 API。
Q2:mergeConfig 为什么不能简单 Object.assign?
A:headers 分 common、get、post 等桶,浅合并会覆盖整棵子树;url/method 又必须每次请求独立。官方对每类字段有 valueFromConfig2、defaultToConfig2、mergeDeepProperties 等策略。
Q3:浏览器端 axios 底层是 Fetch 还是 XHR?
A:默认 XHR (adapters/xhr.js)。因此支持 onUploadProgress、xhr.timeout;与 Fetch 的 AbortController 是另一套取消模型(v1.6+ 也支持 signal)。
二、自定义实现 Axios 核心
以下 §2.1~§2.7 与 §0.4 八步路线一一对应;每节附带可保存为
.html的完整示例,在本目录下打开即可运行。
2.1 创建 axios 函数
核心原理:axios 是一个函数,但拥有 Axios 类实例的所有属性和方法。
#mermaid-svg-eXwHG7eHK1ttIcEh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eXwHG7eHK1ttIcEh .error-icon{fill:#552222;}#mermaid-svg-eXwHG7eHK1ttIcEh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eXwHG7eHK1ttIcEh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eXwHG7eHK1ttIcEh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eXwHG7eHK1ttIcEh .marker.cross{stroke:#333333;}#mermaid-svg-eXwHG7eHK1ttIcEh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eXwHG7eHK1ttIcEh p{margin:0;}#mermaid-svg-eXwHG7eHK1ttIcEh g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-eXwHG7eHK1ttIcEh g.classGroup text .title{font-weight:bolder;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster-label text{fill:#333;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster-label span{color:#333;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster-label span p{background-color:transparent;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster text{fill:#333;}#mermaid-svg-eXwHG7eHK1ttIcEh .cluster span{color:#333;}#mermaid-svg-eXwHG7eHK1ttIcEh .nodeLabel,#mermaid-svg-eXwHG7eHK1ttIcEh .edgeLabel{color:#131300;}#mermaid-svg-eXwHG7eHK1ttIcEh .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-eXwHG7eHK1ttIcEh .label text{fill:#131300;}#mermaid-svg-eXwHG7eHK1ttIcEh .labelBkg{background:#ECECFF;}#mermaid-svg-eXwHG7eHK1ttIcEh .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-eXwHG7eHK1ttIcEh .classTitle{font-weight:bolder;}#mermaid-svg-eXwHG7eHK1ttIcEh .node rect,#mermaid-svg-eXwHG7eHK1ttIcEh .node circle,#mermaid-svg-eXwHG7eHK1ttIcEh .node ellipse,#mermaid-svg-eXwHG7eHK1ttIcEh .node polygon,#mermaid-svg-eXwHG7eHK1ttIcEh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eXwHG7eHK1ttIcEh .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh g.clickable{cursor:pointer;}#mermaid-svg-eXwHG7eHK1ttIcEh g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-eXwHG7eHK1ttIcEh g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-eXwHG7eHK1ttIcEh .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-eXwHG7eHK1ttIcEh .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-eXwHG7eHK1ttIcEh .dashed-line{stroke-dasharray:3;}#mermaid-svg-eXwHG7eHK1ttIcEh .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-eXwHG7eHK1ttIcEh #compositionStart,#mermaid-svg-eXwHG7eHK1ttIcEh .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #compositionEnd,#mermaid-svg-eXwHG7eHK1ttIcEh .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #dependencyStart,#mermaid-svg-eXwHG7eHK1ttIcEh .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #dependencyStart,#mermaid-svg-eXwHG7eHK1ttIcEh .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #extensionStart,#mermaid-svg-eXwHG7eHK1ttIcEh .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #extensionEnd,#mermaid-svg-eXwHG7eHK1ttIcEh .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #aggregationStart,#mermaid-svg-eXwHG7eHK1ttIcEh .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #aggregationEnd,#mermaid-svg-eXwHG7eHK1ttIcEh .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #lollipopStart,#mermaid-svg-eXwHG7eHK1ttIcEh .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh #lollipopEnd,#mermaid-svg-eXwHG7eHK1ttIcEh .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-eXwHG7eHK1ttIcEh .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-eXwHG7eHK1ttIcEh .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eXwHG7eHK1ttIcEh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eXwHG7eHK1ttIcEh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eXwHG7eHK1ttIcEh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 委托给实例
Axios
+defaults
+interceptors
+request()
+get()
+post()
axios
+defaults
+interceptors
+request()
+get()
+post()
Function
+call()
【代码注释】 此类图揭示了 axios 的双重身份------既是函数(继承自 Function),又拥有 Axios 类实例的所有属性和方法(通过委托)。这种设计让 axios(config) 和 axios.get(url) 两种调用方式都有效。委托实现通过 bind(Axios.prototype.request, context) 完成,调用 axios() 实际执行 context.request(),且 this 正确指向实例 context。
入门示例:基础结构
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Axios 函数创建原理</title>
</head>
<body>
<script type="module">
// 定义默认请求配置项
const defaults = {
timeout: 0
};
// 核心类
class Axios {
constructor(instanceConfig) {
this.defaults = instanceConfig;
}
request() {
console.log('request 方法被调用');
return Promise.resolve('模拟请求');
}
}
// 函数绑定工具
function bind(fn, thisArg) {
return function wrap() {
// fn 就是 Axios.prototype.request
// arguments 是伪数组,成员是传递给 axios 函数的参数
// 就是在调用 Axios.prototype.request,并设置里面的 this 为 Axios 类的一个实例
return fn.apply(thisArg, arguments);
}
}
/**
* 创建 axios 函数
* axios 函数本质不是 Axios 的实例,而是委托给实例的方法
*/
function createInstance(defaultConfig) {
// 实例化核心类得到实例
const context = new Axios(defaultConfig);
// 创建 axios 函数,绑定 request 方法到 context
const instance = bind(Axios.prototype.request, context);
// 将 Axios 实例自身的所有属性都添加到 axios 上
for (let key in context) {
instance[key] = context[key];
}
// 将 Axios 实例的原型上所有属性都添加到 axios 上
Object.getOwnPropertyNames(Axios.prototype).forEach(key => {
instance[key] = Axios.prototype[key].bind(context);
});
return instance;
}
// 创建 axios
const axios = createInstance(defaults);
// 测试
console.log('=== axios 对象结构 ===');
console.log('axios 是函数吗?', typeof axios === 'function'); // true
console.log('axios 是 Axios 实例吗?', axios instanceof Axios); // false
console.log('axios 有 defaults 属性吗?', 'defaults' in axios); // true
console.log('axios.defaults:', axios.defaults);
console.log('\n=== 调用 axios 函数 ===');
axios().then(result => console.log('请求结果:', result));
</script>
</body>
</html>
【代码注释】 bind 函数实现了函数委托模式 ------让 axios 函数调用时实际执行 Axios.prototype.request,且内部的 this 正确指向 Axios 实例 context。这种设计让 axios 既是函数又有属性方法,比单纯用类更灵活。市面应用 :Lodash 的 _.throttle、React Redux 的 connect 高阶组件都用类似模式------对外暴露函数,内部委托给实例。
【本章小结】
| 特性 | 说明 |
|---|---|
| 设计模式 | 函数委托模式 + 原型继承 |
| axios 本质 | 函数(通过 bind 创建) |
| 属性来源 | 从 Axios 实例复制而来 |
| 方法来源 | 从 Axios.prototype.bind(context) 而来 |
记忆口诀 :"axios 是函数,属性实例绑;方法原型来,this 指实例"
【面试考点】
Q1:为什么 axios 既是函数又是对象?
A:这是通过函数委托模式 实现的------createInstance 中用 bind(Axios.prototype.request, context) 创建函数,再把实例属性(defaults、interceptors)和原型方法(get、post)挂到函数上。技术上利用了 JS 中"函数也是对象"的特性。追问"为什么不直接用类"时答:函数更灵活,支持 axios(config) 和 axios.get(url) 两种调用方式,API 更简洁。
2.2 实现请求发送与 Promise 封装
核心原理 :基于 XMLHttpRequest 封装,返回 Promise 对象支持链式调用。
入门示例:基础请求发送
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Axios 请求发送与 Promise 封装</title>
</head>
<body>
<h1>Axios 请求发送测试</h1>
<div id="result"></div>
<script type="module">
// 定义默认请求配置项
const defaults = {
timeout: 0,
responseType: 'json'
};
/**
* 发送 ajax 请求
* @param {object} config 请求配置项
* @returns {Promise}
*/
function dispatchRequest(config) {
return new Promise((resolve, reject) => {
// 创建 xhr 对象
const xhr = new XMLHttpRequest();
// 设置响应类型
xhr.responseType = config.responseType;
// 初始化
xhr.open(config.method, config.url);
// 发送
xhr.send();
// 监听成功响应
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
data: xhr.response,
status: xhr.status,
statusText: xhr.statusText,
headers: xhr.getAllResponseHeaders(),
config: config,
request: xhr
});
} else {
reject({
code: "ERR_BAD_REQUEST",
config,
message: "Request failed with status code " + xhr.status,
name: "AxiosError",
request: xhr
});
}
}
// 监听失败响应
xhr.onerror = () => {
reject({
code: "ERR_NETWORK",
config,
message: "ERR_NETWORK",
name: "AxiosError",
request: xhr
});
}
});
}
// 核心类
class Axios {
constructor(instanceConfig) {
this.defaults = instanceConfig;
}
/**
* 发送 ajax 请求
* @param {String|Object} configOrUrl url 或者请求配置对象
* @param {?Object} config 请求配置对象
* @returns {Promise}
*/
request(configOrUrl = {}, config = {}) {
// 判断第一个参数是否是 url
if (typeof configOrUrl === 'string') {
config.url = configOrUrl;
} else {
config = configOrUrl;
}
// 将传入的请求配置对象和全局请求配置对象合并
config = Object.assign({}, this.defaults, config);
// 设置默认请求方式是 GET
config.method = (config.method || this.defaults.method || 'get').toUpperCase();
// 调用函数发送请求
return dispatchRequest.call(this, config);
}
}
function bind(fn, thisArg) {
return function wrap() {
return fn.apply(thisArg, arguments);
}
}
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = bind(Axios.prototype.request, context);
for (let key in context) {
instance[key] = context[key];
}
Object.getOwnPropertyNames(Axios.prototype).forEach(key => {
instance[key] = Axios.prototype[key].bind(context);
});
return instance;
}
// 创建 axios
const axios = createInstance(defaults);
// 测试请求
console.log('=== 发送 GET 请求 ===');
// 使用 GitHub API 测试
axios({
url: 'https://api.github.com/users',
method: 'GET'
})
.then(response => {
console.log('请求成功:', response);
document.getElementById('result').innerHTML = `
<h2>请求成功</h2>
<p>状态码: ${response.status}</p>
<p>数据长度: ${response.data.length} 个用户</p>
<pre>${JSON.stringify(response.data[0], null, 2)}</pre>
`;
})
.catch(error => {
console.error('请求失败:', error);
document.getElementById('result').innerHTML = `
<h2>请求失败</h2>
<p>错误信息: ${error.message}</p>
`;
});
</script>
</body>
</html>
【代码注释】 dispatchRequest 是真正的请求执行函数 ------创建 XHR 对象、设置配置、监听事件、返回 Promise。request 方法负责配置合并与参数处理 ,然后调用 dispatchRequest。分离设计让代码职责清晰:request 处理配置,dispatchRequest 处理网络。市面应用 :这种"配置处理 + 执行分离"模式在 fetch-retry、axios-mock-adapter 等库中广泛使用,便于中间件插入。
【实战要点】
- 经典应用场景:XHR 是浏览器最稳定的请求 API,支持进度监控、超时控制、取消请求,而 Fetch API 在旧浏览器不兼容
- 常见坑 :忘记
xhr.send()会导致请求不发送;xhr.onload只在网络层成功时触发,HTTP 4xx/5xx 也算成功,需手动判断status - 性能与最佳实践 :设置
responseType: 'json'让浏览器自动解析 JSON,比JSON.parse(xhr.responseText)性能更好且更安全
【本章小结】
| 职责 | 函数 | 说明 |
|---|---|---|
| 配置合并 | request |
Object.assign(defaults, config),method 转大写 |
| 真正发请求 | dispatchRequest |
返回 Promise,内部 new XHR |
| 成功判定 | xhr.onload |
仅 2xx resolve,其余 reject 为 AxiosError |
【面试考点】
Q4:dispatchRequest 为什么要单独拆出来?
A:拦截器只改 config,不应关心 XHR 细节;dispatchRequest 是链的「终点」,之后才可换适配器(xhr/http/fetch)。测试时也可 mock dispatchRequest 而不发真实网络请求。
Q5:request 里 dispatchRequest.call(this, config) 的 this 有什么用?
A:后续若要在适配器里读 this.defaults 或挂实例级状态,需要绑定实例;与官方 dispatchRequest 作为纯函数略有不同,手写版保留 call(this) 便于扩展。
2.3 请求配置项深度解析
Axios 支持丰富的配置项,以下是核心配置的完整实现。
实战示例:完整配置项支持
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Axios 请求配置项完整示例</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.config-item {
background: #f5f5f5;
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
button {
padding: 10px 20px;
margin: 10px 5px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Axios 请求配置项演示</h1>
<div class="config-item">
<h3>1. baseURL + url</h3>
<button onclick="testBaseURL()">测试 baseURL</button>
<div id="baseURL-result"></div>
</div>
<div class="config-item">
<h3>2. params 参数</h3>
<button onclick="testParams()">测试 params</button>
<div id="params-result"></div>
</div>
<div class="config-item">
<h3>3. headers 请求头</h3>
<button onclick="testHeaders()">测试 headers</button>
<div id="headers-result"></div>
</div>
<div class="config-item">
<h3>4. data 请求体</h3>
<button onclick="testData()">测试 data</button>
<div id="data-result"></div>
</div>
<div class="config-item">
<h3>5. responseType 响应类型</h3>
<button onclick="testResponseType()">测试 responseType</button>
<div id="responseType-result"></div>
</div>
<script type="module">
// 定义默认请求配置项
const defaults = {
timeout: 0,
responseType: 'json'
};
/**
* 发送 ajax 请求
*/
function dispatchRequest(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = config.responseType;
// 初始化
xhr.open(config.method, config.url);
// 设置请求头
if (config.headers) {
for (let key in config.headers) {
xhr.setRequestHeader(key, config.headers[key]);
}
}
// 设置请求体
let body;
// 只有允许的请求方法,才可以携带请求体
if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
if (typeof config.data === 'string') {
body = config.data;
// 如果没有显式设置 Content-Type,则设置默认值
if (!config.headers?.['Content-Type']) {
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
}
} else if (Object.prototype.toString.call(config.data) === '[object Object]') {
body = JSON.stringify(config.data);
if (!config.headers?.['Content-Type']) {
xhr.setRequestHeader('Content-type', 'application/json');
}
} else {
body = config.data;
}
}
// 发送
xhr.send(body);
// 监听响应
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
data: xhr.response,
status: xhr.status,
statusText: xhr.statusText,
headers: xhr.getAllResponseHeaders(),
config: config,
request: xhr
});
} else {
reject({
code: "ERR_BAD_REQUEST",
config,
message: "Request failed with status code " + xhr.status,
name: "AxiosError",
request: xhr
});
}
};
xhr.onerror = () => {
reject({
code: "ERR_NETWORK",
config,
message: "ERR_NETWORK",
name: "AxiosError",
request: xhr
});
};
});
}
// 核心类
class Axios {
constructor(instanceConfig) {
this.defaults = instanceConfig;
}
request(configOrUrl = {}, config = {}) {
// 判断第一个参数是否是 url
if (typeof configOrUrl === 'string') {
config.url = configOrUrl;
} else {
config = configOrUrl;
}
// 合并配置项
config = Object.assign({}, this.defaults, config);
// 设置默认请求方式
config.method = (config.method || this.defaults.method || 'get').toUpperCase();
// 合并 baseURL 和 url
if (config.baseURL && !config.url.startsWith('http://') && !config.url.startsWith('https://')) {
config.url = config.baseURL + config.url;
}
// 将 params 拼接到 url 后面
if (config.params) {
const params = Object.entries(config.params)
.map(item => item[0] + '=' + encodeURIComponent(item[1]))
.join('&');
config.url += '?' + params;
}
return dispatchRequest.call(this, config);
}
get(url, config = {}) {
return this.request(url, { ...config, method: 'GET' });
}
post(url, data, config = {}) {
return this.request(url, { ...config, data, method: 'POST' });
}
}
function bind(fn, thisArg) {
return function wrap() {
return fn.apply(thisArg, arguments);
}
}
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = bind(Axios.prototype.request, context);
for (let key in context) {
instance[key] = context[key];
}
Object.getOwnPropertyNames(Axios.prototype).forEach(key => {
instance[key] = Axios.prototype[key].bind(context);
});
return instance;
}
// 创建 axios 并设置全局配置
const axios = createInstance(defaults);
// 全局配置示例
axios.defaults.baseURL = 'https://api.github.com';
axios.defaults.headers = {
'Accept': 'application/vnd.github.v3+json'
};
// 暴露测试函数到全局
window.testBaseURL = () => {
axios({
url: '/users',
method: 'GET'
})
.then(response => {
document.getElementById('baseURL-result').innerHTML = `
<p><strong>请求 URL:</strong> ${response.config.url}</p>
<p><strong>用户数量:</strong> ${response.data.length}</p>
`;
})
.catch(error => {
document.getElementById('baseURL-result').innerHTML = `
<p style="color: red;">错误: ${error.message}</p>
`;
});
};
window.testParams = () => {
axios.get('/search/repositories', {
params: {
q: 'javascript',
sort: 'stars'
}
})
.then(response => {
document.getElementById('params-result').innerHTML = `
<p><strong>请求 URL:</strong> ${response.config.url}</p>
<p><strong>仓库数量:</strong> ${response.data.total_count}</p>
`;
})
.catch(error => {
document.getElementById('params-result').innerHTML = `
<p style="color: red;">错误: ${error.message}</p>
`;
});
};
window.testHeaders = () => {
axios.get('/users/github', {
headers: {
'User-Agent': 'My-Axios-App/1.0'
}
})
.then(response => {
document.getElementById('headers-result').innerHTML = `
<p><strong>请求头:</strong></p>
<pre>${JSON.stringify(response.config.headers, null, 2)}</pre>
<p><strong>用户名:</strong> ${response.data.name}</p>
`;
})
.catch(error => {
document.getElementById('headers-result').innerHTML = `
<p style="color: red;">错误: ${error.message}</p>
`;
});
};
window.testData = () => {
// 使用 JSONPlaceholder 测试 POST 请求
const axios2 = createInstance({ responseType: 'json' });
axios2.post('https://jsonplaceholder.typicode.com/posts', {
title: 'foo',
body: 'bar',
userId: 1
})
.then(response => {
document.getElementById('data-result').innerHTML = `
<p><strong>请求数据:</strong> {title: 'foo', body: 'bar', userId: 1}</p>
<p><strong>响应数据:</strong></p>
<pre>${JSON.stringify(response.data, null, 2)}</pre>
`;
})
.catch(error => {
document.getElementById('data-result').innerHTML = `
<p style="color: red;">错误: ${error.message}</p>
`;
});
};
window.testResponseType = () => {
// 测试文本响应
const axios3 = createInstance({ responseType: 'text' });
axios3.get('https://api.github.com/zen')
.then(response => {
document.getElementById('responseType-result').innerHTML = `
<p><strong>响应类型:</strong> text</p>
<p><strong>Zen 格言:</strong> ${response.data}</p>
`;
})
.catch(error => {
document.getElementById('responseType-result').innerHTML = `
<p style="color: red;">错误: ${error.message}</p>
`;
});
};
</script>
</body>
</html>
【代码注释】 baseURL + url 合并采用前缀检测 ------只有相对路径(非 http:// 或 https:// 开头)才拼接,避免破坏完整 URL。params 处理使用 encodeURIComponent 编码参数值,防止特殊字符(如空格、&)破坏 URL 结构。data 处理根据类型自动设置 Content-Type------对象转 JSON、字符串直接发送。市面应用 :REST API 调用几乎都用 baseURL(如 https://api.example.com/v1)+ 相对路径,方便环境切换;params 用于 GET 请求的查询参数构建。
【实战要点】
- 经典应用场景 :
baseURL用于环境管理 (开发/测试/生产 API 地址切换);params用于列表筛选 、搜索 ;headers用于身份认证 (Authorization: Bearer token) - 常见坑 :
params值未编码导致 URL 解析错误(如{q: 'a&b'}变成?q=a&b,b被当成独立参数)- POST 发送对象时忘设置
Content-Type: application/json,后端无法解析 baseURL末尾斜杠不一致(https://api.com+/v1/usersvshttps://api.com/+v1/users)导致双斜杠或缺少斜杠
- 性能与最佳实践 :
- 全局配置用
axios.defaults.baseURL,避免每次重复设置 - 敏感信息(如 token)不要放在 URL(
params),应放headers避免日志泄露 - 大文件上传用
FormData,不要手动转 JSON
- 全局配置用
【本章小结】
| 配置项 | 作用 | 常见值 |
|---|---|---|
| baseURL | 基础 URL,自动拼接相对路径 | https://api.example.com |
| url | 请求路径 | /users、https://other.com/api |
| params | URL 查询参数 | {page: 1, size: 10} |
| headers | 请求头 | {'Authorization': 'Bearer token'} |
| data | 请求体(POST/PUT/PATCH) | {name: 'John'}、FormData |
| method | 请求方法 | 'GET'、'POST'、'PUT'、'DELETE' |
| responseType | 响应数据类型 | 'json'、'text'、'blob'、'arraybuffer' |
记忆口诀 :"base 接路径,params 查询配;headers 身份认,data 请求体;method 方法定,response 类型归"
【面试考点】
Q2:axios 如何实现跨环境的 baseURL 处理?
A:axios 的 baseURL 只对相对路径 生效,检测逻辑是 !url.startsWith('http://') && !url.startsWith('https://')。这样设计让完整 URL(如第三方 API)不受 baseURL 影响。追问"如何处理不同环境的 baseURL"时答:通过构建工具(如 Webpack 的 DefinePlugin)注入环境变量,axios.defaults.baseURL = process.env.API_BASE_URL。
Q3:为什么 POST 请求的 data 要根据类型设置不同的 Content-Type?
A:因为 HTTP 协议规定请求体格式必须与 Content-Type 匹配 ,后端才能正确解析。对象转 JSON 需设置 application/json,字符串如 a=1&b=2 需 application/x-www-form-urlencoded,文件上传用 multipart/form-data。axios 能自动识别对象和字符串,但 FormData 需手动设置。
2.4 响应结果处理机制
Axios 的响应对象结构标准化,便于统一处理。
#mermaid-svg-xjGDfo1hVwa3Snji{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xjGDfo1hVwa3Snji .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xjGDfo1hVwa3Snji .error-icon{fill:#552222;}#mermaid-svg-xjGDfo1hVwa3Snji .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xjGDfo1hVwa3Snji .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xjGDfo1hVwa3Snji .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xjGDfo1hVwa3Snji .marker.cross{stroke:#333333;}#mermaid-svg-xjGDfo1hVwa3Snji svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xjGDfo1hVwa3Snji p{margin:0;}#mermaid-svg-xjGDfo1hVwa3Snji .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xjGDfo1hVwa3Snji .cluster-label text{fill:#333;}#mermaid-svg-xjGDfo1hVwa3Snji .cluster-label span{color:#333;}#mermaid-svg-xjGDfo1hVwa3Snji .cluster-label span p{background-color:transparent;}#mermaid-svg-xjGDfo1hVwa3Snji .label text,#mermaid-svg-xjGDfo1hVwa3Snji span{fill:#333;color:#333;}#mermaid-svg-xjGDfo1hVwa3Snji .node rect,#mermaid-svg-xjGDfo1hVwa3Snji .node circle,#mermaid-svg-xjGDfo1hVwa3Snji .node ellipse,#mermaid-svg-xjGDfo1hVwa3Snji .node polygon,#mermaid-svg-xjGDfo1hVwa3Snji .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xjGDfo1hVwa3Snji .rough-node .label text,#mermaid-svg-xjGDfo1hVwa3Snji .node .label text,#mermaid-svg-xjGDfo1hVwa3Snji .image-shape .label,#mermaid-svg-xjGDfo1hVwa3Snji .icon-shape .label{text-anchor:middle;}#mermaid-svg-xjGDfo1hVwa3Snji .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xjGDfo1hVwa3Snji .rough-node .label,#mermaid-svg-xjGDfo1hVwa3Snji .node .label,#mermaid-svg-xjGDfo1hVwa3Snji .image-shape .label,#mermaid-svg-xjGDfo1hVwa3Snji .icon-shape .label{text-align:center;}#mermaid-svg-xjGDfo1hVwa3Snji .node.clickable{cursor:pointer;}#mermaid-svg-xjGDfo1hVwa3Snji .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xjGDfo1hVwa3Snji .arrowheadPath{fill:#333333;}#mermaid-svg-xjGDfo1hVwa3Snji .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xjGDfo1hVwa3Snji .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xjGDfo1hVwa3Snji .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xjGDfo1hVwa3Snji .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xjGDfo1hVwa3Snji .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xjGDfo1hVwa3Snji .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xjGDfo1hVwa3Snji .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xjGDfo1hVwa3Snji .cluster text{fill:#333;}#mermaid-svg-xjGDfo1hVwa3Snji .cluster span{color:#333;}#mermaid-svg-xjGDfo1hVwa3Snji div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xjGDfo1hVwa3Snji .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xjGDfo1hVwa3Snji rect.text{fill:none;stroke-width:0;}#mermaid-svg-xjGDfo1hVwa3Snji .icon-shape,#mermaid-svg-xjGDfo1hVwa3Snji .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xjGDfo1hVwa3Snji .icon-shape p,#mermaid-svg-xjGDfo1hVwa3Snji .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xjGDfo1hVwa3Snji .icon-shape .label rect,#mermaid-svg-xjGDfo1hVwa3Snji .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xjGDfo1hVwa3Snji .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xjGDfo1hVwa3Snji .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xjGDfo1hVwa3Snji :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 200-299
其他
XHR 响应
状态码判断
构建成功响应
构建错误响应
resolve response
reject error
【代码注释】 此流程图说明响应处理的核心逻辑------根据 HTTP 状态码决定 Promise 的走向。200-299 范围的状态码被视为成功,resolve 响应对象;其他状态码(4xx、5xx)被视为失败,reject 错误对象。关键点:xhr.onload 在所有 HTTP 请求完成后触发,包括 404、500,因此必须手动判断 status。这与 xhr.onerror 不同------后者只在网络层失败时触发。
实战示例:响应处理完整实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Axios 响应处理机制</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 50px auto;
padding: 20px;
}
.response-box {
background: #f5f5f5;
padding: 15px;
margin: 15px 0;
border-radius: 5px;
border-left: 4px solid #4CAF50;
}
.error-box {
background: #ffebee;
padding: 15px;
margin: 15px 0;
border-radius: 5px;
border-left: 4px solid #f44336;
}
button {
padding: 10px 20px;
margin: 5px;
cursor: pointer;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
}
button:hover {
background: #0b7dda;
}
pre {
background: white;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
</style>
</head>
<body>
<h1>Axios 响应处理机制演示</h1>
<div>
<button onclick="testSuccess()">测试成功响应 (200)</button>
<button onclick="testNotFound()">测试 404 错误</button>
<button onclick="testNetworkError()">测试网络错误</button>
</div>
<div id="result"></div>
<script type="module">
const defaults = {
timeout: 5000,
responseType: 'json'
};
function dispatchRequest(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = config.responseType;
xhr.timeout = config.timeout;
xhr.open(config.method, config.url);
if (config.headers) {
for (let key in config.headers) {
xhr.setRequestHeader(key, config.headers[key]);
}
}
let body;
if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
if (typeof config.data === 'string') {
body = config.data;
} else if (Object.prototype.toString.call(config.data) === '[object Object]') {
body = JSON.stringify(config.data);
if (!config.headers?.['Content-Type']) {
xhr.setRequestHeader('Content-type', 'application/json');
}
} else {
body = config.data;
}
}
xhr.send(body);
// 监听成功响应
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
// 数据
data: xhr.response,
// 状态码
status: xhr.status,
// 状态文本
statusText: xhr.statusText,
// 响应头(原始字符串)
headers: xhr.getAllResponseHeaders(),
// 响应头(解析为对象)
headersObj: parseHeaders(xhr.getAllResponseHeaders()),
// 请求配置
config: config,
// XHR 对象
request: xhr
});
} else {
// HTTP 错误(4xx, 5xx)
reject({
code: "ERR_BAD_REQUEST",
config,
message: `Request failed with status code ${xhr.status}`,
name: "AxiosError",
request: xhr,
response: {
data: xhr.response,
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders())
}
});
}
};
// 监听网络错误
xhr.onerror = () => {
reject({
code: "ERR_NETWORK",
config,
message: "Network Error",
name: "AxiosError",
request: xhr
});
};
// 监听超时
xhr.ontimeout = () => {
reject({
code: "ECONNABORTED",
config,
message: `timeout of ${config.timeout}ms exceeded`,
name: "AxiosError",
request: xhr
});
};
});
}
// 解析响应头为对象
function parseHeaders(headersStr) {
if (!headersStr) return {};
return headersStr
.split('\r\n')
.filter(line => line.trim())
.reduce((obj, line) => {
const [key, ...valueParts] = line.split(': ');
const value = valueParts.join(': '); // 处理值中包含 ": " 的情况
if (key) {
obj[key] = value;
}
return obj;
}, {});
}
// Axios 类省略,直接使用简化版本
class Axios {
constructor(config) {
this.defaults = config;
}
request(urlOrConfig, config = {}) {
if (typeof urlOrConfig === 'string') {
config.url = urlOrConfig;
} else {
config = urlOrConfig;
}
config = Object.assign({}, this.defaults, config);
config.method = (config.method || 'get').toUpperCase();
return dispatchRequest(config);
}
get(url, config = {}) {
return this.request(url, { ...config, method: 'GET' });
}
post(url, data, config = {}) {
return this.request(url, { ...config, data, method: 'POST' });
}
}
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = function(config) {
return context.request(config);
};
instance.defaults = context.defaults;
['get', 'post', 'request'].forEach(method => {
instance[method] = context[method].bind(context);
});
return instance;
}
const axios = createInstance(defaults);
// 测试函数
window.testSuccess = () => {
axios.get('https://api.github.com/users/github')
.then(response => {
displayResponse('成功响应示例', response, false);
})
.catch(error => {
displayError(error);
});
};
window.testNotFound = () => {
axios.get('https://api.github.com/users/this-user-definitely-does-not-exist-12345')
.then(response => {
displayResponse('响应', response, false);
})
.catch(error => {
displayResponse('404 错误响应(注意:HTTP错误也返回response)', error, true);
});
};
window.testNetworkError = () => {
axios.get('https://this-domain-definitely-does-not-exist-12345.com')
.then(response => {
displayResponse('响应', response, false);
})
.catch(error => {
displayError(error);
});
};
function displayResponse(title, data, isError = false) {
const boxClass = isError ? 'error-box' : 'response-box';
const html = `
<div class="${boxClass}">
<h3>${title}</h3>
<p><strong>状态码:</strong> ${data.status || 'N/A'}</p>
<p><strong>状态文本:</strong> ${data.statusText || 'N/A'}</p>
${data.code ? `<p><strong>错误码:</strong> ${data.code}</p>` : ''}
<p><strong>消息:</strong> ${data.message || 'Success'}</p>
${data.data ? `<p><strong>响应数据:</strong></p><pre>${JSON.stringify(data.data, null, 2)}</pre>` : ''}
${data.headersObj ? `<p><strong>响应头:</strong></p><pre>${JSON.stringify(data.headersObj, null, 2)}</pre>` : ''}
${data.config ? `<p><strong>请求配置:</strong></p><pre>${JSON.stringify({url: data.config.url, method: data.config.method}, null, 2)}</pre>` : ''}
</div>
`;
document.getElementById('result').innerHTML = html;
}
function displayError(error) {
const html = `
<div class="error-box">
<h3>网络错误</h3>
<p><strong>错误码:</strong> ${error.code}</p>
<p><strong>错误消息:</strong> ${error.message}</p>
<p><strong>错误名称:</strong> ${error.name}</p>
<p><strong>请求配置:</strong></p>
<pre>${JSON.stringify({url: error.config?.url, method: error.config?.method}, null, 2)}</pre>
</div>
`;
document.getElementById('result').innerHTML = html;
}
</script>
</body>
</html>
【代码注释】 响应处理的核心是区分 HTTP 错误与网络错误 ------xhr.onload 会在 HTTP 请求完成时触发(包括 404、500),需手动判断 status 范围;xhr.onerror 只在网络层失败(如 DNS 解析失败、跨域被阻止)时触发。parseHeaders 函数处理多行响应头解析 ,用 split('\r\n') 分割后逐行提取 key: value。市面应用:这种响应结构在所有 HTTP 客户端中通用(Fetch、Request、SuperAgent),便于错误处理中间件复用。
【实战要点】
- 经典应用场景 :
- 统一错误处理 :根据
error.code区分错误类型,ERR_NETWORK提示"网络连接失败",ERR_BAD_REQUEST提示"服务器返回错误" - 响应日志记录 :
response.config.url+response.status构成请求日志,用于调试 - 响应拦截器 :统一提取
response.data,简化业务代码
- 统一错误处理 :根据
- 常见坑 :
- 误以为
xhr.onerror会捕获 4xx/5xx,其实这些在xhr.onload中 - 忘记
responseType: 'json'导致response.data是字符串而非对象 - 跨域请求时,
xhr.status为 0 且xhr.onerror触发(CORS 被阻止)
- 误以为
- 性能与最佳实践 :
- 用
response.data直接获取解析后数据,避免重复JSON.parse - 响应拦截器中统一处理
error.response,避免每个请求单独写catch
- 用
【本章小结】
| 事件 | 何时触发 | 业务含义 |
|---|---|---|
xhr.onload |
HTTP 完成(含 404/500) | 用 status 区分成功/失败 |
xhr.onerror |
网络层失败、CORS 拦截 | status 常为 0 |
xhr.ontimeout |
超过 xhr.timeout |
ECONNABORTED |
响应对象建议统一:{ data, status, statusText, headers, config, request },与官方 AxiosResponse 对齐,方便拦截器只返回 response.data。
【面试考点】
Q6:为什么 404 不会进 xhr.onerror?
A:onerror 表示传输层 失败;404 是服务器正常返回的 HTTP 语义错误,状态码在 onload 里可读。axios 在 onload 里对非 2xx reject,才能被 catch 或响应拦截器的错误分支捕获。
Q7:跨域失败时 status 为什么是 0?
A:浏览器安全策略下,JS 读不到真实状态码,XHR 以失败结束,常表现为 onerror + status === 0。这与 CORS 响应头未配置有关,不是 axios 特例。
2.5 超时设置与错误处理
超时机制防止请求无限等待,提升用户体验。
#mermaid-svg-sf5krCk4LsYq1Z8R{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sf5krCk4LsYq1Z8R .error-icon{fill:#552222;}#mermaid-svg-sf5krCk4LsYq1Z8R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sf5krCk4LsYq1Z8R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sf5krCk4LsYq1Z8R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sf5krCk4LsYq1Z8R .marker.cross{stroke:#333333;}#mermaid-svg-sf5krCk4LsYq1Z8R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sf5krCk4LsYq1Z8R p{margin:0;}#mermaid-svg-sf5krCk4LsYq1Z8R .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster-label text{fill:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster-label span{color:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster-label span p{background-color:transparent;}#mermaid-svg-sf5krCk4LsYq1Z8R .label text,#mermaid-svg-sf5krCk4LsYq1Z8R span{fill:#333;color:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R .node rect,#mermaid-svg-sf5krCk4LsYq1Z8R .node circle,#mermaid-svg-sf5krCk4LsYq1Z8R .node ellipse,#mermaid-svg-sf5krCk4LsYq1Z8R .node polygon,#mermaid-svg-sf5krCk4LsYq1Z8R .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sf5krCk4LsYq1Z8R .rough-node .label text,#mermaid-svg-sf5krCk4LsYq1Z8R .node .label text,#mermaid-svg-sf5krCk4LsYq1Z8R .image-shape .label,#mermaid-svg-sf5krCk4LsYq1Z8R .icon-shape .label{text-anchor:middle;}#mermaid-svg-sf5krCk4LsYq1Z8R .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sf5krCk4LsYq1Z8R .rough-node .label,#mermaid-svg-sf5krCk4LsYq1Z8R .node .label,#mermaid-svg-sf5krCk4LsYq1Z8R .image-shape .label,#mermaid-svg-sf5krCk4LsYq1Z8R .icon-shape .label{text-align:center;}#mermaid-svg-sf5krCk4LsYq1Z8R .node.clickable{cursor:pointer;}#mermaid-svg-sf5krCk4LsYq1Z8R .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sf5krCk4LsYq1Z8R .arrowheadPath{fill:#333333;}#mermaid-svg-sf5krCk4LsYq1Z8R .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sf5krCk4LsYq1Z8R .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sf5krCk4LsYq1Z8R .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sf5krCk4LsYq1Z8R .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sf5krCk4LsYq1Z8R .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sf5krCk4LsYq1Z8R .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster text{fill:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R .cluster span{color:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sf5krCk4LsYq1Z8R .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sf5krCk4LsYq1Z8R rect.text{fill:none;stroke-width:0;}#mermaid-svg-sf5krCk4LsYq1Z8R .icon-shape,#mermaid-svg-sf5krCk4LsYq1Z8R .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sf5krCk4LsYq1Z8R .icon-shape p,#mermaid-svg-sf5krCk4LsYq1Z8R .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sf5krCk4LsYq1Z8R .icon-shape .label rect,#mermaid-svg-sf5krCk4LsYq1Z8R .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sf5krCk4LsYq1Z8R .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sf5krCk4LsYq1Z8R .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sf5krCk4LsYq1Z8R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} < timeout
>= timeout
发起请求
设置 xhr.timeout
响应时间
触发 xhr.ontimeout
正常响应
reject ECONNABORTED
正常处理
【代码注释】 此图展示超时机制的决策流程------当响应时间小于设定值时走正常响应路径,否则触发 xhr.ontimeout 事件并 reject 错误对象(错误码 ECONNABORTED)。重要概念:超时是客户端中止等待,请求可能已到达服务器并正在处理,因此超时后不应自动重试写操作(POST/DELETE),避免数据重复提交。
实战示例:超时与完整错误处理
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Axios 超时与错误处理</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 900px;
margin: 50px auto;
padding: 20px;
}
.test-section {
background: #f9f9f9;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
button {
padding: 10px 20px;
margin: 5px;
cursor: pointer;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
}
button:hover {
background: #0b7dda;
}
.result {
margin-top: 15px;
padding: 15px;
border-radius: 4px;
font-family: monospace;
white-space: pre-wrap;
}
.success {
background: #d4edda;
color: #155724;
border-left: 4px solid #28a745;
}
.error {
background: #f8d7da;
color: #721c24;
border-left: 4px solid #dc3545;
}
.timeout {
background: #fff3cd;
color: #856404;
border-left: 4px solid #ffc107;
}
</style>
</head>
<body>
<h1>Axios 超时与错误处理演示</h1>
<div class="test-section">
<h2>1. 超时测试</h2>
<p>使用一个会延迟响应的 API(delay.ms)测试超时机制</p>
<button onclick="testTimeout()">测试 2 秒超时</button>
<button onclick="testNoTimeout()">测试不超时</button>
<div id="timeout-result" class="result"></div>
</div>
<div class="test-section">
<h2>2. 错误类型测试</h2>
<button onclick="testNetworkError()">测试网络错误</button>
<button onclick="testHTTPError()">测试 HTTP 错误 (404)</button>
<button onclick="testServerError()">测试服务器错误 (500)</button>
<div id="error-result" class="result"></div>
</div>
<div class="test-section">
<h2>3. 全局错误处理示例</h2>
<button onclick="testGlobalHandler()">测试全局拦截器</button>
<div id="global-result" class="result"></div>
</div>
<script type="module">
const defaults = {
timeout: 0, // 默认不超时
responseType: 'json'
};
function dispatchRequest(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = config.responseType;
xhr.timeout = config.timeout; // 设置超时时间
xhr.open(config.method, config.url);
if (config.headers) {
for (let key in config.headers) {
xhr.setRequestHeader(key, config.headers[key]);
}
}
let body;
if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
if (typeof config.data === 'string') {
body = config.data;
} else if (Object.prototype.toString.call(config.data) === '[object Object]') {
body = JSON.stringify(config.data);
if (!config.headers?.['Content-Type']) {
xhr.setRequestHeader('Content-type', 'application/json');
}
} else {
body = config.data;
}
}
xhr.send(body);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
data: xhr.response,
status: xhr.status,
statusText: xhr.statusText,
config: config,
request: xhr
});
} else {
reject({
code: "ERR_BAD_REQUEST",
config,
message: `Request failed with status code ${xhr.status}`,
name: "AxiosError",
request: xhr,
response: {
data: xhr.response,
status: xhr.status,
statusText: xhr.statusText
}
});
}
};
xhr.onerror = () => {
reject({
code: "ERR_NETWORK",
config,
message: "Network Error",
name: "AxiosError",
request: xhr
});
};
// 超时事件
xhr.ontimeout = () => {
reject({
code: "ECONNABORTED",
config,
message: `timeout of ${config.timeout}ms exceeded`,
name: "AxiosError",
request: xhr
});
};
});
}
class Axios {
constructor(config) {
this.defaults = config;
}
request(urlOrConfig, config = {}) {
if (typeof urlOrConfig === 'string') {
config.url = urlOrConfig;
} else {
config = urlOrConfig;
}
config = Object.assign({}, this.defaults, config);
config.method = (config.method || 'get').toUpperCase();
return dispatchRequest(config);
}
get(url, config = {}) {
return this.request(url, { ...config, method: 'GET' });
}
post(url, data, config = {}) {
return this.request(url, { ...config, data, method: 'POST' });
}
}
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = function(config) {
return context.request(config);
};
instance.defaults = context.defaults;
['get', 'post', 'request'].forEach(method => {
instance[method] = context[method].bind(context);
});
return instance;
}
// 创建两个实例:一个带超时,一个不带
const axiosWithTimeout = createInstance({ ...defaults, timeout: 2000 });
const axiosNoTimeout = createInstance(defaults);
// 测试函数
window.testTimeout = () => {
const resultDiv = document.getElementById('timeout-result');
resultDiv.textContent = '发送请求...(2秒超时)';
resultDiv.className = 'result';
// 使用 delay.ms 模拟延迟响应
axiosWithTimeout.get('https://delay.ms/3/https://api.github.com/users/github')
.then(response => {
resultDiv.textContent = `成功:${response.data.name}`;
resultDiv.className = 'result success';
})
.catch(error => {
if (error.code === 'ECONNABORTED') {
resultDiv.textContent = `超时错误:${error.message}`;
resultDiv.className = 'result timeout';
} else {
resultDiv.textContent = `其他错误:${error.message}`;
resultDiv.className = 'result error';
}
});
};
window.testNoTimeout = () => {
const resultDiv = document.getElementById('timeout-result');
resultDiv.textContent = '发送请求...(不超时)';
resultDiv.className = 'result';
axiosNoTimeout.get('https://delay.ms/1/https://api.github.com/users/github')
.then(response => {
resultDiv.textContent = `成功:${response.data.name}`;
resultDiv.className = 'result success';
})
.catch(error => {
resultDiv.textContent = `错误:${error.message} (${error.code})`;
resultDiv.className = 'result error';
});
};
window.testNetworkError = () => {
const resultDiv = document.getElementById('error-result');
resultDiv.textContent = '发送网络错误请求...';
resultDiv.className = 'result';
axiosNoTimeout.get('https://this-domain-does-not-exist-12345.com/api')
.then(response => {
resultDiv.textContent = `成功:${JSON.stringify(response.data)}`;
resultDiv.className = 'result success';
})
.catch(error => {
let errorMsg = `错误码: ${error.code}\n`;
errorMsg += `错误消息: ${error.message}\n`;
errorMsg += `错误名称: ${error.name}`;
resultDiv.textContent = errorMsg;
resultDiv.className = 'result error';
});
};
window.testHTTPError = () => {
const resultDiv = document.getElementById('error-result');
resultDiv.textContent = '发送 404 请求...';
resultDiv.className = 'result';
axiosNoTimeout.get('https://api.github.com/users/this-user-does-not-exist-12345')
.then(response => {
resultDiv.textContent = `成功:${JSON.stringify(response.data)}`;
resultDiv.className = 'result success';
})
.catch(error => {
let errorMsg = `错误码: ${error.code}\n`;
errorMsg += `错误消息: ${error.message}\n`;
errorMsg += `状态码: ${error.response?.status}\n`;
errorMsg += `错误名称: ${error.name}`;
resultDiv.textContent = errorMsg;
resultDiv.className = 'result error';
});
};
window.testServerError = () => {
const resultDiv = document.getElementById('error-result');
resultDiv.textContent = '发送 500 错误请求...';
resultDiv.className = 'result';
// 使用 httpbin.org 测试服务器错误
axiosNoTimeout.get('https://httpbin.org/status/500')
.then(response => {
resultDiv.textContent = `成功:状态码 ${response.status}`;
resultDiv.className = 'result success';
})
.catch(error => {
let errorMsg = `错误码: ${error.code}\n`;
errorMsg += `错误消息: ${error.message}\n`;
errorMsg += `状态码: ${error.response?.status}\n`;
errorMsg += `错误名称: ${error.name}`;
resultDiv.textContent = errorMsg;
resultDiv.className = 'result error';
});
};
window.testGlobalHandler = () => {
const resultDiv = document.getElementById('global-result');
resultDiv.textContent = '测试全局拦截器错误处理...';
resultDiv.className = 'result';
// 模拟全局错误处理
const axios = createInstance(defaults);
// 模拟请求拦截器
const requestInterceptor = config => {
console.log('发送请求:', config.url);
return config;
};
// 模拟响应拦截器
const responseInterceptor = response => {
console.log('收到响应:', response.status);
return response;
};
// 模拟错误拦截器
const errorInterceptor = error => {
if (error.code === 'ERR_NETWORK') {
return Promise.reject({
...error,
userMessage: '网络连接失败,请检查网络设置'
});
} else if (error.code === 'ERR_BAD_REQUEST') {
return Promise.reject({
...error,
userMessage: `请求失败,状态码:${error.response?.status}`
});
} else if (error.code === 'ECONNABORTED') {
return Promise.reject({
...error,
userMessage: '请求超时,请稍后重试'
});
}
return Promise.reject(error);
};
// 使用拦截器(简化实现)
const requestWithInterceptors = config => {
config = requestInterceptor(config);
return axios.request(config)
.then(responseInterceptor)
.catch(errorInterceptor);
};
requestWithInterceptors({ url: 'https://nonexistent-domain-12345.com' })
.then(response => {
resultDiv.textContent = `成功:${response.data}`;
resultDiv.className = 'result success';
})
.catch(error => {
resultDiv.textContent = `用户友好错误消息:${error.userMessage || error.message}`;
resultDiv.className = 'result error';
});
};
</script>
</body>
</html>
【代码注释】 超时设置通过 xhr.timeout 实现,单位是毫秒 。超时触发 xhr.ontimeout 事件,此时 xhr.status 为 0 且 xhr.readyState 为 4(请求完成但被中止)。错误码 ECONNABORTED 是 axios 的约定,表示连接被中止(包括超时和取消)。市面应用 :移动端网络不稳定,超时设置通常比桌面端短(3-5秒);长轮询场景禁用超时(timeout: 0)。
【实战要点】
- 经典应用场景 :
- 移动端 API :设置
timeout: 5000,5秒未响应则提示用户"网络较弱" - 文件上传 :大文件上传禁用超时或设置较长值(如
timeout: 60000) - 轮询请求 :短超时避免阻塞下一轮(如
timeout: 3000)
- 移动端 API :设置
- 常见坑 :
- 超时后请求已发送到服务器,可能服务端仍处理成功(超时 ≠ 取消)
timeout: 0表示永不超时,可能导致请求永久挂起- 超时错误
ECONNABORTED也用于请求取消,需用axios.isCancel()区分
- 性能与最佳实践 :
- 根据网络环境调整超时:移动端 3-5秒,桌面端 10-30秒
- 全局配置
axios.defaults.timeout = 10000,特殊请求单独覆盖 - 超时后提供重试按钮,不要自动重试(可能重复提交订单等)
【本章小结】
| 错误码 | 含义 | 触发场景 | 处理建议 |
|---|---|---|---|
ERR_NETWORK |
网络错误 | DNS 失败、跨域、连接被拒绝 | 检查网络、提示用户 |
ERR_BAD_REQUEST |
HTTP 错误 | 4xx(客户端错误)、5xx(服务器错误) | 显示具体错误信息 |
ECONNABORTED |
连接中止 | 超时、主动取消 | 提示超时或取消 |
记忆口诀 :"Network 网络错,BadRequest HTTP 错,Abort 超时取消要"
【面试考点】
Q4:axios 的超时机制是如何实现的?超时后请求会被取消吗?
A:通过 xhr.timeout = ms 设置超时时间,超时触发 xhr.ontimeout 事件。关键点 :超时只是客户端中止等待,请求可能已到达服务器并正在处理。如果服务端处理成功,会产生数据不一致(如重复扣款)。追问"如何避免"时答:超时后不要自动重试写操作(POST/DELETE),可提供重试按钮让用户手动触发;读操作(GET)可安全重试。
2.6 请求取消机制
Axios 的取消请求基于 Promise 状态控制实现。
XHR CancelToken Client XHR CancelToken Client #mermaid-svg-x8rvARNzdkp9uoXE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-x8rvARNzdkp9uoXE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-x8rvARNzdkp9uoXE .error-icon{fill:#552222;}#mermaid-svg-x8rvARNzdkp9uoXE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-x8rvARNzdkp9uoXE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-x8rvARNzdkp9uoXE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-x8rvARNzdkp9uoXE .marker.cross{stroke:#333333;}#mermaid-svg-x8rvARNzdkp9uoXE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-x8rvARNzdkp9uoXE p{margin:0;}#mermaid-svg-x8rvARNzdkp9uoXE .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-x8rvARNzdkp9uoXE text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-x8rvARNzdkp9uoXE .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-x8rvARNzdkp9uoXE .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-x8rvARNzdkp9uoXE .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-x8rvARNzdkp9uoXE .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-x8rvARNzdkp9uoXE #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-x8rvARNzdkp9uoXE .sequenceNumber{fill:white;}#mermaid-svg-x8rvARNzdkp9uoXE #sequencenumber{fill:#333;}#mermaid-svg-x8rvARNzdkp9uoXE #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-x8rvARNzdkp9uoXE .messageText{fill:#333;stroke:none;}#mermaid-svg-x8rvARNzdkp9uoXE .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-x8rvARNzdkp9uoXE .labelText,#mermaid-svg-x8rvARNzdkp9uoXE .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-x8rvARNzdkp9uoXE .loopText,#mermaid-svg-x8rvARNzdkp9uoXE .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-x8rvARNzdkp9uoXE .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-x8rvARNzdkp9uoXE .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-x8rvARNzdkp9uoXE .noteText,#mermaid-svg-x8rvARNzdkp9uoXE .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-x8rvARNzdkp9uoXE .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-x8rvARNzdkp9uoXE .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-x8rvARNzdkp9uoXE .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-x8rvARNzdkp9uoXE .actorPopupMenu{position:absolute;}#mermaid-svg-x8rvARNzdkp9uoXE .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-x8rvARNzdkp9uoXE .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-x8rvARNzdkp9uoXE .actor-man circle,#mermaid-svg-x8rvARNzdkp9uoXE line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-x8rvARNzdkp9uoXE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求进行中 创建 cancelToken 发送请求(config含cancelToken) then(取消函数) 调用 cancel() Promise resolve abort() 取消请求 onabort 触发
【代码注释】 此序列图展示取消请求的完整流程------CancelToken 内部维护一个 Promise,调用 cancel() 时 resolve 该 Promise,XHR 监听到 resolve 后调用 abort() 方法取消请求。核心机制是异步通知 :dispatchRequest 中用 config.cancelToken.then(() => xhr.abort()) 实现取消监听。取消后 xhr.onabort 触发,reject 错误对象(code: "ERR_CANCELED")。
实战示例:请求取消实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Axios 请求取消机制</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 900px;
margin: 50px auto;
padding: 20px;
}
.demo-section {
background: #f9f9f9;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
}
button {
padding: 10px 20px;
margin: 5px;
cursor: pointer;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
}
button:hover {
background: #0b7dda;
}
button.danger {
background: #f44336;
}
button.danger:hover {
background: #d32f2f;
}
button:disabled {
background: #cccccc;
cursor: not-allowed;
}
.result {
margin-top: 15px;
padding: 15px;
border-radius: 4px;
font-family: monospace;
white-space: pre-wrap;
}
.success {
background: #d4edda;
color: #155724;
border-left: 4px solid #28a745;
}
.error {
background: #f8d7da;
color: #721c24;
border-left: 4px solid #dc3545;
}
.info {
background: #d1ecf1;
color: #0c5460;
border-left: 4px solid #17a2b8;
}
</style>
</head>
<body>
<h1>Axios 请求取消机制演示</h1>
<div class="demo-section">
<h2>1. 基础取消示例</h2>
<p>发起一个延迟 3 秒的请求,在请求完成前点击"取消请求"</p>
<button id="send-btn" onclick="startRequest()">发送请求</button>
<button id="cancel-btn" class="danger" onclick="cancelRequest()" disabled>取消请求</button>
<div id="cancel-result" class="result"></div>
</div>
<div class="demo-section">
<h2>2. 搜索框自动取消示例</h2>
<p>输入搜索关键词时,自动取消上一个请求(防抖 + 取消)</p>
<input type="text" id="search-input" placeholder="输入搜索关键词..." style="padding: 10px; width: 300px;">
<div id="search-result" class="result"></div>
</div>
<script type="module">
const defaults = {
timeout: 0,
responseType: 'json'
};
// 模拟 CancelToken(基于 Promise)
class CancelToken {
constructor(executor) {
this.promise = new Promise((resolve) => {
this.resolve = resolve;
});
// 执行 executor,传入取消函数
executor(() => {
this.resolve();
});
}
}
function dispatchRequest(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = config.responseType;
xhr.timeout = config.timeout;
xhr.open(config.method, config.url);
if (config.headers) {
for (let key in config.headers) {
xhr.setRequestHeader(key, config.headers[key]);
}
}
let body;
if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
if (typeof config.data === 'string') {
body = config.data;
} else if (Object.prototype.toString.call(config.data) === '[object Object]') {
body = JSON.stringify(config.data);
if (!config.headers?.['Content-Type']) {
xhr.setRequestHeader('Content-type', 'application/json');
}
} else {
body = config.data;
}
}
// 设置取消请求的 Promise 状态改变
if (config.cancelToken) {
config.cancelToken.promise.then(() => {
xhr.abort(); // 取消请求
});
}
xhr.send(body);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
data: xhr.response,
status: xhr.status,
statusText: xhr.statusText,
config: config,
request: xhr
});
} else {
reject({
code: "ERR_BAD_REQUEST",
config,
message: `Request failed with status code ${xhr.status}`,
name: "AxiosError",
request: xhr
});
}
};
xhr.onerror = () => {
reject({
code: "ERR_NETWORK",
config,
message: "Network Error",
name: "AxiosError",
request: xhr
});
};
xhr.ontimeout = () => {
reject({
code: "ECONNABORTED",
config,
message: `timeout of ${config.timeout}ms exceeded`,
name: "AxiosError",
request: xhr
});
};
// 监听取消请求的事件
xhr.onabort = () => {
reject({
code: "ERR_CANCELED",
message: "canceled",
name: "CanceledError"
});
};
});
}
class Axios {
constructor(config) {
this.defaults = config;
}
request(urlOrConfig, config = {}) {
if (typeof urlOrConfig === 'string') {
config.url = urlOrConfig;
} else {
config = urlOrConfig;
}
config = Object.assign({}, this.defaults, config);
config.method = (config.method || 'get').toUpperCase();
return dispatchRequest(config);
}
get(url, config = {}) {
return this.request(url, { ...config, method: 'GET' });
}
post(url, data, config = {}) {
return this.request(url, { ...config, data, method: 'POST' });
}
}
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = function(config) {
return context.request(config);
};
instance.defaults = context.defaults;
instance.CancelToken = CancelToken;
instance.isCancel = (error) => error.code === "ERR_CANCELED";
['get', 'post', 'request'].forEach(method => {
instance[method] = context[method].bind(context);
});
return instance;
}
const axios = createInstance(defaults);
// === 示例 1:基础取消 ===
let currentCancel = null;
let isRequesting = false;
window.startRequest = () => {
if (isRequesting) {
alert('请求正在进行中...');
return;
}
const resultDiv = document.getElementById('cancel-result');
const sendBtn = document.getElementById('send-btn');
const cancelBtn = document.getElementById('cancel-btn');
// 创建 CancelToken
const source = axios.CancelToken((cancel) => {
currentCancel = cancel;
});
isRequesting = true;
sendBtn.disabled = true;
cancelBtn.disabled = false;
resultDiv.textContent = '发送请求...(3秒延迟)';
resultDiv.className = 'result info';
// 使用延迟 API 模拟长时间请求
axios.get('https://delay.ms/3000/https://api.github.com/users/github', {
cancelToken: source.promise
})
.then(response => {
resultDiv.textContent = `成功:${response.data.name}\n仓库:${response.data.blog}`;
resultDiv.className = 'result success';
})
.catch(error => {
if (axios.isCancel(error)) {
resultDiv.textContent = '请求已被取消';
resultDiv.className = 'result info';
} else {
resultDiv.textContent = `错误:${error.message}`;
resultDiv.className = 'result error';
}
})
.finally(() => {
isRequesting = false;
sendBtn.disabled = false;
cancelBtn.disabled = true;
currentCancel = null;
});
};
window.cancelRequest = () => {
if (currentCancel) {
currentCancel(); // 调用取消函数
currentCancel = null;
}
};
// === 示例 2:搜索框自动取消 ===
let searchCancelToken = null;
const searchInput = document.getElementById('search-input');
let searchTimeout = null;
searchInput.addEventListener('input', (e) => {
const query = e.target.value.trim();
const resultDiv = document.getElementById('search-result');
// 清除之前的防抖定时器
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// 取消上一个请求
if (searchCancelToken) {
searchCancelToken();
searchCancelToken = null;
}
if (!query) {
resultDiv.textContent = '请输入搜索关键词';
resultDiv.className = 'result info';
return;
}
resultDiv.textContent = '输入中...(防抖 500ms)';
resultDiv.className = 'result info';
// 防抖:500ms 后发送请求
searchTimeout = setTimeout(() => {
// 创建新的 CancelToken
const source = axios.CancelToken((cancel) => {
searchCancelToken = cancel;
});
resultDiv.textContent = '搜索中...';
resultDiv.className = 'result info';
// 搜索 GitHub 用户
axios.get(`https://api.github.com/search/users?q=${encodeURIComponent(query)}`, {
cancelToken: source.promise
})
.then(response => {
const count = response.data.total_count;
const users = response.data.items.slice(0, 5);
let html = `找到 ${count} 个用户,前 5 个:\n`;
users.forEach(user => {
html += `\n- ${user.login} (${user.html_url})`;
});
resultDiv.textContent = html;
resultDiv.className = 'result success';
})
.catch(error => {
if (axios.isCancel(error)) {
resultDiv.textContent = '已取消上一个搜索请求,正在发送新请求...';
resultDiv.className = 'result info';
} else {
resultDiv.textContent = `错误:${error.message}`;
resultDiv.className = 'result error';
}
})
.finally(() => {
searchCancelToken = null;
});
}, 500);
});
</script>
</body>
</html>
【代码注释】 取消机制的核心是 Promise 状态控制 ------CancelToken 内部维护一个 Promise,调用 cancel() 时 resolve 该 Promise,dispatchRequest 中监听到 resolve 后调用 xhr.abort()。xhr.abort() 会触发 xhr.onabort 事件,此时 reject 错误对象,code: "ERR_CANCELED" 用于标识取消类型。市面应用 :React 组件卸载时取消请求(useEffect 清理函数)、搜索框输入自动取消上一次请求、路由跳转时取消未完成请求。
【实战要点】
- 经典应用场景 :
- 组件卸载取消 :
useEffect返回清理函数() => cancel(),避免已卸载组件更新状态 - 搜索框防抖:每次输入取消上一个请求,只保留最新请求
- 页面跳转取消 :
router.beforeEach中取消所有进行中的请求
- 组件卸载取消 :
- 常见坑 :
- 忘记判断
axios.isCancel(error)导致把取消错误当成普通错误处理 CancelToken旧语法(new axios.CancelToken())在新版本已废弃,应用AbortController- 取消后
xhr.status为 0,与网络错误相同,需用code区分
- 忘记判断
- 性能与最佳实践 :
- React 中用自定义 Hook
useAxios统一管理取消逻辑 - 多并发请求时用数组存储取消函数:
const cancels = []; unmount() { cancels.forEach(c => c()); } - 新项目推荐
AbortController(标准 API),兼容性更好
- React 中用自定义 Hook
【本章小结】
| 方面 | 说明 |
|---|---|
| 取消原理 | Promise 状态控制 + xhr.abort() |
| 错误标识 | code: "ERR_CANCELED" |
| 判断方法 | axios.isCancel(error) |
| 新标准 | AbortController(推荐) |
记忆口诀 :"Cancel Promise 控状态,abort() 取消不彷徨;isCancel 判真伪,AbortController 新方向"
【面试考点】
Q5:axios 的请求取消原理是什么?
A:基于 Promise 状态控制 ------CancelToken 内部维护一个 Promise,调用 cancel() 时 resolve 该 Promise,dispatchRequest 监听到 resolve 后调用 xhr.abort() 取消请求。取消后 reject 的错误对象 code: "ERR_CANCELED",可用 axios.isCancel() 判断。追问"Promise 如何触发 XHR 取消"时答:通过 config.cancelToken.then(() => xhr.abort()) 实现,Promise resolve 时执行回调。
2.7 便捷方法实现
Axios 提供了 get、post、put、delete 等便捷方法。
#mermaid-svg-whSMI4JyBh2spO3n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-whSMI4JyBh2spO3n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-whSMI4JyBh2spO3n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-whSMI4JyBh2spO3n .error-icon{fill:#552222;}#mermaid-svg-whSMI4JyBh2spO3n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-whSMI4JyBh2spO3n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-whSMI4JyBh2spO3n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-whSMI4JyBh2spO3n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-whSMI4JyBh2spO3n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-whSMI4JyBh2spO3n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-whSMI4JyBh2spO3n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-whSMI4JyBh2spO3n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-whSMI4JyBh2spO3n .marker.cross{stroke:#333333;}#mermaid-svg-whSMI4JyBh2spO3n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-whSMI4JyBh2spO3n p{margin:0;}#mermaid-svg-whSMI4JyBh2spO3n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-whSMI4JyBh2spO3n .cluster-label text{fill:#333;}#mermaid-svg-whSMI4JyBh2spO3n .cluster-label span{color:#333;}#mermaid-svg-whSMI4JyBh2spO3n .cluster-label span p{background-color:transparent;}#mermaid-svg-whSMI4JyBh2spO3n .label text,#mermaid-svg-whSMI4JyBh2spO3n span{fill:#333;color:#333;}#mermaid-svg-whSMI4JyBh2spO3n .node rect,#mermaid-svg-whSMI4JyBh2spO3n .node circle,#mermaid-svg-whSMI4JyBh2spO3n .node ellipse,#mermaid-svg-whSMI4JyBh2spO3n .node polygon,#mermaid-svg-whSMI4JyBh2spO3n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-whSMI4JyBh2spO3n .rough-node .label text,#mermaid-svg-whSMI4JyBh2spO3n .node .label text,#mermaid-svg-whSMI4JyBh2spO3n .image-shape .label,#mermaid-svg-whSMI4JyBh2spO3n .icon-shape .label{text-anchor:middle;}#mermaid-svg-whSMI4JyBh2spO3n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-whSMI4JyBh2spO3n .rough-node .label,#mermaid-svg-whSMI4JyBh2spO3n .node .label,#mermaid-svg-whSMI4JyBh2spO3n .image-shape .label,#mermaid-svg-whSMI4JyBh2spO3n .icon-shape .label{text-align:center;}#mermaid-svg-whSMI4JyBh2spO3n .node.clickable{cursor:pointer;}#mermaid-svg-whSMI4JyBh2spO3n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-whSMI4JyBh2spO3n .arrowheadPath{fill:#333333;}#mermaid-svg-whSMI4JyBh2spO3n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-whSMI4JyBh2spO3n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-whSMI4JyBh2spO3n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-whSMI4JyBh2spO3n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-whSMI4JyBh2spO3n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-whSMI4JyBh2spO3n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-whSMI4JyBh2spO3n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-whSMI4JyBh2spO3n .cluster text{fill:#333;}#mermaid-svg-whSMI4JyBh2spO3n .cluster span{color:#333;}#mermaid-svg-whSMI4JyBh2spO3n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-whSMI4JyBh2spO3n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-whSMI4JyBh2spO3n rect.text{fill:none;stroke-width:0;}#mermaid-svg-whSMI4JyBh2spO3n .icon-shape,#mermaid-svg-whSMI4JyBh2spO3n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-whSMI4JyBh2spO3n .icon-shape p,#mermaid-svg-whSMI4JyBh2spO3n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-whSMI4JyBh2spO3n .icon-shape .label rect,#mermaid-svg-whSMI4JyBh2spO3n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-whSMI4JyBh2spO3n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-whSMI4JyBh2spO3n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-whSMI4JyBh2spO3n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} axios.get/url
调用 request
axios.post/url,data
调用 request
合并配置
method: GET
合并配置
method: POST, data
dispatchRequest
【代码注释】 此图展示便捷方法的实现原理------所有便捷方法最终都调用 request() 方法,只是自动填充了 HTTP 方法和参数。get(url, config) 转换为 request(url, {...config, method: 'GET'}),post(url, data, config) 转换为 request(url, {...config, data, method: 'POST'})。这种设计避免重复代码,所有配置合并、拦截器逻辑都在 request 中统一处理。
实战示例:便捷方法完整实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Axios 便捷方法实现</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 50px auto;
padding: 20px;
}
.method-section {
background: #f9f9f9;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
}
button {
padding: 10px 20px;
margin: 5px;
cursor: pointer;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
}
button:hover {
background: #0b7dda;
}
.get { background: #4CAF50; }
.post { background: #2196F3; }
.put { background: #FF9800; }
.delete { background: #f44336; }
.result {
margin-top: 15px;
padding: 15px;
border-radius: 4px;
background: white;
font-family: monospace;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
</style>
</head>
<body>
<h1>Axios 便捷方法演示</h1>
<div class="method-section">
<h2>GET 请求</h2>
<button class="get" onclick="testGet()">GET 请求示例</button>
<div id="get-result" class="result"></div>
</div>
<div class="method-section">
<h2>POST 请求</h2>
<button class="post" onclick="testPost()">POST 请求示例</button>
<div id="post-result" class="result"></div>
</div>
<div class="method-section">
<h2>PUT 请求</h2>
<button class="put" onclick="testPut()">PUT 请求示例</button>
<div id="put-result" class="result"></div>
</div>
<div class="method-section">
<h2>DELETE 请求</h2>
<button class="delete" onclick="testDelete()">DELETE 请求示例</button>
<div id="delete-result" class="result"></div>
</div>
<script type="module">
const defaults = {
timeout: 10000,
responseType: 'json'
};
function dispatchRequest(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = config.responseType;
xhr.timeout = config.timeout;
xhr.open(config.method, config.url);
if (config.headers) {
for (let key in config.headers) {
xhr.setRequestHeader(key, config.headers[key]);
}
}
let body;
if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
if (typeof config.data === 'string') {
body = config.data;
} else if (Object.prototype.toString.call(config.data) === '[object Object]') {
body = JSON.stringify(config.data);
if (!config.headers?.['Content-Type']) {
xhr.setRequestHeader('Content-type', 'application/json');
}
} else {
body = config.data;
}
}
xhr.send(body);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
data: xhr.response,
status: xhr.status,
statusText: xhr.statusText,
config: config,
request: xhr
});
} else {
reject({
code: "ERR_BAD_REQUEST",
config,
message: `Request failed with status code ${xhr.status}`,
name: "AxiosError",
request: xhr
});
}
};
xhr.onerror = () => {
reject({
code: "ERR_NETWORK",
config,
message: "Network Error",
name: "AxiosError",
request: xhr
});
};
xhr.ontimeout = () => {
reject({
code: "ECONNABORTED",
config,
message: `timeout of ${config.timeout}ms exceeded`,
name: "AxiosError",
request: xhr
});
};
});
}
class Axios {
constructor(config) {
this.defaults = config;
}
request(urlOrConfig, config = {}) {
if (typeof urlOrConfig === 'string') {
config.url = urlOrConfig;
} else {
config = urlOrConfig;
}
config = Object.assign({}, this.defaults, config);
config.method = (config.method || 'get').toUpperCase();
return dispatchRequest(config);
}
get(url, config = {}) {
return this.request(url, { ...config, method: 'GET' });
}
post(url, data, config = {}) {
return this.request(url, { ...config, data, method: 'POST' });
}
put(url, data, config = {}) {
return this.request(url, { ...config, data, method: 'PUT' });
}
delete(url, config = {}) {
return this.request(url, { ...config, method: 'DELETE' });
}
patch(url, data, config = {}) {
return this.request(url, { ...config, data, method: 'PATCH' });
}
}
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = function(config) {
return context.request(config);
};
instance.defaults = context.defaults;
// 将实例的所有属性复制到 instance 上
for (let key in context) {
instance[key] = context[key];
}
// 将原型的所有方法绑定到 context 后添加到 instance 上
Object.getOwnPropertyNames(Axios.prototype).forEach(key => {
instance[key] = Axios.prototype[key].bind(context);
});
return instance;
}
const axios = createInstance(defaults);
// === 测试函数 ===
window.testGet = () => {
const resultDiv = document.getElementById('get-result');
resultDiv.textContent = '发送 GET 请求...';
// 方式1:使用 get 方法
axios.get('https://api.github.com/users/github')
.then(response => {
resultDiv.textContent = `GET 请求成功!\n\n` +
`用户名: ${response.data.name}\n` +
`登录名: ${response.data.login}\n` +
`仓库数: ${response.data.public_repos}\n` +
`位置: ${response.data.location}\n` +
`博客: ${response.data.blog}`;
})
.catch(error => {
resultDiv.textContent = `错误: ${error.message}`;
});
};
window.testPost = () => {
const resultDiv = document.getElementById('post-result');
resultDiv.textContent = '发送 POST 请求...';
// 使用 JSONPlaceholder 测试 POST
axios.post('https://jsonplaceholder.typicode.com/posts', {
title: 'foo',
body: 'bar',
userId: 1
})
.then(response => {
resultDiv.textContent = `POST 请求成功!\n\n` +
`响应状态: ${response.status}\n` +
`创建的资源 ID: ${response.data.id}\n` +
`请求数据:\n${JSON.stringify({title: 'foo', body: 'bar', userId: 1}, null, 2)}\n\n` +
`响应数据:\n${JSON.stringify(response.data, null, 2)}`;
})
.catch(error => {
resultDiv.textContent = `错误: ${error.message}`;
});
};
window.testPut = () => {
const resultDiv = document.getElementById('put-result');
resultDiv.textContent = '发送 PUT 请求...';
// PUT 更新资源
axios.put('https://jsonplaceholder.typicode.com/posts/1', {
id: 1,
title: 'updated title',
body: 'updated body',
userId: 1
})
.then(response => {
resultDiv.textContent = `PUT 请求成功!\n\n` +
`响应状态: ${response.status}\n` +
`更新后的数据:\n${JSON.stringify(response.data, null, 2)}`;
})
.catch(error => {
resultDiv.textContent = `错误: ${error.message}`;
});
};
window.testDelete = () => {
const resultDiv = document.getElementById('delete-result');
resultDiv.textContent = '发送 DELETE 请求...';
// DELETE 删除资源
axios.delete('https://jsonplaceholder.typicode.com/posts/1')
.then(response => {
resultDiv.textContent = `DELETE 请求成功!\n\n` +
`响应状态: ${response.status}\n` +
`响应数据:\n${JSON.stringify(response.data, null, 2)}`;
})
.catch(error => {
resultDiv.textContent = `错误: ${error.message}`;
});
};
</script>
</body>
</html>
【代码注释】 便捷方法的核心是参数转发与配置合并 ------get(url, config) 转成 request(url, {...config, method: 'GET'}),post(url, data, config) 转成 request(url, {...config, data, method: 'POST'})。实现时要注意 data 参数只对 POST/PUT/PATCH 有效,GET/DELETE 请求的参数用 config.params。市面应用 :RESTful API 调用几乎全用便捷方法,如 axios.get('/users')、axios.post('/users', userData),比 axios({method: 'POST', url: '/users', data: userData}) 简洁。
【本章小结】
| 方法 | 参数 | 请求体 | 用途 |
|---|---|---|---|
get(url, config) |
params 在 config 中 |
无 | 获取资源 |
post(url, data, config) |
data 是请求体 | 有 | 创建资源 |
put(url, data, config) |
data 是请求体 | 有 | 全量更新 |
patch(url, data, config) |
data 是请求体 | 有 | 部分更新 |
delete(url, config) |
params 在 config 中 |
无 | 删除资源 |
记忆口诀 :"get/delete 读资源,post/put 写数据;patch 改部分,put 改全部"
三、拦截器机制深度剖析
3.1 拦截器核心概念
拦截器是 Axios 最强大的功能之一,采用责任链模式实现。
#mermaid-svg-Tqhvj2yUbZTNmM3v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Tqhvj2yUbZTNmM3v .error-icon{fill:#552222;}#mermaid-svg-Tqhvj2yUbZTNmM3v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Tqhvj2yUbZTNmM3v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .marker.cross{stroke:#333333;}#mermaid-svg-Tqhvj2yUbZTNmM3v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Tqhvj2yUbZTNmM3v p{margin:0;}#mermaid-svg-Tqhvj2yUbZTNmM3v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster-label text{fill:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster-label span{color:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster-label span p{background-color:transparent;}#mermaid-svg-Tqhvj2yUbZTNmM3v .label text,#mermaid-svg-Tqhvj2yUbZTNmM3v span{fill:#333;color:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .node rect,#mermaid-svg-Tqhvj2yUbZTNmM3v .node circle,#mermaid-svg-Tqhvj2yUbZTNmM3v .node ellipse,#mermaid-svg-Tqhvj2yUbZTNmM3v .node polygon,#mermaid-svg-Tqhvj2yUbZTNmM3v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .rough-node .label text,#mermaid-svg-Tqhvj2yUbZTNmM3v .node .label text,#mermaid-svg-Tqhvj2yUbZTNmM3v .image-shape .label,#mermaid-svg-Tqhvj2yUbZTNmM3v .icon-shape .label{text-anchor:middle;}#mermaid-svg-Tqhvj2yUbZTNmM3v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .rough-node .label,#mermaid-svg-Tqhvj2yUbZTNmM3v .node .label,#mermaid-svg-Tqhvj2yUbZTNmM3v .image-shape .label,#mermaid-svg-Tqhvj2yUbZTNmM3v .icon-shape .label{text-align:center;}#mermaid-svg-Tqhvj2yUbZTNmM3v .node.clickable{cursor:pointer;}#mermaid-svg-Tqhvj2yUbZTNmM3v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .arrowheadPath{fill:#333333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tqhvj2yUbZTNmM3v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Tqhvj2yUbZTNmM3v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tqhvj2yUbZTNmM3v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster text{fill:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v .cluster span{color:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Tqhvj2yUbZTNmM3v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Tqhvj2yUbZTNmM3v rect.text{fill:none;stroke-width:0;}#mermaid-svg-Tqhvj2yUbZTNmM3v .icon-shape,#mermaid-svg-Tqhvj2yUbZTNmM3v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tqhvj2yUbZTNmM3v .icon-shape p,#mermaid-svg-Tqhvj2yUbZTNmM3v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Tqhvj2yUbZTNmM3v .icon-shape .label rect,#mermaid-svg-Tqhvj2yUbZTNmM3v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tqhvj2yUbZTNmM3v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Tqhvj2yUbZTNmM3v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Tqhvj2yUbZTNmM3v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求配置
请求拦截器 2
后添加先执行
请求拦截器 1
先添加后执行
dispatchRequest
发送请求
响应拦截器 1
先添加先执行
响应拦截器 2
后添加后执行
响应数据
【代码注释】 此图展示拦截器的执行顺序------请求拦截器采用后进先出(LIFO) ,后添加的先执行;响应拦截器采用先进先出(FIFO) ,先添加的先执行。实现上通过数组操作完成:请求拦截器用 unshift 插到数组前面,响应拦截器用 push 插到后面。这种设计让后注册的请求拦截器能优先处理配置(如最后添加的身份认证),而先注册的响应拦截器能优先处理响应(如第一个添加的数据转换)。
名词解释:
- 请求拦截器:在请求发送前执行,可修改配置、添加 token
- 响应拦截器:在响应返回后执行,可统一处理错误、提取数据
- 责任链模式:将处理器串成链,依次处理请求/响应
3.2 拦截器实现原理
拦截器通过 Promise 链式调用实现异步串联。
ResInterceptor2 ResInterceptor1 Dispatch ReqInterceptor2 ReqInterceptor1 Client ResInterceptor2 ResInterceptor1 Dispatch ReqInterceptor2 ReqInterceptor1 Client #mermaid-svg-9gDOukLKWjE1yQX8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9gDOukLKWjE1yQX8 .error-icon{fill:#552222;}#mermaid-svg-9gDOukLKWjE1yQX8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9gDOukLKWjE1yQX8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9gDOukLKWjE1yQX8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9gDOukLKWjE1yQX8 .marker.cross{stroke:#333333;}#mermaid-svg-9gDOukLKWjE1yQX8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9gDOukLKWjE1yQX8 p{margin:0;}#mermaid-svg-9gDOukLKWjE1yQX8 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9gDOukLKWjE1yQX8 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9gDOukLKWjE1yQX8 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-9gDOukLKWjE1yQX8 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-9gDOukLKWjE1yQX8 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-9gDOukLKWjE1yQX8 .sequenceNumber{fill:white;}#mermaid-svg-9gDOukLKWjE1yQX8 #sequencenumber{fill:#333;}#mermaid-svg-9gDOukLKWjE1yQX8 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-9gDOukLKWjE1yQX8 .messageText{fill:#333;stroke:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9gDOukLKWjE1yQX8 .labelText,#mermaid-svg-9gDOukLKWjE1yQX8 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .loopText,#mermaid-svg-9gDOukLKWjE1yQX8 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9gDOukLKWjE1yQX8 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9gDOukLKWjE1yQX8 .noteText,#mermaid-svg-9gDOukLKWjE1yQX8 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-9gDOukLKWjE1yQX8 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9gDOukLKWjE1yQX8 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9gDOukLKWjE1yQX8 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9gDOukLKWjE1yQX8 .actorPopupMenu{position:absolute;}#mermaid-svg-9gDOukLKWjE1yQX8 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-9gDOukLKWjE1yQX8 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9gDOukLKWjE1yQX8 .actor-man circle,#mermaid-svg-9gDOukLKWjE1yQX8 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-9gDOukLKWjE1yQX8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} config config config response response response
【代码注释】 此序列图展示拦截器的数据流向------请求配置经过所有请求拦截器处理后到达 dispatchRequest,响应数据再经过所有响应拦截器处理后返回客户端。核心实现是 Promise 链 :chain 数组存储 [fulfilledHandler, rejectedHandler] 对,通过 while (chain.length) { promise = promise.then(...chain.shift()); } 串联成链。每个拦截器的返回值成为下一个拦截器的输入,形成完整的处理管道。
入门示例:拦截器基础实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Axios 拦截器实现原理</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 50px auto;
padding: 20px;
}
.demo-box {
background: #f9f9f9;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
}
button {
padding: 10px 20px;
margin: 5px;
cursor: pointer;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
}
.log-box {
background: #263238;
color: #aed581;
padding: 15px;
border-radius: 4px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
max-height: 500px;
overflow-y: auto;
}
.log-entry {
margin: 5px 0;
padding: 5px;
border-left: 3px solid transparent;
}
.request { border-left-color: #4CAF50; }
.response { border-left-color: #2196F3; }
.error { border-left-color: #f44336; }
</style>
</head>
<body>
<h1>Axios 拦截器实现原理演示</h1>
<div class="demo-box">
<h2>1. 拦截器执行顺序测试</h2>
<button onclick="testOrder()">测试拦截器执行顺序</button>
<div id="log-box" class="log-box"></div>
</div>
<div class="demo-box">
<h2>2. 请求拦截器:添加 Token</h2>
<button onclick="testToken()">测试自动添加 Token</button>
<div id="token-result" class="log-box"></div>
</div>
<div class="demo-box">
<h2>3. 响应拦截器:统一错误处理</h3>
<button onclick="testErrorHandler()">测试统一错误处理</button>
<div id="error-result" class="log-box"></div>
</div>
<script type="module">
// 拦截器管理器
class InterceptorManager {
constructor() {
this.handlers = [];
}
use(onResolved, onRejected) {
// 添加拦截器到数组
this.handlers.push({
onResolved,
onRejected
});
// 返回索引,用于 eject
return this.handlers.length - 1;
}
eject(id) {
// 移除指定拦截器
if (this.handlers[id]) {
this.handlers[id] = null;
}
}
forEach(fn) {
// 遍历所有拦截器
this.handlers.forEach(handler => {
if (handler !== null) {
fn(handler);
}
});
}
}
const defaults = {
timeout: 10000,
responseType: 'json'
};
function dispatchRequest(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = config.responseType;
xhr.timeout = config.timeout;
xhr.open(config.method, config.url);
if (config.headers) {
for (let key in config.headers) {
xhr.setRequestHeader(key, config.headers[key]);
}
}
let body;
if (['POST', 'PUT', 'PATCH'].includes(config.method)) {
if (typeof config.data === 'string') {
body = config.data;
} else if (Object.prototype.toString.call(config.data) === '[object Object]') {
body = JSON.stringify(config.data);
if (!config.headers?.['Content-Type']) {
xhr.setRequestHeader('Content-type', 'application/json');
}
} else {
body = config.data;
}
}
xhr.send(body);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
data: xhr.response,
status: xhr.status,
statusText: xhr.statusText,
config: config,
request: xhr
});
} else {
reject({
code: "ERR_BAD_REQUEST",
config,
message: `Request failed with status code ${xhr.status}`,
name: "AxiosError",
request: xhr,
response: {
data: xhr.response,
status: xhr.status,
statusText: xhr.statusText
}
});
}
};
xhr.onerror = () => {
reject({
code: "ERR_NETWORK",
config,
message: "Network Error",
name: "AxiosError",
request: xhr
});
};
xhr.ontimeout = () => {
reject({
code: "ECONNABORTED",
config,
message: `timeout of ${config.timeout}ms exceeded`,
name: "AxiosError",
request: xhr
});
};
});
}
class Axios {
constructor(config) {
this.defaults = config;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
request(urlOrConfig, config = {}) {
if (typeof urlOrConfig === 'string') {
config.url = urlOrConfig;
} else {
config = urlOrConfig;
}
config = Object.assign({}, this.defaults, config);
config.method = (config.method || 'get').toUpperCase();
// === 核心:构建拦截器链 ===
// 1. 创建执行链,初始包含 dispatchRequest
const chain = [[dispatchRequest.bind(this, config), undefined]];
// 2. 将所有请求拦截器添加到链的前面(后进先出)
this.interceptors.request.handlers.forEach(handler => {
if (handler !== null) {
chain.unshift([handler.onResolved, handler.onRejected]);
}
});
// 3. 将所有响应拦截器添加到链的后面(先进先出)
this.interceptors.response.handlers.forEach(handler => {
if (handler !== null) {
chain.push([handler.onResolved, handler.onRejected]);
}
});
// 4. 使用 Promise 链式调用执行
let promise = Promise.resolve(config);
while (chain.length) {
// shift() 取出第一个元素并从数组中删除
promise = promise.then(...chain.shift());
}
return promise;
}
get(url, config = {}) {
return this.request(url, { ...config, method: 'GET' });
}
post(url, data, config = {}) {
return this.request(url, { ...config, data, method: 'POST' });
}
}
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = function(config) {
return context.request(config);
};
instance.defaults = context.defaults;
instance.interceptors = context.interceptors;
for (let key in context) {
if (key !== 'interceptors') {
instance[key] = context[key];
}
}
Object.getOwnPropertyNames(Axios.prototype).forEach(key => {
if (key !== 'constructor') {
instance[key] = Axios.prototype[key].bind(context);
}
});
return instance;
}
const axios = createInstance(defaults);
// === 测试函数 ===
function addLog(elementId, message, type = 'request') {
const logBox = document.getElementById(elementId);
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
const timestamp = new Date().toLocaleTimeString();
entry.textContent = `[${timestamp}] ${message}`;
logBox.appendChild(entry);
logBox.scrollTop = logBox.scrollHeight;
}
window.testOrder = () => {
const logBox = document.getElementById('log-box');
logBox.innerHTML = '';
// 清除之前的拦截器
axios.interceptors.request.handlers = [];
axios.interceptors.response.handlers = [];
// 添加请求拦截器
axios.interceptors.request.use(
config => {
addLog('log-box', '请求拦截器 1:添加配置', 'request');
config.metadata = { startTime: Date.now() };
return config;
},
error => {
addLog('log-box', '请求拦截器 1:错误', 'error');
return Promise.reject(error);
}
);
axios.interceptors.request.use(
config => {
addLog('log-box', '请求拦截器 2:添加 headers', 'request');
config.headers['X-Custom-Header'] = 'test-value';
return config;
},
error => {
addLog('log-box', '请求拦截器 2:错误', 'error');
return Promise.reject(error);
}
);
// 添加响应拦截器
axios.interceptors.response.use(
response => {
addLog('log-box', '响应拦截器 1:处理响应数据', 'response');
return response;
},
error => {
addLog('log-box', '响应拦截器 1:处理错误', 'error');
return Promise.reject(error);
}
);
axios.interceptors.response.use(
response => {
const duration = Date.now() - response.config.metadata.startTime;
addLog('log-box', `响应拦截器 2:计算耗时 ${duration}ms`, 'response');
return response;
},
error => {
addLog('log-box', '响应拦截器 2:错误', 'error');
return Promise.reject(error);
}
);
// 发送请求
axios.get('https://api.github.com/users/github')
.then(response => {
addLog('log-box', `最终结果:${response.data.name}`, 'response');
})
.catch(error => {
addLog('log-box', `最终错误:${error.message}`, 'error');
});
};
window.testToken = () => {
const resultDiv = document.getElementById('token-result');
resultDiv.innerHTML = '';
// 清除之前的拦截器
axios.interceptors.request.handlers = [];
axios.interceptors.response.handlers = [];
// 模拟 token 存储
const token = 'mock-jwt-token-12345';
// 添加 token 拦截器
axios.interceptors.request.use(
config => {
addLog('token-result', `请求拦截器:为 ${config.url} 添加 token`, 'request');
// 为所有请求添加 Authorization 头
config.headers['Authorization'] = `Bearer ${token}`;
addLog('token-result', `Authorization: Bearer ${token}`, 'request');
return config;
},
error => {
return Promise.reject(error);
}
);
// 添加响应拦截器处理 token 过期
axios.interceptors.response.use(
response => {
return response;
},
error => {
if (error.response?.status === 401) {
addLog('token-result', '响应拦截器:Token 过期,需重新登录', 'error');
// 这里可以跳转到登录页
}
return Promise.reject(error);
}
);
// 发送请求
axios.get('https://api.github.com/users/github')
.then(response => {
addLog('token-result', `请求成功:${response.data.name}`, 'response');
addLog('token-result', `请求头包含:${Object.keys(response.config.headers).join(', ')}`, 'response');
})
.catch(error => {
addLog('token-result', `请求失败:${error.message}`, 'error');
});
};
window.testErrorHandler = () => {
const resultDiv = document.getElementById('error-result');
resultDiv.innerHTML = '';
// 清除之前的拦截器
axios.interceptors.request.handlers = [];
axios.interceptors.response.handlers = [];
// 统一错误处理拦截器
axios.interceptors.response.use(
response => {
// 成功响应直接返回
return response;
},
error => {
addLog('error-result', `统一错误处理:${error.code}`, 'error');
// 根据错误类型返回用户友好的错误信息
if (error.code === 'ERR_NETWORK') {
addLog('error-result', '错误:网络连接失败,请检查网络', 'error');
return Promise.reject({
...error,
userMessage: '网络连接失败,请检查您的网络设置'
});
} else if (error.code === 'ERR_BAD_REQUEST') {
const status = error.response?.status;
if (status === 404) {
addLog('error-result', '错误:请求的资源不存在', 'error');
return Promise.reject({
...error,
userMessage: '请求的资源不存在'
});
} else if (status >= 500) {
addLog('error-result', '错误:服务器错误', 'error');
return Promise.reject({
...error,
userMessage: '服务器出现错误,请稍后重试'
});
}
} else if (error.code === 'ECONNABORTED') {
addLog('error-result', '错误:请求超时', 'error');
return Promise.reject({
...error,
userMessage: '请求超时,请稍后重试'
});
}
return Promise.reject(error);
}
);
// 测试 404 错误
axios.get('https://api.github.com/users/nonexistent-user-12345')
.then(response => {
addLog('error-result', `成功:${response.data.name}`, 'response');
})
.catch(error => {
addLog('error-result', `用户友好的错误消息:${error.userMessage || error.message}`, 'error');
});
};
</script>
</body>
</html>
【代码注释】 拦截器链的核心是 Promise 链式调用 ------chain 数组存储 [fulfilledHandler, rejectedHandler] 对,请求拦截器用 unshift 插到数组前面(后进先出),响应拦截器用 push 插到后面(先进先出)。promise.then(...chain.shift()) 每次取出第一个处理器并串联成 Promise 链。市面应用:Vue Router 的导航守卫、Express 的中间件、Redux 的中间件都用类似机制,只是实现细节不同。
【实战要点】
- 经典应用场景 :
- 身份认证 :请求拦截器自动添加
Authorization: Bearer token - 错误处理:响应拦截器统一处理 401(跳转登录)、500(提示错误)
- 请求日志:记录请求耗时、参数,用于调试
- 数据转换 :响应拦截器自动提取
response.data
- 身份认证 :请求拦截器自动添加
- 常见坑 :
- 请求拦截器必须返回
config,否则请求丢失配置 - 响应拦截器必须返回
response或Promise.reject(error),否则后续拦截器接收不到数据 - 异步拦截器必须返回 Promise,否则链式调用中断
- 请求拦截器必须返回
- 性能与最佳实践 :
- 拦截器中避免耗时操作(如复杂计算、同步请求)
- 错误拦截器用
Promise.reject(error)保持错误链 - 多拦截器时注意顺序------身份认证 → 请求日志 → 参数处理
【本章小结】
| 方面 | 请求拦截器 | 响应拦截器 |
|---|---|---|
| 执行时机 | 请求发送前 | 响应返回后 |
| 执行顺序 | 后进先出(LIFO) | 先进先出(FIFO) |
| 接收参数 | config | response |
| 返回值 | config | response |
| 常见用途 | 添加 token、日志、参数处理 | 错误处理、数据转换、loading |
记忆口诀 :"请求拦截后先出,响应拦截先先出;request 加认证,response 处错误"
【面试考点】
Q6:axios 拦截器的执行顺序是什么?为什么?
A:请求拦截器是后进先出(LIFO) ,因为用 unshift 插到数组前面;响应拦截器是先进先出(FIFO) ,因为用 push 插到后面。这样设计让后注册的请求拦截器先执行(类似栈),先注册的响应拦截器先执行(类似队列)。追问"如何让请求拦截器按注册顺序执行"时答:改用 push 插入,但需调整 dispatchRequest 的插入位置。
Q7:拦截器中如何处理异步操作?
A:拦截器必须返回 Promise ------异步操作用 async/await 或返回 Promise.then()。如 async config => { const token = await getToken(); config.headers.Authorization = token; return config; }。如果拦截器不返回 Promise,后续拦截器会收到 undefined 而非配置对象。
3.3 拦截器实战应用
生产里拦截器常组合成「横切关注点」管道,典型分层如下:
#mermaid-svg-bEliazkaz91nYMOq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bEliazkaz91nYMOq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bEliazkaz91nYMOq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bEliazkaz91nYMOq .error-icon{fill:#552222;}#mermaid-svg-bEliazkaz91nYMOq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bEliazkaz91nYMOq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bEliazkaz91nYMOq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bEliazkaz91nYMOq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bEliazkaz91nYMOq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bEliazkaz91nYMOq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bEliazkaz91nYMOq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bEliazkaz91nYMOq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bEliazkaz91nYMOq .marker.cross{stroke:#333333;}#mermaid-svg-bEliazkaz91nYMOq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bEliazkaz91nYMOq p{margin:0;}#mermaid-svg-bEliazkaz91nYMOq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bEliazkaz91nYMOq .cluster-label text{fill:#333;}#mermaid-svg-bEliazkaz91nYMOq .cluster-label span{color:#333;}#mermaid-svg-bEliazkaz91nYMOq .cluster-label span p{background-color:transparent;}#mermaid-svg-bEliazkaz91nYMOq .label text,#mermaid-svg-bEliazkaz91nYMOq span{fill:#333;color:#333;}#mermaid-svg-bEliazkaz91nYMOq .node rect,#mermaid-svg-bEliazkaz91nYMOq .node circle,#mermaid-svg-bEliazkaz91nYMOq .node ellipse,#mermaid-svg-bEliazkaz91nYMOq .node polygon,#mermaid-svg-bEliazkaz91nYMOq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bEliazkaz91nYMOq .rough-node .label text,#mermaid-svg-bEliazkaz91nYMOq .node .label text,#mermaid-svg-bEliazkaz91nYMOq .image-shape .label,#mermaid-svg-bEliazkaz91nYMOq .icon-shape .label{text-anchor:middle;}#mermaid-svg-bEliazkaz91nYMOq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bEliazkaz91nYMOq .rough-node .label,#mermaid-svg-bEliazkaz91nYMOq .node .label,#mermaid-svg-bEliazkaz91nYMOq .image-shape .label,#mermaid-svg-bEliazkaz91nYMOq .icon-shape .label{text-align:center;}#mermaid-svg-bEliazkaz91nYMOq .node.clickable{cursor:pointer;}#mermaid-svg-bEliazkaz91nYMOq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bEliazkaz91nYMOq .arrowheadPath{fill:#333333;}#mermaid-svg-bEliazkaz91nYMOq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bEliazkaz91nYMOq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bEliazkaz91nYMOq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bEliazkaz91nYMOq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bEliazkaz91nYMOq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bEliazkaz91nYMOq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bEliazkaz91nYMOq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bEliazkaz91nYMOq .cluster text{fill:#333;}#mermaid-svg-bEliazkaz91nYMOq .cluster span{color:#333;}#mermaid-svg-bEliazkaz91nYMOq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bEliazkaz91nYMOq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bEliazkaz91nYMOq rect.text{fill:none;stroke-width:0;}#mermaid-svg-bEliazkaz91nYMOq .icon-shape,#mermaid-svg-bEliazkaz91nYMOq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bEliazkaz91nYMOq .icon-shape p,#mermaid-svg-bEliazkaz91nYMOq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bEliazkaz91nYMOq .icon-shape .label rect,#mermaid-svg-bEliazkaz91nYMOq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bEliazkaz91nYMOq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bEliazkaz91nYMOq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bEliazkaz91nYMOq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 响应拦截器 由内到外
解包 data
401 跳登录
Loading 计数 -1
请求拦截器 由外到内
traceId / 时间戳
Token / Cookie
Loading 计数 +1
真实请求
【代码注释】
流程图体现拦截器的注册顺序与执行顺序相反/相同 :请求侧后注册的(如 Token)先执行,保证离网络最近的一层最后改 config;响应侧先注册的先执行,先解包再统一 401。Loading 放在请求链末尾、响应链末尾配对,才能包住整次往返耗时。
实战示例:Loading + Token + 业务码解包(伪代码骨架)
javascript
let pendingCount = 0;
function showLoading() {
if (pendingCount++ === 0) document.getElementById('loading').style.display = 'block';
}
function hideLoading() {
if (--pendingCount <= 0) {
pendingCount = 0;
document.getElementById('loading').style.display = 'none';
}
}
axios.interceptors.request.use((config) => {
showLoading();
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
config.headers['X-Request-Id'] = crypto.randomUUID?.() || String(Date.now());
return config;
});
axios.interceptors.response.use(
(response) => {
hideLoading();
// 后端约定 { code: 0, data, msg }
const body = response.data;
if (body && typeof body.code === 'number' && body.code !== 0) {
return Promise.reject({ ...response, userMessage: body.msg || '业务失败' });
}
return body?.data !== undefined ? { ...response, data: body.data } : response;
},
(error) => {
hideLoading();
if (error.response?.status === 401) {
location.href = '/login';
}
return Promise.reject(error);
}
);
【代码注释】
上述骨架演示生产里三层拦截器的典型分工:pendingCount 控制全局 Loading,避免并行请求导致遮罩闪烁;请求阶段注入 Authorization 与 X-Request-Id 便于网关鉴权与链路追踪;响应阶段把 { code, data, msg } 解包为 data,非零 code 主动 reject 让业务统一走 catch。与仅处理 HTTP 状态码的库不同,国内很多 REST 接口 HTTP 200 仍表示业务失败,必须在拦截器层转换。
- Loading 用引用计数:多个并行请求时,只在第一个发出时显示、最后一个结束时隐藏,避免闪烁。
- 业务码与 HTTP 码分离 :HTTP 200 但
code !== 0时应在响应拦截器里reject,否则业务catch永远进不去。 - 401 统一跳转 :比在每个页面
catch里写if (status===401)可维护。 - 市面应用:Ant Design Pro、Vue Element Admin 的请求封装都是「请求拦截加 Token + 响应拦截解包 + 全局错误提示」同一套路。
【实战要点】
- 常见坑 :响应拦截器
return response.data后,外层then拿到的是数据而不是完整response,与未解包时代码不兼容,需团队统一约定。 - 性能 :拦截器里避免
await慢接口(如每次请求都拉新 token);Token 刷新应配合 401 重试队列,而不是阻塞所有请求。 - 取消与拦截器 :
eject(id)后该槽位变null,forEach跳过;热更新环境注意重复use导致拦截器叠加。
【面试考点】
Q8:如何实现「同一接口短时间只发最后一次」?
A:在请求拦截器里为 url+params 维护 AbortController(或 CancelToken),新请求 abort 旧请求;或 debounce 搜索框输入。axios v1.6+ 推荐 config.signal。
Q9:响应拦截器里 return Promise.reject 和直接 throw 区别?
A:在 async 拦截器里等价;在普通函数里必须 return Promise.reject(err) 才能把链置为 rejected,否则错误被吞掉。
四、总结
4.1 知识点回顾
#mermaid-svg-oFD3HRoQxcGpMF6U{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oFD3HRoQxcGpMF6U .error-icon{fill:#552222;}#mermaid-svg-oFD3HRoQxcGpMF6U .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oFD3HRoQxcGpMF6U .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oFD3HRoQxcGpMF6U .marker.cross{stroke:#333333;}#mermaid-svg-oFD3HRoQxcGpMF6U svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oFD3HRoQxcGpMF6U p{margin:0;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge{stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 text{fill:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth--1{stroke-width:17;}#mermaid-svg-oFD3HRoQxcGpMF6U .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-0{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-0{stroke-width:14;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-1{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-1{stroke-width:11;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 text{fill:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-2{stroke-width:8;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-3{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-3{stroke-width:5;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-4{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-4{stroke-width:2;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-5{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-5{stroke-width:-1;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-6{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-6{stroke-width:-4;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-7{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-7{stroke-width:-7;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-8{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-8{stroke-width:-10;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-9{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-9{stroke-width:-13;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 polygon,#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 text{fill:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .node-icon-10{font-size:40px;color:black;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .edge-depth-10{stroke-width:-16;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled circle,#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:lightgray;}#mermaid-svg-oFD3HRoQxcGpMF6U .disabled text{fill:#efefef;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-root rect,#mermaid-svg-oFD3HRoQxcGpMF6U .section-root path,#mermaid-svg-oFD3HRoQxcGpMF6U .section-root circle,#mermaid-svg-oFD3HRoQxcGpMF6U .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-oFD3HRoQxcGpMF6U .section-root text{fill:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-root span{color:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .section-2 span{color:#ffffff;}#mermaid-svg-oFD3HRoQxcGpMF6U .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-oFD3HRoQxcGpMF6U .edge{fill:none;}#mermaid-svg-oFD3HRoQxcGpMF6U .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-oFD3HRoQxcGpMF6U :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Axios
核心技术
架构设计
函数委托模式
适配器模式
Promise 化
拦截器链
核心功能
请求发送
响应处理
配置合并
超时控制
请求取消
高级特性
拦截器机制
责任链模式
Promise 链
错误处理
实战应用
身份认证
错误统一处理
请求日志
取消重复请求
【代码注释】 此思维导图总结本文的四大知识模块:架构设计 (函数委托、适配器、Promise 化、拦截器链)、核心功能 (请求发送、响应处理、配置合并、超时控制、请求取消)、高级特性 (拦截器机制、责任链模式、Promise 链、错误处理)、实战应用(身份认证、错误统一处理、请求日志、取消重复请求)。这四个层次层层递进,从底层设计到实际应用,构成了完整的 Axios 技术体系。
4.2 高频面试题速查
| 问题 | 核心答案 |
|---|---|
| axios 既是函数又是对象? | 函数委托模式 + bind + 属性复制 |
| 拦截器执行顺序? | 请求 LIFO(后进先出),响应 FIFO(先进先出) |
| 请求取消原理? | Promise 状态控制 + xhr.abort() |
| 超时机制? | xhr.timeout + xhr.ontimeout 事件 |
| 跨环境原理? | 适配器模式:浏览器用 XHR,Node 用 http |
| 4xx/5xx 如何捕获? | xhr.onload 中判断 status,非 200-299 reject |
| 如何统一处理错误? | 响应拦截器中根据 error.code 分发错误信息 |
| 为何不能简单 Object.assign 合并配置? | headers 需深度合并;url/method 以本次请求为准 |
| 404 为何走 onload 而非 onerror? | HTTP 语义错误在传输层已成功,需在 onload 里 reject |
| 生产拦截器如何分层? | Token/traceId → Loading 计数 → 业务码解包 → 401 跳转 |
4.3 学习建议
- 按路线实操 :按 §0.4 八步从「创建函数」做到「拦截器」,每步只改当前版
axios.js并用 HTML 验证 - 对照官方 :下载 axios 官方仓库 的
lib/,与手写版并排,从axios.js→Axios.js→xhr.js跟读 - 先会用再读源码 :能写
axios.create、拦截器、all后,再理解createInstance与mergeConfig的实现差异 - 面试准备 :熟记 §4.2 表 + 各章 【面试考点】(函数委托、拦截器顺序、取消原理、404 与 onerror)
- 生产演进 :新项目取消优先
AbortController.signal;CancelToken 为兼容旧版 axios
推荐阅读:
参考资料:
- Axios 官方仓库
- Axios 官方文档
- MDN XMLHttpRequest API
- Promise A+ 规范
- 责任链模式 - Refactoring Guru
- Axios 拦截器实现原理 - 腾讯云
- 面试官:你了解axios的原理吗?- Vue3面试题
- 最全、最详细Axios源码解读 - 掘金
说明 :文中示例可在本目录保存为 .html 后直接打开;对照学习时可自备 Axios 1.x 官方 lib/ 源码。
适用版本 :Axios 1.x 系列(与 axios 官方仓库 v1.x 分支一致)