axios库的拦截器源码解析

88 阅读7分钟

Ajax 类代码详解

下面对给定的 Ajax 类代码进行逐行分析,并结合示例说明各部分功能和作用。

导入依赖

  • import defaultConfig from './config.js';:引入默认配置对象 defaultConfig,通常包含基础 URL、超时时间等基础设置,例如 { baseURL: "https://api.example.com", timeout: 5000 }。后续实例化时会与用户传入的配置合并。
  • import { isString } from '../utils/type.js';:引入 isString 工具函数,用于判断传入值是否为字符串。该函数用于后续判断请求参数是否为字符串,比如 isString("hello") 返回 trueisString(123) 返回 false
  • import Interceptor from './interceptor.js';:引入 Interceptor 类,用来创建请求和响应的拦截器对象。拦截器允许在请求发出前或响应到达后执行自定义逻辑(如添加请求头、日志记录等)。例如,可以通过 ajax.interceptors.request.use(config => {/*修改config*/return config;}) 注册请求拦截器。
  • import doRequest from './doRequest.js';:引入 doRequest 函数,负责执行实际的 HTTP 请求(可能采用 fetchXMLHttpRequest)。doRequest 接受请求配置并返回一个 Promise,对应请求完成后的响应。
  • import CustomEventSource from './CustomEventSource.js';:引入 CustomEventSource 类,用于处理服务器推送事件(SSE)。在下面的 createEventSource 方法中,会用它来创建基于 fetch 的事件源,用于监听后端持续推送的数据。

Ajax 类与构造函数

class Ajax {
    constructor(instanceConfig = {}) {
        // 实例默认配置
        this.defaults = Object.assign({}, defaultConfig, instanceConfig);
        this.interceptors = {
            request: new Interceptor(),
            response: new Interceptor()
        };
    }
    // ...
}
  • constructor(instanceConfig = {}):构造函数接受一个可选的实例配置对象 instanceConfig(默认空对象)。

  • this.defaults = Object.assign({}, defaultConfig, instanceConfig);:使用 Object.assign() 将一个空对象 {} 依次合并 defaultConfiginstanceConfig。这会将默认配置和实例配置的属性复制到新对象中,并赋值给 this.defaults。例如,如果 defaultConfig{ baseURL: "https://api", timeout: 5000 }instanceConfig{ timeout: 10000 },合并后 this.defaults 变为 { baseURL: "https://api", timeout: 10000 }

  • this.interceptors = { request: new Interceptor(), response: new Interceptor() };:为当前 Ajax 实例创建两个拦截器对象,分别管理请求拦截器和响应拦截器。这样可以在请求发出前或响应返回后统一处理逻辑。拦截器通常有一个 use 方法用于注册回调,例如:

    ajax.interceptors.request.use(
      config => { config.headers['X-Test'] = 'value'; return config; },
      error => Promise.reject(error)
    );
    ajax.interceptors.response.use(
      response => { console.log('Response arrived'); return response; },
      error => Promise.reject(error)
    );
    

    上述示例中,请求拦截器将给每个请求添加 X-Test 头部,响应拦截器则在每次响应到达时打印日志。

通用请求方法 request

async request(requestConfig, otherConfig) {
    const config = {};
    // 参数规范化
    if (isString(requestConfig)) {
        Object.assign(config, this.defaults, {
            url: requestConfig
        }, otherConfig);
    } else {
        Object.assign(config, this.defaults, requestConfig);
    }
    if (!config.url.startsWith('http')) {
        config.url = config.baseURL + config.url;
    }
    config.method = config.method.toUpperCase();
    config.responseType = config.responseType.toLowerCase();
    // 请求拦截器队列处理
    const requestInterceptorChain = [];
    this.interceptors.request.forEach(function(interceptor) {
        requestInterceptorChain.unshift(interceptor.resolved, interceptor.rejected);
    });
    // 响应拦截器队列处理
    const responseInterceptorChain = [];
    this.interceptors.response.forEach(function(interceptor) {
        responseInterceptorChain.push(interceptor.resolved, interceptor.rejected);
    });
    // 队列处理
    const taskChain = [doRequest.bind(this), undefined];
    taskChain.unshift(...requestInterceptorChain);
    taskChain.push(...responseInterceptorChain);
    let i = 0;
    let len = taskChain.length;
    let promise = Promise.resolve(config);
    while (i < len) {
        promise = promise.then(taskChain[i++], taskChain[i++]);
    }
    return promise;
}
  • 方法声明为 async(异步),所以它会返回一个 Promise 对象。调用此方法时,最终会返回一个 Promise,可以使用 await.then() 来处理请求结果。

  • 参数规范化:先创建一个空的 config 对象,然后判断传入的 requestConfig 类型。

    • 如果 requestConfig 是字符串(通过 isString 判断),则视为 URL 字符串,执行:

      Object.assign(config, this.defaults, { url: requestConfig }, otherConfig);
      

      这句将默认配置、一个包含 url 属性的对象,以及可选的 otherConfig 合并到 config 中。例如,调用 ajax.request('/users', { method: 'GET' }) 时,会得到 { ..., url: '/users', method: 'GET' }

    • 否则,requestConfig 被视为配置对象,执行:

      Object.assign(config, this.defaults, requestConfig);
      

      这会合并默认配置和用户传入的配置(包括 urlmethoddata 等)。

    • 在任一情况下,合并后 config 包含默认设置和指定的请求选项。Object.assign 会覆盖同名属性。

  • 处理 URL:如果 config.url 不以 "http" 开头(判断使用 startsWith 方法),则将默认的 baseURL(来自 this.defaults.baseURL)拼接到 URL 前面,如:

    if (!config.url.startsWith('http')) {
        config.url = config.baseURL + config.url;
    }
    

    例如默认 baseURL"https://api.example.com",请求 url"/users",则最终 config.url 变为 "https://api.example.com/users"

  • 规范化大小写config.method = config.method.toUpperCase(); 将 HTTP 方法名转换为全大写(toUpperCase() 返回字符串的大写形式)。例如 "get" 变成 "GET"config.responseType = config.responseType.toLowerCase(); 则将响应类型转换为小写(toLowerCase() 返回小写字符串),确保一致性。

  • 拦截器队列:将所有注册的请求拦截器和响应拦截器组织成数组。

    • 请求拦截器队列:

      const requestInterceptorChain = [];
      this.interceptors.request.forEach(function(interceptor) {
          requestInterceptorChain.unshift(interceptor.resolved, interceptor.rejected);
      });
      

      这段代码遍历 request 拦截器对象中的每个拦截器,将它们的 resolved(成功回调)和 rejected(失败回调)函数,按照“后注册的拦截器先执行”的顺序(使用 unshift 逆序添加)推入 requestInterceptorChain 数组中。

    • 响应拦截器队列类似,只是将回调函数按注册顺序(push)加入 responseInterceptorChain

      const responseInterceptorChain = [];
      this.interceptors.response.forEach(function(interceptor) {
          responseInterceptorChain.push(interceptor.resolved, interceptor.rejected);
      });
      

    这样一来,requestInterceptorChainresponseInterceptorChain 分别存放了所有拦截器回调函数,供后续链式调用。

  • 执行链式调用

    const taskChain = [doRequest.bind(this), undefined];
    taskChain.unshift(...requestInterceptorChain);
    taskChain.push(...responseInterceptorChain);
    let promise = Promise.resolve(config);
    while (i < taskChain.length) {
        promise = promise.then(taskChain[i++], taskChain[i++]);
    }
    return promise;
    

    首先 taskChain 初始为 [doRequest.bind(this), undefined],其中 doRequest.bind(this) 是绑定当前 this 的请求发送函数,undefined 用于占位捕获错误。然后将请求拦截器插入头部、响应拦截器插入尾部,形成完整的调用链。Promise.resolve(config) 创建了一个已解决的 Promise,以 config 作为初始值。在循环中,依次从 taskChain 中取出回调函数对来调用 .then(resolved, rejected),从而构建出完整的 Promise 链:先执行所有请求拦截器,再执行 doRequest 发起实际请求,最后执行所有响应拦截器。整个过程返回一个最终的 Promise,对应最终的响应结果。例如:

    ajax.interceptors.request.use(
      config => { console.log('准备发送请求:', config.url); return config; }
    );
    ajax.interceptors.response.use(
      response => { console.log('收到响应'); return response; }
    );
    ajax.get('/users');
    

    上面示例中,在真正发起请求前会先打印出要请求的 URL(请求拦截器处理),请求完成后打印“收到响应”(响应拦截器处理),最后将 response 对象返回给调用方。

JSONP 方法

async jsonp(url, data, config) {
    return this.request({
        method: 'GET',
        url,
        data,
        ...config,
        responseType: 'jsonp'
    });
}
  • jsonp 方法是对 request 的封装,用于发送 JSONP 请求。它固定使用 GET 方法,将用户传入的 urldata(查询参数等)和额外配置合并,并强制将 responseType 设置为 'jsonp'

  • JSONP(JSON with Padding)是一种通过 <script> 标签跨域获取 JSON 数据的传统技术。例如:

    ajax.jsonp('https://api.example.com/data?callback=cb', { foo: 'bar' }).then(data => {
        console.log('JSONP 返回的数据:', data);
    });
    

    上例中,实际发送的是对 https://api.example.com/data?callback=cb&foo=bar<script> 请求,不受同源策略限制,服务器需要返回调用 cb(...) 的 JavaScript 片段。responseType: 'jsonp' 会告诉 doRequest 以 JSONP 的方式处理响应。

事件源方法 createEventSource

createEventSource(url, requestConfig) {
    const _ = this;
    const controller = new AbortController();
    const config = Object.assign({}, _.defaults, requestConfig, {
        url,
        method: 'GET',
        headers: {
            'Accept': 'text/event-stream'
        },
        responseType: 'stream',
        signal: controller.signal
    });
    return new CustomEventSource(function(){
        return _.request(config);
    },{
        ...config,
        controller
    });
}
  • 该方法用于创建一个基于 fetch 的自定义事件源(类似浏览器的 EventSource),以监听服务器发送的持续事件流(SSE)。

  • 首先,const controller = new AbortController(); 创建一个 AbortController 实例。这个控制器用于在需要时中断异步操作(如取消请求)。

  • 接着使用 Object.assign 合并默认配置和传入的配置,构造最终的请求配置 config。在其中强制设置:

    • urlmethod: 'GET'(SSE 通常使用 GET)
    • 请求头 Accept: 'text/event-stream',告知服务器使用事件流格式
    • responseType: 'stream' 表示期望返回可读流
    • signal: controller.signalAbortController 的信号对象加入配置,以便后续可以通过 controller.abort() 取消请求。
  • 最后,返回一个新的 CustomEventSource 实例。它接收一个函数(用于获取 Promise,如调用 _.request(config) 发起请求)和一个配置对象(包括 controller 本身)。使用方法示例:

    const source = ajax.createEventSource('/events', { baseURL: 'https://api.example.com' });
    source.addEventListener('message', event => {
      console.log('收到事件:', event.data);
    });
    // 需要停止时可以调用:
    source.controller.abort();
    

    在这个示例中,调用 ajax.createEventSource 后会发起请求并持续监听服务器推送的事件,message 事件触发时可以在回调中处理数据。通过 source.controller.abort() 可以随时取消连接。

快捷方法封装

最后一段代码定义了对常用 HTTP 方法的简易封装:

['get', 'post', 'push', 'patch'].forEach(function(method){
    Ajax.prototype[method] = function(url, data, config) {
        return this.request({
            method,
            url,
            data,
            ...config
        });
    };
});
['delete', 'head', 'options'].forEach(function(method){
    Ajax.prototype[method] = function(url, config) {
        return this.request({
            method,
            url,
            data: null,
            ...config
        });
    };
});
  • 第一组 ['get', 'post', 'push', 'patch']:为每个方法在原型上定义对应函数,使之简写调用 request。例如:

    // 等效于 ajax.request({method: 'get', url: '/users', data: {id: 1}})
    ajax.get('/users', {id: 1});  
    // 等效于 ajax.request({method: 'post', url: '/users', data: {name: 'Alice'}})
    ajax.post('/users', {name: 'Alice'});  
    

    这里注意代码中写的是 'push',一般应为 'put'(更新数据),但逻辑相同。调用时传递 urldata(请求体或参数)以及可选的 config 对象,最终传给 request 方法处理。

  • 第二组 ['delete', 'head', 'options']:这些方法不需要请求体(data),因此函数只接受 urlconfig。它们在调用 request 时会强制 data: null,例如:

    // 等效于 ajax.request({method: 'delete', url: '/users/123', data: null})
    ajax.delete('/users/123');  
    // 等效于 ajax.request({method: 'options', url: '/users', data: null})
    ajax.options('/users');  
    

    这样用户可以直接 ajax.delete(url)ajax.head(url) 等简洁地发起请求,无需自己写完整的配置。

以上就是代码中每个部分的详细说明。最后一句 export default Ajax;Ajax 类作为模块的默认导出,以供其他文件通过 import Ajax from './Ajax.js' 引用使用。

参考文献: 本解释参考了 JavaScript 官方文档和相关技术资料。