Vue项目越做越多,Axios一直作为请求发送的基础工程,这里就深究一下Axios的拦截器相关的一些逻辑和对应一个比较恶心的场景。
Axios GitHub
回顾下Promise
-
Promise的基础知识不做多介绍可以参考两个文章
- 《ECMAScript 6 入门》:Promise 对象
- ES6 Promise的resolved深入理解 这个是我看到的对于Promise状态解释比较清晰的一个文章
-
Promise的状态
- Promise状态一旦改变就不能再变,一直保持此状态
- Promise可以被其他Promise锁定----这个很重要,跟后面的要说到的Axios的请求阻塞等待有关系
- 一个重要的Demo
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) });
-
输出如下结果
inner Promise "string" "123" "string" "Randy1234"
- 简单解释下上面的结果
-
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和response的拦截器都可以有多对,其中每一个点都会挂在一个
then()
的调用上,promise.then(chain.shift(), chain.shift());
- request和response的拦截器都可以有多对,其中每一个点都会挂在一个
使用场景:应对OAuth中refresh_token
换access_token
时其他请求需等待的问题
-
根据场景来看,我们需要有一下几个能力
-
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
,并能正确处理其他进入的请求,这个后面会讲到
- 刷新token之后
-
处理之后调用链会变成这样
- 处理方式可以采用在
-
当请求已发送,服务端识别到了Token失效时(这个情况比较多,服务器时间与本地有间隙;Token不支持多点登陆等等),需要先
refresh_token
,然后重发请求- 可以采用与
Request
拦截器相似的处理,在拦截器中同样开启refresh_token
,成功之后重新创建已经失败的请求,执行完请求之后将重新创建的请求获取到的返回值resolve给response的返回值
- 可以采用与
-
let res = response.data;
switch (res.code) {
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));
});
});