跟着ai学习Axios

3 阅读4分钟

第一阶段 背景

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 灵活性的最高体现。

遇到的问题

  1. 数据的“变形”过程 (Transformation) 痛点:为什么我传的对象变成了字符串?为什么大数 ID 变样了?

精髓:重点关注 transformRequest 和 transformResponse。

深度细节:parseReviver 是全书的“题眼”。它是唯一能深入 JSON.parse 内部进行微观干预的钩子。如果你发现数据在进入拦截器之前就已经坏了,那一定是这里的转换逻辑出了问题。

  1. 内存与资源的解耦 (Clean-up) 痛点:请求结束后,那些闭包和监听器去哪了?

精髓:关注 done() 函数和 request = null。

深度细节:Axios 在 xhr.js 中手动切断对原生 XHR 对象的引用。这种“手动 GC(垃圾回收)辅助”是工业级库防止内存泄漏的基石。

  1. 环境的自动化适配 (Environment) 痛点:为什么一份代码能在浏览器跑,也能在 Node.js 跑?

精髓:关注 getDefaultAdapter。它通过探测环境变量(如 process 或 XMLHttpRequest)来动态加载适配器,这是“同构 JS”最标准的写法。

使用的设计模式

策略模式 (Strategy Pattern) —— 解决“在哪发” 体现:Adapters 模块。

逻辑:Axios 不直接写死 xhr,而是定义了适配器接口。

为什么要用:当需要支持微信小程序、uniapp 或 Node.js 时,只需要增加一个新的“策略(Adapter)”,而不需要修改核心逻辑(dispatchRequest)。

  1. 职责链模式 (Chain of Responsibility) —— 解决“怎么管” 体现:Interceptors(拦截器)。

逻辑:请求 -> 拦截器 A -> 拦截器 B -> 核心请求 -> 响应拦截器。

为什么要用:解耦。日志记录、Token 注入、错误统一处理,每个功能都是链条上的一个独立环扣,互不干扰,且支持异步等待。

  1. 适配器模式 (Adapter Pattern) —— 解决“怎么看” 体现:transformResponse、parseHeaders。

逻辑:将浏览器返回的原始、杂乱的 Raw Headers 字符串和 ResponseText 转换为开发者喜欢的干净的 JS 对象。

为什么要用:抹平底层 API 的原始性与业务层的高级需求之间的鸿沟。

  1. 模板方法模式 (Template Method) —— 解决“稳不稳” 体现:Axios.prototype.request。

逻辑:它规定了:必须先跑拦截器 -> 再发请求 -> 再跑响应转换。这个算法的骨架是雷打不动的。

为什么要用:框架开发者通过模板方法控制了“主流程”的安全性,而把具体的细节(比如具体传什么参数、用哪个适配器)开放给用户去填充。