Vue項(xiàng)目越做越多鸭叙,Axios一直作為請求發(fā)送的基礎(chǔ)工程常熙,這里就深究一下Axios的攔截器相關(guān)的一些邏輯和對應(yīng)一個(gè)比較惡心的場景瞧捌。
Axios GitHub
回顧下Promise
-
Promise的基礎(chǔ)知識不做多介紹可以參考兩個(gè)文章
- 《ECMAScript 6 入門》:Promise 對象
- ES6 Promise的resolved深入理解 這個(gè)是我看到的對于Promise狀態(tài)解釋比較清晰的一個(gè)文章
-
Promise的狀態(tài)
- Promise狀態(tài)一旦改變就不能再變弥咪,一直保持此狀態(tài)
- Promise可以被其他Promise鎖定----這個(gè)很重要络它,跟后面的要說到的Axios的請求阻塞等待有關(guān)系
- 一個(gè)重要的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) });
-
輸出如下結(jié)果
inner Promise "string" "123" "string" "Randy1234"
- 簡單解釋下上面的結(jié)果
-
Promise.resolve
創(chuàng)建一個(gè)Promise對象咖城,依賴于inner的Promise
的resolve結(jié)果 - 內(nèi)部的
new Promise().then()
創(chuàng)建了一個(gè)Promise
茬腿,new Promise()
resolve的結(jié)果是123
,then()
將結(jié)果改為1234
宜雀,打印"string" "123"
切平,然后返回'1234'
這個(gè)作為外層的resolve結(jié)果 - 外層中第一個(gè)
then()
返回了一個(gè)Promise
返回"Randy1234"
作為resolve結(jié)果 - 外層中第二個(gè)
then()
接收到前一個(gè)的返回值,然后打印"string" "Randy1234"
-
- 人話描述下這里用到的幾個(gè)知識點(diǎn)
-
Promise.resolve(data)
等于new Promise(resolve=>{resolve(data)})
-
Promise A
可以使用另一個(gè)Promise B
的resolve值作為自己的resolve值進(jìn)入A
的調(diào)用鏈 -
then()
可以對處理結(jié)果進(jìn)行修改
-
Axios
接下來開始整體辐董,說說Axios悴品。Axios是基于Promise機(jī)制實(shí)現(xiàn)的異步的鏈?zhǔn)秸埱罂蚣堋sw積小简烘,源碼易懂苔严。非常適合做基礎(chǔ)的請求庫。
Axios結(jié)構(gòu)
-
代碼結(jié)構(gòu)
-
axios.js
:入口文件孤澎,將Axios
實(shí)例的request
函數(shù)綁定為入口函數(shù)届氢,axios.create
其實(shí)返回的是一個(gè)function
,就是Axios
實(shí)例的Axios.prototype.request
-
lib/Axios.js
:真正的Axios
的實(shí)例覆旭,用于拼接攔截器的調(diào)用鏈退子,關(guān)鍵代碼如下:// 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
:攔截器管理,是一個(gè)對[]
的封裝 -
lib/dispatchRequest.js
:發(fā)送請求的Promise
型将,完成發(fā)送請求的邏輯寂祥。注意看Axios.js
中的var chain = [dispatchRequest, undefined];
-
adapter/*
:適配器,這里的代碼保證了Axios在ssr模式下和瀏覽器環(huán)境中區(qū)分環(huán)境實(shí)現(xiàn)請求返送的邏輯七兜。里面存放了兩個(gè)定義好的適配器丸凭,可以參照README.md
中的描述自定義適配器
-
-
攔截器模型
Axios攔截器示意圖.png- request和response的攔截器都可以有多對,其中每一個(gè)點(diǎn)都會掛在一個(gè)
then()
的調(diào)用上,promise.then(chain.shift(), chain.shift());
- request和response的攔截器都可以有多對,其中每一個(gè)點(diǎn)都會掛在一個(gè)
使用場景:應(yīng)對OAuth中refresh_token
換access_token
時(shí)其他請求需等待的問題
-
根據(jù)場景來看贮乳,我們需要有一下幾個(gè)能力
-
Request
攔截器中任意的請求(比如請求A)進(jìn)入之后忧换,如果主動(dòng)檢測到了access_token
的超時(shí),那么停止當(dāng)前請求A向拆,開啟refresh_token
的請求亚茬,當(dāng)成功之后再執(zhí)行A請求 - 當(dāng)請求已發(fā)送,服務(wù)端識別到了token失效浓恳,
Response
攔截器中的處理跟Request
攔截器要做的事一樣 - 當(dāng)有進(jìn)行中的
refresh_token
請求時(shí)刹缝,此請求需要等待這個(gè)進(jìn)行中的refresh_token
的請求成功之后再進(jìn)行發(fā)送
-
-
那我們一個(gè)一個(gè)來處理
-
當(dāng)請求進(jìn)入攔截器,主動(dòng)發(fā)現(xiàn)需要
refresh_token
時(shí)(比如access_token
有效期臨近)需要將請求放置在refresh_token
成功之后- 處理方式可以采用在
then()
調(diào)用攔截器的方法時(shí)返回一個(gè)Promise
颈将,然后在Promise
中等待refresh_token
的請求成功之后再進(jìn)行當(dāng)前進(jìn)入的請求的發(fā)送
// axios 的 request攔截器 axios.interceptors.request.use(config => { return new Promise(resolve => { // 模擬等待refresh_token setTimeout(function (config_param) { resolve(config_param); }, 2000, config) }); });
- 上面的代碼只是一個(gè)簡單的示意梢夯,實(shí)際處理中要注意以下幾點(diǎn),
- 刷新token之后
config_param
要處理新Token的拼裝晴圾; - 請求攔截器中要能識別出是否是
refresh_token
的請求颂砸; - 能識別出是否正在進(jìn)行
refresh_token
,并能正確處理其他進(jìn)入的請求死姚,這個(gè)后面會講到
- 刷新token之后
-
處理之后調(diào)用鏈會變成這樣
請求攔截器中加入Promise
- 處理方式可以采用在
-
當(dāng)請求已發(fā)送人乓,服務(wù)端識別到了Token失效時(shí)(這個(gè)情況比較多,服務(wù)器時(shí)間與本地有間隙都毒;Token不支持多點(diǎn)登陸等等)色罚,需要先
refresh_token
,然后重發(fā)請求- 可以采用與
Request
攔截器相似的處理账劲,在攔截器中同樣開啟refresh_token
戳护,成功之后重新創(chuàng)建已經(jīng)失敗的請求,執(zhí)行完請求之后將重新創(chuàng)建的請求獲取到的返回值resolve給response的返回值
- 可以采用與
-
let res = response.data;
switch (res.code) {
case RespStatus.UNAUTHORIZED.code: {
let respConfig = response.config;
if (isRefreshTokenReq(respConfig.url)) {
//刷新Token的請求如果出現(xiàn)401直接退出登錄
showLoginOut();
} else {
logDebug('請求的返回值出現(xiàn)401瀑焦,由請求' + config.url + '的返回值觸發(fā)腌且,開始進(jìn)行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;
-
處理之后調(diào)用鏈會變成這樣
響應(yīng)攔截器中加入Promise和二次請求
- 對于在
refresh_token
時(shí)其他請求的進(jìn)入需要安排這個(gè)請求動(dòng)作切蟋,讓請求發(fā)生在refresh_token
之后進(jìn)行
- 解決思路如下,在全局的狀態(tài)中記錄是否正在刷新請求榆芦,并且保存refresh_token
的Promise
。當(dāng)遇到請求之后新創(chuàng)建一個(gè)Promise
交給攔截器喘鸟,在新創(chuàng)建的Promise
中用then()
等待refresh_token匆绣。
new Promise(resolve => {
pendingPromise.then(() => {
logDebug('刷新Token成功,開始處理之前等待的請求', config.url);
resolve(attachAuthInfoToConfig(storage.state.user.auth, config));
});
});