Axios 核心技术深度解析:从源码分析到自定义实现

导读:本文基于 Axios 官方源码(v1.x系列),结合 ECMA-262 规范与 MDN 文档,系统讲解 Axios 的底层实现原理、拦截器机制、请求取消等核心技术。文章包含完整可运行的示例代码,涵盖从基础使用到源码级自定义实现的完整路径,适合希望深入理解 HTTP 库设计的前端工程师阅读。


目录


零、导读与学习价值

0.1 示例覆盖清单

本文完整覆盖以下知识点与可运行示例:

示例核心知识点本文章节
Axios 源码架构剖析目录结构、createInstance + utils.extend§1
核心源码精读Axios.js 的 request 链、dispatchRequestsettle§1
创建 axios 函数示例bind 委托、axios 本质不是 Axios 实例§2.1
请求发送与 Promise 封装示例dispatchRequest + XMLHttpRequest§2.2
请求配置项示例baseURLparamsheadersdata§2.3
响应结果处理示例响应对象结构、4xx/5xx 转 reject§2.4
超时设置示例xhr.timeoutECONNABORTED 错误码§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 建议练习路线(八步递进)

01 创建函数

02 XHR+Promise

03 配置合并

04 响应对象

05 超时

06 取消

07 get/post

08 拦截器

对照官方 lib 源码

【代码注释】

  • 每一步只扩展当前版本的 axios.js,用配套 HTML 在浏览器里验证;跳步会导致 bug 难定位。
  • 步骤 01 完成后,可再引入官方打包的 axios.js,对比 axiosaxios.defaults 的对象结构。
  • 无模块化构建时,用 IIFE 把迷你 axios 挂到 window.axios,在控制台直接调试。
  • 步骤 08 做完后,打开官方 lib/core/InterceptorManager.jsAxios.js#request,对照 Promise 链如何拼出来。
步骤本步新增能力与官方源码大致对应
01createInstance + bindlib/axios.js
02dispatchRequest、XHRadapters/xhr.js + dispatchRequest.js
03配置合并(入门用 Object.assignmergeConfig.js
04resolve/reject 响应结构settle.js + 响应体组装
05ontimeoutxhr 适配器内
06CancelTokencancel/CancelToken.js
07get/post 挂到 instanceAxios.js 原型方法
08拦截器链InterceptorManager.js + Axios.js request

【实战要点】

  • 入门版 createInstance 往往只做到 bind,尚未 extend 原型方法;§2.1 的完整版用 for...ingetOwnPropertyNames 补全,更接近官方 utils.extend
  • 读源码时用 IDE「跳转到定义」从 axios(config) 跟到 Axios.prototype.request → 拦截器链 → dispatchRequest

【面试考点】

  • 手写 axios 最小要实现哪几个函数/类?(createInstanceAxios#requestdispatchRequest、XHR 适配器、可选 InterceptorManager
  • 官方 createInstance 与「只做 bind」的入门版差在哪?(utils.extend 复制原型方法与实例属性、instance.create 递归合并默认配置)

一、Axios 源码架构分析

1.1 名词解释

  • axios 实例:对外暴露的「可调用函数」,内部 this 绑定到 Axios 上下文,拥有 defaultsinterceptorsget/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) 时,执行的是实例上的 requestthis 指向持有 defaultsinterceptorsAxios 对象。

1.2 概念与底层原理

官方一次请求的完整生命周期(见 axios 仓库 AGENTS.md)可概括为:

  1. 用户调用 axios(url)axios(config)
  2. requestmergeConfig(this.defaults, config),再校验 transitionalparamsSerializer 等;
  3. buildFullPath + buildURL 拼出最终 URL;
  4. 请求拦截器(LIFO)依次改写 config
  5. dispatchRequest 选适配器 → transformRequest → 发网络请求;
  6. 适配器返回后 settle 根据状态码 resolve/reject;
  7. 响应拦截器(FIFO)处理 responseerror
  8. 最外层 Promise 交给业务 then/catchawait

为何 axios 是函数而不是 class 实例?
JavaScript 允许函数带属性。createInstancebind(Axios.prototype.request, context) 得到可调用的 instance,再用 utils.extendget/postdefaultsinterceptors 挂到同一对象上,于是 API 同时支持 axios({ url })axios.get(url),且 axios.create() 可派生多实例(多 baseURL、多超时策略)。

mergeConfig 在 v1.x 的分层策略Configuration Merging):不同字段用不同合并规则——例如 urlmethod 以本次请求为准;baseURLtimeouttransformRequest 等默认「请求级覆盖实例级」;headers 常做深度合并。手写版用 Object.assign 是简化版,生产库必须处理 headers.common / headers.post 等嵌套,否则 POST 会丢全局 Content-Type

xhr/http 适配器 dispatchRequest 拦截器链 Axios 业务代码 xhr/http 适配器 dispatchRequest 拦截器链 Axios 业务代码 axios(config) mergeConfig 请求拦截器 LIFO config adapter(config) response 响应拦截器 FIFO Promise

【代码注释】
序列图把「配置合并 → 拦截器 → 适配器」串成一条线。追问「axios.getaxios(config) 走同一套吗」时答:便捷方法最终仍调用 request,只是预先填好 methoddata/params 的位置。

1.3 源码目录结构

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.jshttp.js

1.4 Axios 运行流程

调用 axios/config

合并配置项
defaults + config

请求拦截器
后进先出 LIFO

分发请求
dispatchRequest

适配器选择
xhr/http

发送网络请求

响应拦截器
先进先出 FIFO

返回响应数据

【代码注释】 此流程图展示了一个完整请求的生命周期:从调用 axios 函数开始,经过配置合并、请求拦截器(LIFO顺序)、请求分发、适配器选择(浏览器用XHR、Node用http模块)、网络请求、响应拦截器(FIFO顺序),最终返回响应数据。拦截器的执行顺序是关键——请求拦截器后添加的先执行,响应拦截器先添加的先执行。

流程说明

  1. 配置合并:将用户配置与默认配置合并
  2. 请求拦截器:按照后进先出(LIFO)顺序执行
  3. 请求分发:调用 dispatchRequest 选择适配器
  4. 网络请求:底层使用 XMLHttpRequest 或 http 模块
  5. 响应拦截器:按照先进先出(FIFO)顺序执行
  6. 数据返回:返回格式化后的响应数据

【实战要点】

  • 断点建议:Axios.jsrequestdispatchRequestxhr.jsonload
  • 请求拦截器里改 config.urlreturn config,否则后续拿到 undefined

【面试考点】

  • 画出从 axios(config) 到 XHR send 的调用栈(至少 4 层)。
  • 为何响应拦截器用 FIFO、请求拦截器用 LIFO?

1.5 官方入口与入门实现的差异

官方 createInstancelib/axios.js):

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
  • 导出时还挂载 CancelTokenisAxiosErrorallspread 等静态工具(见文件后半段)。
  • 无模块化时可将 createInstance 包在 IIFE 里挂到 window.axios,便于在 HTML 里直接调试。

1.6 建议精读的核心文件

文件作用
core/Axios.jsrequest:合并配置 → 走拦截器链 → dispatchRequest
core/dispatchRequest.js选适配器(xhr/http),发真实请求
core/mergeConfig.js深度合并 defaults 与本次 config
core/settle.js根据 status 决定 resolve 还是 reject
core/InterceptorManager.jsuse / eject,handlers 数组
helpers/buildURL.jsbaseURL + url + params 拼完整地址
helpers/combineURLs.jsisAbsoluteURL.js相对路径与绝对路径拼接规则
adapters/xhr.js浏览器端 XHR 实现(与 §2.2 手写高度相似)

【代码注释】

  • transformData.jsAxiosHeaders.js 负责请求/响应头与体的转换,进阶阅读即可。
  • dist/axios.min.js 为打包产物,调试原理请读 lib/ 源码。
  • 不必通读 helpers/ 下每一个流式/压缩相关文件,按表内顺序即可建立全局图。

【本章小结】

模块职责记忆点
lib/axios.jscreateInstance、导出 createaxios 是 bind 出来的函数
core/Axios.jsrequest、拦截器链不直接发请求
dispatchRequest + adapters选 xhr/http、转换数据跨环境关键
mergeConfig / settle合并配置、定 Promise 态4xx 在 onload 里 reject
InterceptorManageruse/eject请求 LIFO、响应 FIFO

记忆口诀“函数门面 request 心,合并配置再拦截;适配器里真发请求,settle 定成败。”

【面试考点】

Q1:从 axios(config) 到网络发出,至少经过哪四层?
A:createInstance 得到的函数 → Axios#requestmergeConfig)→ 请求拦截器链 → dispatchRequest 选适配器并 xhr.send。响应方向再走响应拦截器。答不出 mergeConfigdispatchRequest 说明只背了 API。

Q2:mergeConfig 为什么不能简单 Object.assign
A:headerscommongetpost 等桶,浅合并会覆盖整棵子树;url/method 又必须每次请求独立。官方对每类字段有 valueFromConfig2defaultToConfig2mergeDeepProperties 等策略。

Q3:浏览器端 axios 底层是 Fetch 还是 XHR?
A:默认 XHRadapters/xhr.js)。因此支持 onUploadProgressxhr.timeout;与 Fetch 的 AbortController 是另一套取消模型(v1.6+ 也支持 signal)。


二、自定义实现 Axios 核心

以下 §2.1~§2.7 与 §0.4 八步路线一一对应;每节附带可保存为 .html 的完整示例,在本目录下打开即可运行。

2.1 创建 axios 函数

核心原理:axios 是一个函数,但拥有 Axios 类实例的所有属性和方法。

委托给实例

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

入门示例:基础结构
<!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) 创建函数,再把实例属性(defaultsinterceptors)和原型方法(getpost)挂到函数上。技术上利用了 JS 中"函数也是对象"的特性。追问"为什么不直接用类"时答:函数更灵活,支持 axios(config)axios.get(url) 两种调用方式,API 更简洁。


2.2 实现请求发送与 Promise 封装

核心原理:基于 XMLHttpRequest 封装,返回 Promise 对象支持链式调用。

入门示例:基础请求发送
<!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-retryaxios-mock-adapter 等库中广泛使用,便于中间件插入。

【实战要点】

  • 经典应用场景:XHR 是浏览器最稳定的请求 API,支持进度监控、超时控制、取消请求,而 Fetch API 在旧浏览器不兼容
  • 常见坑:忘记 xhr.send() 会导致请求不发送;xhr.onload 只在网络层成功时触发,HTTP 4xx/5xx 也算成功,需手动判断 status
  • 性能与最佳实践:设置 responseType: 'json' 让浏览器自动解析 JSON,比 JSON.parse(xhr.responseText) 性能更好且更安全

【本章小结】

职责函数说明
配置合并requestObject.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:requestdispatchRequest.call(this, config)this 有什么用?
A:后续若要在适配器里读 this.defaults 或挂实例级状态,需要绑定实例;与官方 dispatchRequest 作为纯函数略有不同,手写版保留 call(this) 便于扩展。


2.3 请求配置项深度解析

Axios 支持丰富的配置项,以下是核心配置的完整实现。

实战示例:完整配置项支持
<!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
  • 常见坑
    1. params 值未编码导致 URL 解析错误(如 {q: 'a&b'} 变成 ?q=a&bb 被当成独立参数)
    2. POST 发送对象时忘设置 Content-Type: application/json,后端无法解析
    3. baseURL 末尾斜杠不一致(https://api.com + /v1/users vs https://api.com/ + v1/users)导致双斜杠或缺少斜杠
  • 性能与最佳实践
    1. 全局配置用 axios.defaults.baseURL,避免每次重复设置
    2. 敏感信息(如 token)不要放在 URL(params),应放 headers 避免日志泄露
    3. 大文件上传用 FormData,不要手动转 JSON

【本章小结】

配置项作用常见值
baseURL基础 URL,自动拼接相对路径https://api.example.com
url请求路径/usershttps://other.com/api
paramsURL 查询参数{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=2application/x-www-form-urlencoded,文件上传用 multipart/form-data。axios 能自动识别对象和字符串,但 FormData 需手动设置。


2.4 响应结果处理机制

Axios 的响应对象结构标准化,便于统一处理。

200-299

其他

XHR 响应

状态码判断

构建成功响应

构建错误响应

resolve response

reject error

【代码注释】 此流程图说明响应处理的核心逻辑——根据 HTTP 状态码决定 Promise 的走向。200-299 范围的状态码被视为成功,resolve 响应对象;其他状态码(4xx、5xx)被视为失败,reject 错误对象。关键点:xhr.onload 在所有 HTTP 请求完成后触发,包括 404、500,因此必须手动判断 status。这与 xhr.onerror 不同——后者只在网络层失败时触发。

实战示例:响应处理完整实现
<!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),便于错误处理中间件复用。

【实战要点】

  • 经典应用场景
    1. 统一错误处理:根据 error.code 区分错误类型,ERR_NETWORK 提示"网络连接失败",ERR_BAD_REQUEST 提示"服务器返回错误"
    2. 响应日志记录response.config.url + response.status 构成请求日志,用于调试
    3. 响应拦截器:统一提取 response.data,简化业务代码
  • 常见坑
    1. 误以为 xhr.onerror 会捕获 4xx/5xx,其实这些在 xhr.onload
    2. 忘记 responseType: 'json' 导致 response.data 是字符串而非对象
    3. 跨域请求时,xhr.status 为 0 且 xhr.onerror 触发(CORS 被阻止)
  • 性能与最佳实践
    1. response.data 直接获取解析后数据,避免重复 JSON.parse
    2. 响应拦截器中统一处理 error.response,避免每个请求单独写 catch

【本章小结】

事件何时触发业务含义
xhr.onloadHTTP 完成(含 404/500)status 区分成功/失败
xhr.onerror网络层失败、CORS 拦截status 常为 0
xhr.ontimeout超过 xhr.timeoutECONNABORTED

响应对象建议统一:{ 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 超时设置与错误处理

超时机制防止请求无限等待,提升用户体验。

< timeout

>= timeout

发起请求

设置 xhr.timeout

响应时间

触发 xhr.ontimeout

正常响应

reject ECONNABORTED

正常处理

【代码注释】 此图展示超时机制的决策流程——当响应时间小于设定值时走正常响应路径,否则触发 xhr.ontimeout 事件并 reject 错误对象(错误码 ECONNABORTED)。重要概念:超时是客户端中止等待,请求可能已到达服务器并正在处理,因此超时后不应自动重试写操作(POST/DELETE),避免数据重复提交。

实战示例:超时与完整错误处理
<!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)。

【实战要点】

  • 经典应用场景
    1. 移动端 API:设置 timeout: 5000,5秒未响应则提示用户"网络较弱"
    2. 文件上传:大文件上传禁用超时或设置较长值(如 timeout: 60000
    3. 轮询请求:短超时避免阻塞下一轮(如 timeout: 3000
  • 常见坑
    1. 超时后请求已发送到服务器,可能服务端仍处理成功(超时 ≠ 取消
    2. timeout: 0 表示永不超时,可能导致请求永久挂起
    3. 超时错误 ECONNABORTED 也用于请求取消,需用 axios.isCancel() 区分
  • 性能与最佳实践
    1. 根据网络环境调整超时:移动端 3-5秒,桌面端 10-30秒
    2. 全局配置 axios.defaults.timeout = 10000,特殊请求单独覆盖
    3. 超时后提供重试按钮,不要自动重试(可能重复提交订单等)

【本章小结】

错误码含义触发场景处理建议
ERR_NETWORK网络错误DNS 失败、跨域、连接被拒绝检查网络、提示用户
ERR_BAD_REQUESTHTTP 错误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 请求进行中 创建 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")。

实战示例:请求取消实现
<!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 清理函数)、搜索框输入自动取消上一次请求、路由跳转时取消未完成请求。

【实战要点】

  • 经典应用场景
    1. 组件卸载取消useEffect 返回清理函数 () => cancel(),避免已卸载组件更新状态
    2. 搜索框防抖:每次输入取消上一个请求,只保留最新请求
    3. 页面跳转取消router.beforeEach 中取消所有进行中的请求
  • 常见坑
    1. 忘记判断 axios.isCancel(error) 导致把取消错误当成普通错误处理
    2. CancelToken 旧语法(new axios.CancelToken())在新版本已废弃,应用 AbortController
    3. 取消后 xhr.status 为 0,与网络错误相同,需用 code 区分
  • 性能与最佳实践
    1. React 中用自定义 Hook useAxios 统一管理取消逻辑
    2. 多并发请求时用数组存储取消函数:const cancels = []; unmount() { cancels.forEach(c => c()); }
    3. 新项目推荐 AbortController(标准 API),兼容性更好

【本章小结】

方面说明
取消原理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 提供了 getpostputdelete 等便捷方法。

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 中统一处理。

实战示例:便捷方法完整实现
<!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 最强大的功能之一,采用责任链模式实现。

请求配置

请求拦截器 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 config config config response response response

【代码注释】 此序列图展示拦截器的数据流向——请求配置经过所有请求拦截器处理后到达 dispatchRequest,响应数据再经过所有响应拦截器处理后返回客户端。核心实现是 Promise 链chain 数组存储 [fulfilledHandler, rejectedHandler] 对,通过 while (chain.length) { promise = promise.then(...chain.shift()); } 串联成链。每个拦截器的返回值成为下一个拦截器的输入,形成完整的处理管道。

入门示例:拦截器基础实现
<!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 的中间件都用类似机制,只是实现细节不同。

【实战要点】

  • 经典应用场景
    1. 身份认证:请求拦截器自动添加 Authorization: Bearer token
    2. 错误处理:响应拦截器统一处理 401(跳转登录)、500(提示错误)
    3. 请求日志:记录请求耗时、参数,用于调试
    4. 数据转换:响应拦截器自动提取 response.data
  • 常见坑
    1. 请求拦截器必须返回 config,否则请求丢失配置
    2. 响应拦截器必须返回 responsePromise.reject(error),否则后续拦截器接收不到数据
    3. 异步拦截器必须返回 Promise,否则链式调用中断
  • 性能与最佳实践
    1. 拦截器中避免耗时操作(如复杂计算、同步请求)
    2. 错误拦截器用 Promise.reject(error) 保持错误链
    3. 多拦截器时注意顺序——身份认证 → 请求日志 → 参数处理

【本章小结】

方面请求拦截器响应拦截器
执行时机请求发送前响应返回后
执行顺序后进先出(LIFO)先进先出(FIFO)
接收参数configresponse
返回值configresponse
常见用途添加 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 拦截器实战应用

生产里拦截器常组合成「横切关注点」管道,典型分层如下:

响应拦截器 由内到外

解包 data

401 跳登录

Loading 计数 -1

请求拦截器 由外到内

traceId / 时间戳

Token / Cookie

Loading 计数 +1

真实请求

【代码注释】
流程图体现拦截器的注册顺序与执行顺序相反/相同:请求侧后注册的(如 Token)先执行,保证离网络最近的一层最后改 config;响应侧先注册的先执行,先解包再统一 401。Loading 放在请求链末尾、响应链末尾配对,才能包住整次往返耗时。

实战示例:Loading + Token + 业务码解包(伪代码骨架)

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,避免并行请求导致遮罩闪烁;请求阶段注入 AuthorizationX-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) 后该槽位变 nullforEach 跳过;热更新环境注意重复 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 知识点回顾

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 学习建议

  1. 按路线实操:按 §0.4 八步从「创建函数」做到「拦截器」,每步只改当前版 axios.js 并用 HTML 验证
  2. 对照官方:下载 axios 官方仓库lib/,与手写版并排,从 axios.jsAxios.jsxhr.js 跟读
  3. 先会用再读源码:能写 axios.create、拦截器、all 后,再理解 createInstancemergeConfig 的实现差异
  4. 面试准备:熟记 §4.2 表 + 各章 【面试考点】(函数委托、拦截器顺序、取消原理、404 与 onerror)
  5. 生产演进:新项目取消优先 AbortController.signal;CancelToken 为兼容旧版 axios

推荐阅读


参考资料

  1. Axios 官方仓库
  2. Axios 官方文档
  3. MDN XMLHttpRequest API
  4. Promise A+ 规范
  5. 责任链模式 - Refactoring Guru
  6. Axios 拦截器实现原理 - 腾讯云
  7. 面试官:你了解axios的原理吗?- Vue3面试题
  8. 最全、最详细Axios源码解读 - 掘金

说明:文中示例可在本目录保存为 .html 后直接打开;对照学习时可自备 Axios 1.x 官方 lib/ 源码。
适用版本:Axios 1.x 系列(与 axios 官方仓库 v1.x 分支一致)

内容概要:本文提出了一种基于粒子群优化算法(PSO)的多微电网协调运行与优化方法,旨在面向配电网环境实现高效、稳定、经济的能源调度。研究建立了包含分布式电源、储能系统、负荷及电网交互的多微电网数学模型,综合考虑运行成本最小化、可再生能源最大化利用及供电可靠性等多重目标,通过PSO算法进行多目标优化求解。文中配套提供了完整的Matlab代码实现,涵盖系统建模、目标函数设计、约束条件处理及优化求解全过程,便于读者复现、验证并拓展研究,适用于智能电网、分布式能源管理、微电网优化调度等领域的科研与工程实践。; 适合人群:具备电力系统分析、优化算法理论基础及Matlab编程能力的研究生、科研人员及从事新能源系统设计的工程技术人员。; 使用场景及目标:①深入理解多微电网系统在复杂配电网环境下的协调运行机制与能量管理策略;②掌握粒子群优化算法在电力系统多目标优化问题中的建模、实现与调参技巧;③实现面向实际应用场景的微电网经济调度、可再生能源消纳与供电可靠性提升的综合优化仿真验证。; 阅读建议:建议读者结合Matlab代码逐模块分析,重点理解系统模型构建、目标函数与约束条件的数学表达及PSO算法的具体实现流程,关注种群初始化、适应度计算、速度与位置更新等关键环节的编程细节。在掌握基础后,可尝试调整算法参数、更换其他智能优化算法(如遗传算法、灰狼优化器)进行对比实验,以深化对多微电网优化问题本质的认识。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值