axios拦截器源码解读
本文正在参与技术专题征文Node.js进阶之路,点击查看详情
前言
一个优秀的库,拥有一个完善的可扩展插件机制是必不可少的,因为库的开发者不可能把所有情况都想的方方面面。那么,为了实现用户的定制化需求,就需要在主程序运行的某个时候,抛出一系列hook,类似于webpack
的插件、koa
、redux
的中间件等,这可以让后续的用户干预主程序中间的一些环节,从而完成自己的一些需求。下面,我们来看看axios
是怎么实现这个拦截器机制的。
(下图来源ssh)
使用方法
//添加请求拦截
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
方法,分别传三个参数,其中fulfilled
和rejected
参数传入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;
}
//....... 下面的逻辑是立即执行逻辑
};
复制代码
要注意! 请求拦截器用的是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回调没有返回值时,此时的config
为 undefined
,那么自然会报这个错误
Cannot read properties of undefined (reading 'cancelToken')
复制代码
总结
axios
把用户注册的每个拦截器构造成一个 promise.then 所接受的参数,把相对应的拦截器数组进行调用链的头部和尾部组装,在运行时把所有的拦截器按照一个 promise 链的形式以此执行, 这个拦截器设计充分利用了 promise
的特性,十分巧妙。
而koa
的中间件调用,则是采用的是责任链的模式,将下一个中间件的调用方法作为参数传递,由使用者决定是否调用,中间件以嵌套函数的形式执行。
axios
库还有挺多的模块,剩下的以后再更了。(0.0)