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
中的描述自定義適配器
-
-
攔截器模型
Axios攔截器示意圖.png- 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之后
-
處理之后調用鏈會變成這樣
請求攔截器中加入Promise
- 處理方式可以采用在
-
當請求已發送,服務端識別到了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;
-
處理之后調用鏈會變成這樣
響應攔截器中加入Promise和二次請求
- 對于在
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));
});
});