第一阶段 背景
Axios是 XMLHttpRequest 或 fetch 的封装,它是一个同构(Isomorphic) 的请求库
- 同构性:同一套 API 代码,既能跑在浏览器(基于 XHR),也能跑在 Node.js(基于 http 模块)。
- Promise 驱动:从诞生之初就深度集成 Promise,完美契合异步编程。
- 功能完备:内置了请求/响应拦截器、数据自动转换、请求取消、防御 XSRF 等高级功能。
第二阶段 整体结构
| 模块名称 | 职责描述 |
|---|---|
| Axios Instance 🛡️ | 对外暴露的接口(如 axios.get),负责管理配置和拦截器。 |
| Interceptors 🔗 | 拦截器管理器。在请求发出前或响应返回后,按顺序执行用户定义的逻辑。 |
| Dispatch Request 🚀 | 调度员。负责合并配置,并根据当前环境选择合适的"适配器"。 |
| Adapters 🔌 | 灵魂所在。磨平平台差异。浏览器用 XHR,Node.js 用 http 模块。 |
| Transformation 🧪 | 数据转换器。自动把对象转成 JSON 字符串,或把 JSON 响应转回对象。 |
第三阶段 具体细节
1. Axios Instance
class Axios {
constructor() {
this.defaults = { timeout: 1000 };
}
// 核心请求逻辑
request(config) {
console.log(`📡 发送请求到: ${config.url}`);
}
get(url) {
return this.request({ url, method: 'GET' });
}
post(url) {
return this.request({ url, method: 'POST' });
}
}
function createInstance() {
// 1. 创建真正的实例,用于维护上下文(如配置、拦截器)
const context = new Axios();
// 2. 创建一个函数,它执行的就是 Axios.prototype.request
// 注意:必须 bind(context),否则 request 里的 this 会丢失
const instance = Axios.prototype.request.bind(context);
// 3. 把 Axios 原型上的方法(get, post 等)搬运给 instance 函数
// 这样 instance 就具备了对象的特征
Object.keys(Axios.prototype).forEach(key => {
// 同样需要 bind,确保 get/post 内部执行 this.request 时,this 是 context
instance[key] = Axios.prototype[key].bind(context);
});
// 4. 把实例属性(如 defaults)也考过来
Object.assign(instance, context);
return instance;
}
// 最终我们得到的 axios
const axios = createInstance();
// --- 测试 ---
axios({ url: '/api/user' }); // ✅ 作为函数调用
axios.get('/api/user'); // ✅ 作为对象调用
2. Interceptors
// 模拟拦截器管理类
class InterceptorManager {
constructor() {
this.handlers = [];
}
use(fulfilled, rejected) {
this.handlers.push({ fulfilled, rejected });
}
}
class MiniAxios {
constructor() {
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
request(config) {
// 1. 创建初始链条:核心逻辑 + 错误占位
let chain = [this.dispatchRequest, undefined];
// 2. 请求拦截器:后进先出 (unshift)
this.interceptors.request.handlers.forEach(interceptor => {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 3. 响应拦截器:先进先出 (push)
this.interceptors.response.handlers.forEach(interceptor => {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 4. 将链条转换为 Promise 链并执行
let promise = Promise.resolve(config);
while (chain.length > 0) {
// 每次取出一对 (fulfilled, rejected)
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
dispatchRequest(config) {
console.log("🚀 正在发送网络请求...");
return { data: "Server Data", status: 200 };
}
}
// --- 测试 ---
const axios = new MiniAxios();
axios.interceptors.request.use(cfg => {
console.log("1. 请求拦截器执行");
return cfg;
});
axios.request({ url: '/test' }).then(res => console.log("结果:", res.data));
3. Adapters
// 1. 浏览器适配器
function xhrAdapter(config) {
return new Promise((resolve) => {
console.log("使用 XMLHTTPRequest 发送请求到:", config.url);
resolve({ data: "来自浏览器的响应", status: 200 });
});
}
// 2. Node.js 适配器
function httpAdapter(config) {
return new Promise((resolve) => {
console.log("使用 Node.js http 模块发送请求到:", config.url);
resolve({ data: "来自服务器的响应", status: 200 });
});
}
// 3. 核心派发逻辑
function dispatchRequest(config) {
// 根据环境选择适配器(源码中会通过检测 process 或 window 来判断)
const adapter = typeof window !== 'undefined' ? xhrAdapter : httpAdapter;
return adapter(config).then(response => {
// 这里可以对响应数据做最后的加工(Transform Data)
return response;
});
}
// --- 测试 ---
dispatchRequest({ url: '/api/data' }).then(res => console.log(res.data));
第四阶段 整体实战
// 模拟拦截器管理类
class InterceptorManager {
constructor() {
this.handlers = [];
}
use(fulfilled, rejected) {
this.handlers.push({ fulfilled, rejected });
}
}
class CancelToken {
constructor(executor) {
let resolvePromise;
this.promise = new Promise(resolve => {
resolvePromise = resolve;
});
executor(message => {
if(this.reason) return;
this.reason = message || 'Request canceled';
resolvePromise(this.reason);
});
}
}
// 1. 浏览器适配器
/**
* 基于 XMLHttpRequest 的 HTTP 请求适配器
*
* @param {Object} config - 请求配置对象
* @param {string} config.method - HTTP 请求方法(默认为 GET)
* @param {string} config.url - 请求地址
* @param {Object} [config.headers] - 请求头
* @param {Object} [config.data] - 请求数据
* @param {Object} [config.cancelToken] - 取消令牌,用于取消请求
* @returns {Promise} 返回一个 Promise,成功时解析响应数据,失败时拒绝
*/
function xhrAdapter(config) {
return new Promise((resolve, rejected) => {
const xhr = new XMLHttpRequest();
xhr.open((config.method || 'GET').toUpperCase(), config.url, true);
if(config.headers) {
Object.keys(config.headers).forEach(key => {
xhr.setRequestHeader(key, config.headers[key]);
});
}
if(config.cancelToken) {
config.cancelToken.promise.then(reason => {
xhr.abort();
rejected(new Error(reason));
});
}
xhr.onreadystatechange = () => {
if(xhr.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300) {
resolve({ data: JSON.parse(xhr.responseText), status: xhr.status });
} else {
rejected(new Error(`Request failed with status code ${xhr.status}`));
}
}
};
xhr.send(config.data || null);
});
}
// 2. Node.js 适配器
function httpAdapter(config) {
return new Promise((resolve) => {
console.log("使用 Node.js http 模块发送请求到:", config.url);
resolve({ data: "来自服务器的响应", status: 200 });
});
}
// 1. 基础类定义
class Axios {
constructor() {
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
// 核心调度:Promise 链条
async request(config) {
// 初始链条:[实际请求, 占位符]
let chain = [this.dispatchRequest.bind(this), undefined];
// 组装拦截器(请求拦截器在前,响应在后)
this.interceptors.request.handlers.forEach(inter => chain.unshift(inter.fulfilled, inter.rejected));
this.interceptors.response.handlers.forEach(inter => chain.push(inter.fulfilled, inter.rejected));
let promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
// 模拟 dispatchRequest
async dispatchRequest(config) {
// 自动转换逻辑:如果是对象,转为 JSON
if (typeof config.data === 'object') {
config.data = JSON.stringify(config.data);
config.headers = { ...config.headers, 'Content-Type': 'application/json' };
}
// 根据环境选择适配器(源码中会通过检测 process 或 window 来判断)
const adapter = typeof window !== 'undefined' ? xhrAdapter : httpAdapter;
return adapter(config).then(response => {
// 这里可以对响应数据做最后的加工(Transform Data)
return response;
});
}
}
// 2. 工厂函数:实现“既是函数又是对象”
function createInstance() {
const context = new Axios();
const instance = Axios.prototype.request.bind(context);
// 拷贝原型方法(如 get, post)
['get', 'post'].forEach(method => {
instance[method] = (url, config) => {
const mergedConfig = { ...config, url, method };
return instance(mergedConfig);
};
});
// 暴露拦截器接口
instance.interceptors = context.interceptors;
return instance;
}
const axios = createInstance();
axios.interceptors.request.use((config) => {
config.headers = config.headers || {};
config.headers.auth = 'pde'
return config;
})
axios.interceptors.response.use((response) => {
delete response.data.body;
return response;
})
export { CancelToken };
export default axios;
代码片段如下,测试是否可用
第五阶段 源码阅读
重点
Promise 链的构建:拦截器不是简单的数组遍历,而是通过 while(chain.length) 构造的一个不断兑现的 Promise 隧道。
配置的合并策略:mergeConfig。它不是简单的 Object.assign,而是针对不同的字段(如 url 覆盖,headers 合并)有不同的合并逻辑。
函数对象化:利用 bind 和 utils.extend 让一个函数既能执行又能存属性。这是 JS 灵活性的最高体现。
遇到的问题
- 数据的“变形”过程 (Transformation) 痛点:为什么我传的对象变成了字符串?为什么大数 ID 变样了?
精髓:重点关注 transformRequest 和 transformResponse。
深度细节:parseReviver 是全书的“题眼”。它是唯一能深入 JSON.parse 内部进行微观干预的钩子。如果你发现数据在进入拦截器之前就已经坏了,那一定是这里的转换逻辑出了问题。
- 内存与资源的解耦 (Clean-up) 痛点:请求结束后,那些闭包和监听器去哪了?
精髓:关注 done() 函数和 request = null。
深度细节:Axios 在 xhr.js 中手动切断对原生 XHR 对象的引用。这种“手动 GC(垃圾回收)辅助”是工业级库防止内存泄漏的基石。
- 环境的自动化适配 (Environment) 痛点:为什么一份代码能在浏览器跑,也能在 Node.js 跑?
精髓:关注 getDefaultAdapter。它通过探测环境变量(如 process 或 XMLHttpRequest)来动态加载适配器,这是“同构 JS”最标准的写法。
使用的设计模式
策略模式 (Strategy Pattern) —— 解决“在哪发” 体现:Adapters 模块。
逻辑:Axios 不直接写死 xhr,而是定义了适配器接口。
为什么要用:当需要支持微信小程序、uniapp 或 Node.js 时,只需要增加一个新的“策略(Adapter)”,而不需要修改核心逻辑(dispatchRequest)。
- 职责链模式 (Chain of Responsibility) —— 解决“怎么管” 体现:Interceptors(拦截器)。
逻辑:请求 -> 拦截器 A -> 拦截器 B -> 核心请求 -> 响应拦截器。
为什么要用:解耦。日志记录、Token 注入、错误统一处理,每个功能都是链条上的一个独立环扣,互不干扰,且支持异步等待。
- 适配器模式 (Adapter Pattern) —— 解决“怎么看” 体现:transformResponse、parseHeaders。
逻辑:将浏览器返回的原始、杂乱的 Raw Headers 字符串和 ResponseText 转换为开发者喜欢的干净的 JS 对象。
为什么要用:抹平底层 API 的原始性与业务层的高级需求之间的鸿沟。
- 模板方法模式 (Template Method) —— 解决“稳不稳” 体现:Axios.prototype.request。
逻辑:它规定了:必须先跑拦截器 -> 再发请求 -> 再跑响应转换。这个算法的骨架是雷打不动的。
为什么要用:框架开发者通过模板方法控制了“主流程”的安全性,而把具体的细节(比如具体传什么参数、用哪个适配器)开放给用户去填充。