我们在使用 Axios 的过程中,或多或少地要用到它的拦截器,例如要实现:
- 数据转换;
- 添加额外的数据;
- 输出或上报接口的请求时间、失败率等数据;
这些需求,使用拦截器就能非常容易地实现。那么 axios 的拦截器怎么使用,内部又是怎么实现的,这篇文章让我们一探究竟。
1. 拦截器的使用
在 axios 中,拦截器分为请求拦截器和响应拦截器。顾名思义,请求拦截器是在发出请求之前按照顺序执行的,响应拦截器是在收到响应之后(无论接口返回的是否成功)按照顺序执行的。
如果我们要统计每个接口的耗时,可以先在请求拦截器中添加一个时间戳,在响应拦截器中减去这个时间戳,就是这个请求的完整耗时:
// 获取当前时间
const getTime = () => {
if (typeof performance?.now === "function") {
return window.performance.now();
}
return Date.now();
};
// 接口上报
const reportCgi = (response, config) => {
// 响应失败时response为空
const { config: conf } = response || { config };
// 在响应拦截器中计算这个请求的耗时
console.log("response", conf.url, getTime() - conf.requestime);
};
axios.interceptors.request.use((config) => {
// 在请求拦截器中添加发起请求的时间
return { ...config, ...{ requesttime: getTime() } };
});
axios.interceptors.response.use(
(response) => {
reportCgi(response);
return response;
},
(error) => {
reportCgi(error.response, error.config);
return error;
}
);
同时,我们还能添加多个请求拦截器和响应拦截器:
axios.interceptors.request.use((config) => {
// 这里假设要先获取一个token
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ...config, ...{ token: Math.random() } });
}, 500);
});
});
axios.interceptors.request.use((config) => {
// 在请求拦截器中添加发起请求的时间
return { ...config, ...{ requesttime: getTime() } };
});
除此之外,axios 的拦截器还能做很多事情,如输出请求 log 和响应 log,方便在移动端进行调试;上报接口的统计数据等。
2. 拦截器是怎么实现的
拦截器在我们进行接口请求时,非常的方便。那么它内部是如何实现的呢?如何维护多个拦截器并按照顺序执行的呢?
2.1 拦截器的实现
这里的关键文件就是 InterceptorManager.js,这里的代码也比较少,我们一点一点地看它是怎么实现的:
var utils = require("./../utils");
function InterceptorManager() {
// 存储所有的拦截器,但请求拦截器和响应拦截器是分开的
this.handlers = [];
}
/**
* 添加拦截器
* fulfilled: 成功时执行的,在Promise.resolve中
* rejected: 失败时执行的,在Promise.reject中
*
* 返回当前添加的拦截器的ID,用于清除这个拦截器
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
// 把传入的在resolve和reject中要执行的方法添加到数组中
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected,
});
return this.handlers.length - 1;
};
/**
* 根据id请求拦截器
*
* id: 刚才use方法返回的那个数据
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
/**
* 迭代所有的拦截器
*
* 这里会跳过之前使用eject方法设置为null的拦截器
*
* @param {Function} fn 对所有拦截器都执行的一个方法
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
module.exports = InterceptorManager;
InterceptorManager 维护着 handlers 里面所有的拦截器,对外提供了 3 个方法:
- use: 添加拦截器,接受 2 个参数,一个是 Promise 成功时执行的,第 2 个时 Promise 失败时执行的;
- eject: 根据 id 清除这个拦截器;
- forEach: 循环所有的拦截器,并跳过所有为空的拦截器;
InterceptorManager 并不区分是请求拦截器还是响应拦截器,它只是维护他自己的一组拦截器罢了。若创建多个对象,即可分别维护各自的拦截器。
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(), // 请求拦截器
response: new InterceptorManager(), // 响应拦截器
};
}
将拦截器添加到 interceptors 的 request 和 response 两个属性中后,我们就可以像上面的那样调用 use 方法添加拦截器了。request 中维护的是请求拦截器,response 中维护的是响应拦截器。
2.2 将拦截器串联起来
创建一个chain
数组,把所有的拦截器都放进去。我们首先把真正请求接口的方法放进去:
// dispatchRequest 用于请求数据,这里我们先展示不管怎么实现的
// 这里把 dispatchRequest 也当做拦截器添加到队列中
// 每2个是一组,前面用于Promise.resolve, 后面的1个用户Promise.reject
var chain = [dispatchRequest, undefined];
然后把请求拦截器放到 chain 的前面,因为我们要在发起请求之前先执行请求拦截器:
// 拦截器调用forEach方法,把每一个请求拦截器都添加到chain的前面
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 每2个是一组,前面用于Promise.resolve, 后面的1个用户Promise.reject
// 由此也能看到,越是后添加的请求拦截器,越会是先执行
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
再把响应拦截器方法 chain 的后面,因为我们要在收到响应之后才执行响应拦截器:
// 拦截器调用forEach方法,把每一个响应拦截器都添加到chain的后面
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 响应拦截器按照顺序执行
chain.push(interceptor.fulfilled, interceptor.rejected);
});
现在已经把所有的请求拦截器、数据请求和响应拦截器都串联起来了:
然后依次执行就可以:
// 把config初始化为一个Promise对象,方便后面的使用
var promise = Promise.resolve(config);
while (chain.length) {
// 依次取出执行resolve和reject方法
// 将执行后的结果传给下一个拦截器
promise = promise.then(chain.shift(), chain.shift());
}
拦截器的功能就实现啦。
3. 总结
我们学习了 axios 中拦截器的思路,也可以在自己实现的一些功能组件中,使用这种机制,方便更多功能的扩展。