axios拦截器源码解读

1,723 阅读7分钟

axios拦截器源码解读

本文正在参与技术专题征文Node.js进阶之路,点击查看详情

前言

一个优秀的库,拥有一个完善的可扩展插件机制是必不可少的,因为库的开发者不可能把所有情况都想的方方面面。那么,为了实现用户的定制化需求,就需要在主程序运行的某个时候,抛出一系列hook,类似于webpack的插件、koaredux 的中间件等,这可以让后续的用户干预主程序中间的一些环节,从而完成自己的一些需求。下面,我们来看看axios是怎么实现这个拦截器机制的。 (下图来源ssh)

image-20220319203127100

使用方法

//添加请求拦截
axios.interceptors.request.use(function (config) {
    // 做些请求拦截
    return config;
  }, function (error) {
    // 请求未发送,发生错误
    return Promise.reject(error);
  });
​
// 添加响应拦截
axios.interceptors.response.use(function (response) {
    //响应状态码是2xx时,做的响应拦截
    return response;
  }, function (error) {
    // 响应状态码是2xx时,做的响应拦截
    return Promise.reject(error);
  });

源码

InterceptorManager.js

先把代码拉下来

git@github.com:axios/axios.git

文件目录结构

├── /lib/                          // 项目源码目
  └── /adapters/                     // 定义发送请求的适配器
      ├── http.js                       // node环境http对象
      ├── xhr.js                        // 浏览器环境XML对象
  └── /cancel/                       // 定义取消请求功能
  └── /helpers/                      // 一些辅助方法
  └── /core/                         // 一些核心功能
      ├──Axios.js                      // axios实例构造函数                 
      ├── createError.js               // 抛出错误
      ├── dispatchRequest.js           // 用来调用http请求适配器方法发送请求
      ├── InterceptorManager.js        // 拦截器管理器
      ├── mergeConfig.js               // 合并参数
      ├── settle.js                    // 根据http响应状态,改变Promise的状态
      ├── transformData.js             // 转数据格式
 └── axios.js                        // 入口,创建构造函数
 └── defaults.js                     // 默认配置
 └── utils.js                        // 公用工具函数

axios的构造函数中就在Axios.js里面

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

这里可以看到,axios的拦截器实例是由InterceptorManager这个构造函数创建的,这个构造函数在InterceptorManager.js 里面定义

'use strict';
​
var utils = require('./../utils');
​
//handlers数组存放拦截器任务对象
function InterceptorManager() {
  this.handlers = [];
}
​
/**
 * 拦截器任务,返回拦截器任务数组中的索引,以方便移除
 *
 * @param {Function} fulfilled The function to handle `then` for a `Promise`
 * @param {Function} rejected The function to handle `reject` for a `Promise`
 *
 * @return {Number} An ID used to remove interceptor later
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    synchronous: options ? options.synchronous : false,
    runWhen: options ? options.runWhen : null
  });
  return this.handlers.length - 1;
};
​
/**
 * 移除拦截器任务对象,根据拦截器任务的索引,将对象变为空
 *
 * @param {Number} id The ID that was returned by `use`
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};
​
/**
 * axios提供遍历拦截器的方法,主要目的是将handlers数组为null的项跳过执行
 *
 * @param {Function} fn The function to call for each interceptor
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};
​
​
module.exports = InterceptorManager;

use方法的作用就是将处理函数封装成一个处理对象放到拦截器数组(handlers)里。拦截器的use方法,分别传三个参数,其中fulfilledrejected参数传入promise.then()中,是一个函数方法。

这里的eject方法比较巧妙,由于splice 效率低,每次splice操作除了需要分配新的内存区域去存储数据外,还需要不断操作元素的下标,大量移动元素位置。对于移除数组项,axios的拦截器的处理是把拦截器任务对象置为 null 。而不是用splice移除。最后执行时为 null 的项不执行。

Axios.js

核心请求方法是 Axios.prototype.request ,拦截器任务数组 handlers 在这里做处理

Axios.prototype.request = function request(configOrUrl, config) {
 
   //......上面省略的是部分关于config的细节处理
​
  // 请求拦截器链数组
  var requestInterceptorChain = [];
  var synchronousRequestInterceptors = true;
    //遍历,去除不需要执行的请求拦截器任务
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
      if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }
    //配置请求是否是同步的
    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
​
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
//响应拦截器链数组
  var responseInterceptorChain = [];
    //遍历,去除不需要执行的响应拦截器任务
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });
​
  var promise;
    
    //如果配置设定不是立即执行
  if (!synchronousRequestInterceptors) {
      //为什么需要undefine呢,是因为数组里应该是两项为一组,
      //分别传入promise.then方法里的两个参数,正如下面的promise.then(chain.shift(), chain.shift());
    var chain = [dispatchRequest, undefined];
      //拼接请求拦截器链和请求方法和响应拦截器链
    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    chain = chain.concat(responseInterceptorChain);
​
    promise = Promise.resolve(config);
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }
​
    return promise;
  }
​
    //....... 下面的逻辑是立即执行逻辑
    
    
};


image

要注意! 请求拦截器用的是unshift插入数组的头部,而响应拦截器使用的是push 推入数组的尾部。所以在使用的时候,如果编写了这个请求拦截器任务1、2, 那么会先执行任务2,再执行任务1

为什么chain 的初始值是 [dispatchRequest, undefined],需要undefined呢,是因为这个调用链数组里应该是两项为一组, 分别传入promise.then方法里的两个参数,正如源码中的promise.then(chain.shift(), chain.shift());

axios将请求拦截器任务,请求方法和响应拦截器任务拼接在一个chain 队列里面,并循环遍历这个队列,每次从队列中取出两项分别作为promise.then方法的两个参数,形成了一条promise调用链。

    //把config传递给第一个
    promise = Promise.resolve(config);
    //循环遍历chain,形成一条promise调用链
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }

回看使用方法

我们再回看一下这个拦截器是怎么使用的

//添加请求拦截
axios.interceptors.request.use(function (config) {
    // 做些请求拦截
    return config;
  }, function (error) {
    // 请求未发送,发生错误
    return Promise.reject(error);
  });
​
// 添加响应拦截
axios.interceptors.response.use(function (response) {
    //响应状态码是2xx时,做的响应拦截
    return response;
  }, function (error) {
    // 响应状态码是2xx时,做的响应拦截
    return Promise.reject(error);
  });
​

为什么要这里的错误回调要返回一个rejectd状态的promise对象,而不是类似成功回调那样,直接返回一个值呢?

原因跟promise.catch属性有关

Promise.prototype.catch方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

then方法返回的是一个新的Promise实例。其返回值将作为新的promise实例的resolve函数的参数传入。

new Promise(((resolve, reject) => {
    reject('err')
})).then(undefined,(err)=>{
    console.log(err) //err
    return err
}).then(res=>{
    console.log(res)//err
})
//上面与下面等价
new Promise(((resolve, reject) => {
    reject('err')
})).catch((err)=>{
    console.log(err) //err
    return err
}).then(res=>{
    console.log(res)//err
})
​
//这个最后的catch是拿不到err的值的,因为最后的promise是fulfilled状态,而不是rejected状态
new Promise(((resolve, reject) => {
    reject('err')
})).catch((err)=>{
    console.log(err) //err
    return err
}).catch(res=>{
    console.log(res) // 没有值
})
​
​
​
​

要想让最外层能够捕获到异常,所以要返回一个rejectd状态的promise实例,所以return Promise.reject(error);

new Promise(((resolve, reject) => {
    reject('err')
})).catch((err)=>{
    console.log(err) //err
   return  Promise.reject(err);
}).catch(res=>{
    console.log(res) // err
})
​

同理,then方法返回的是一个新的Promise实例。其返回值将作为新的promise实例的resolve函数的参数传入。这样,下一层的then方法,fulfilled回调就能拿到这个返回值。如果不传,则为undefined。

function (config) {
    // 做些请求拦截
    return config;
  }

如果这里不返回,则dispatchRequest函数的参数将为undefined

将抛出异常

Cannot read properties of undefined (reading 'cancelToken')

我们再看看 dispatchRequest 函数

module.exports = function dispatchRequest(config) {
    //发现第一行就是一个抛出异常的方法
  throwIfCancellationRequested(config);
  // ......省略
};
​
​
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
​
  if (config.signal && config.signal.aborted) {
    throw new CanceledError();
  }
}
​

当请求拦截的fulfilled回调没有返回值时,此时的configundefined ,那么自然会报这个错误

Cannot read properties of undefined (reading 'cancelToken')

总结

axios 把用户注册的每个拦截器构造成一个 promise.then 所接受的参数,把相对应的拦截器数组进行调用链的头部和尾部组装,在运行时把所有的拦截器按照一个 promise 链的形式以此执行, 这个拦截器设计充分利用了 promise 的特性,十分巧妙。

koa的中间件调用,则是采用的是责任链的模式,将下一个中间件的调用方法作为参数传递,由使用者决定是否调用,中间件以嵌套函数的形式执行。

axios 库还有挺多的模块,剩下的以后再更了。(0.0)

参考

Koa的洋葱中间件,Redux的中间件,Axios的拦截器让你迷惑吗?实现一个精简版的就彻底搞懂了。

最全、最详细Axios源码解读---看这一篇就足够了

学习 axios 源码整体架构,打造属于自己的请求库