Axios的拦截器原理以及请求等待重发的实现
Vue项目越做越多,Axios一直作为请求发送的基础工程,这里就深究一下Axios的拦截器相关的一些逻辑和对应一个比较恶心的场景。
Axios GitHub
回顾下Promise
Promise的基础知识不做多介绍可以参考两个文章
Promise的状态
Promise.resolve(
new Promise((resolve,reject) => {
console.log('inner Promise');
resolve('123');
}).then(data=>{
console.log(1,typeof(data), data);
return data+'4';
).then(data=>{
return Promise.resolve('Randy'+data);
}).then(data=>{
console.log(2,typeof(data), data)
Promise.resolve
创建一个Promise对象,依赖于inner的Promise
的resolve结果
内部的new Promise().then()
创建了一个Promise
,new Promise()
resolve的结果是123
,then()
将结果改为1234
,打印"string" "123"
,然后返回'1234'
这个作为外层的resolve结果
外层中第一个then()
返回了一个Promise
返回"Randy1234"
作为resolve结果
外层中第二个then()
接收到前一个的返回值,然后打印"string" "Randy1234"
人话描述下这里用到的几个知识点
Promise.resolve(data)
等于new Promise(resolve=>{resolve(data)})
Promise A
可以使用另一个Promise B
的resolve值作为自己的resolve值进入A
的调用链
then()
可以对处理结果进行修改
Axios
接下来开始整体,说说Axios。Axios是基于Promise机制实现的异步的链式请求框架。体积小,源码易懂。非常适合做基础的请求库。
Axios结构
axios.js
:入口文件,将Axios
实例的request
函数绑定为入口函数,axios.create
其实返回的是一个function
,就是Axios
实例的Axios.prototype.request
lib/Axios.js
:真正的Axios
的实例,用于拼接拦截器的调用链,关键代码如下:
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
return promise;
lib/InterceptorManager.js
:拦截器管理,是一个对[]
的封装
lib/dispatchRequest.js
:发送请求的Promise
,完成发送请求的逻辑。注意看Axios.js
中的var chain = [dispatchRequest, undefined];
adapter/*
:适配器,这里的代码保证了Axios在ssr模式下和浏览器环境中区分环境实现请求返送的逻辑。里面存放了两个定义好的适配器,可以参照README.md
中的描述自定义适配器
拦截器模型
Request
拦截器中任意的请求(比如请求A)进入之后,如果主动检测到了access_token
的超时,那么停止当前请求A,开启refresh_token
的请求,当成功之后再执行A请求
当请求已发送,服务端识别到了token失效,Response
拦截器中的处理跟Request
拦截器要做的事一样
当有进行中的refresh_token
请求时,此请求需要等待这个进行中的refresh_token
的请求成功之后再进行发送
那我们一个一个来处理
当请求进入拦截器,主动发现需要refresh_token
时(比如access_token
有效期临近)需要将请求放置在refresh_token
成功之后
处理方式可以采用在then()
调用拦截器的方法时返回一个Promise
,然后在Promise
中等待refresh_token
的请求成功之后再进行当前进入的请求的发送
// axios 的 request拦截器
axios.interceptors.request.use(config => {
return new Promise(resolve => {
// 模拟等待refresh_token
setTimeout(function (config_param) {
resolve(config_param);
}, 2000, config)
上面的代码只是一个简单的示意,实际处理中要注意以下几点,
刷新token之后config_param
要处理新Token的拼装;
请求拦截器中要能识别出是否是refresh_token
的请求;
能识别出是否正在进行refresh_token
,并能正确处理其他进入的请求,这个后面会讲到
处理之后调用链会变成这样
case RespStatus.UNAUTHORIZED.code: {
let respConfig = response.config;
if (isRefreshTokenReq(respConfig.url)) {
//刷新Token的请求如果出现401直接退出登录
showLoginOut();
} else {
logDebug('请求的返回值出现401,由请求' + config.url + '的返回值触发,开始进行refresh_token!');
let auth = storage.state.user.auth;
try {
res = doRefreshToken(auth.refresh_token, auth.wmq_d_current_username, respConfig)
.then(config => {
return wmqhttp(attachAuthInfoToConfig(storage.state.user.auth, config));
}).then(value => {
return Promise.resolve(value);
} catch (e) {
console.log('无法等待刷新Token!', e);
showLoginOut();
break;
default:
logDebug('Axios response default data:', res);
break;
return res;
处理之后调用链会变成这样
对于在refresh_token
时其他请求的进入需要安排这个请求动作,让请求发生在refresh_token
之后进行
- 解决思路如下,在全局的状态中记录是否正在刷新请求,并且保存refresh_token
的Promise
。当遇到请求之后新创建一个Promise
交给拦截器,在新创建的Promise
中用then()
等待refresh_token。
new Promise(resolve => {
pendingPromise.then(() => {
logDebug('刷新Token成功,开始处理之前等待的请求', config.url);
resolve(attachAuthInfoToConfig(storage.state.user.auth, config));