一、axios介紹
現在社群中有數量龐大的ajax(http)庫,為何選擇使用axios呢?
首先,因為它提供的API是Promise式的,目前業務程式碼基本都已經使用async/await來包裹非同步api了。
那為何不使用基於fetch的類庫呢?
因為,選用axios更重要的原因是,需要用到請求的abort。
abort
大部分場景中如果後端處理開銷不大,前端使用類似Promise.race或標記位等方式都可以實現前端業務邏輯中的abort。但是如果該請求是一個非常重型的,對資料庫讀寫有壓力的請求時,一個實實在在的abort還是有必要的。
當然,可以在後端介面上,設計為建立任務、執行任務、取消任務這樣的模式。
由於目前fetch沒有abort方式(AbortController目前尚在實驗階段),所以只能使用XMLHttpRequest類來實現具備abort能力的ajax。
二、為何解讀?
axios提供了cancel:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
});
// cancel the request
cancel();
複製程式碼
實際業務程式碼示意:
axios({
method: 'get',
url: '***',
}).then(response => {
// 業務邏輯
}).catch(err => {
if (axios.isCancel(err)) {
// 取消請求
} else {
// 業務邏輯錯誤
}
})
複製程式碼
期望的結果是,當cancel後,會在業務程式碼的catch中捕獲一個Cancel型別的錯誤。但實際使用中,該cancelError並沒有觸發,而是進入了response相關的業務邏輯。
於是,開始了一波debug。一開始懷疑是axios的坑,但當我開啟github,看到該專案**4.8萬+**的star數時,我確信:
一定是業務程式碼用錯了!
三、程式碼
1. 檔案結構
沒有全部細看,把主流程的js看了一遍。
axios/lib
│
└───adpaters
│ │ ... ajax/http類的封裝
│
└───cancel
│ │ ... 取消請求的相關程式碼
│
└───core
│ │
│ └───Axios.js 核心類,其餘方法沒細看
│
└───helpers
│ │ ... 工具函式集,沒看
│
└───axios.js 入口檔案,例項化了核心類
│
└───defaults.js 預設配置
複製程式碼
2. 主流程
請求發起
|
▼
+----------+
| req中介軟體 | axios稱之為request interceptors
+----------+
|
▼
+----------+
| dispatch | 發起請求,內部包含了一些入參轉化邏輯,不展開
+----------+
|
▼
+----------+
| Adapter | 介面卡,根據環境決定使用http還是xhr模組
+----------+
|
▼
+----------+
| res中介軟體 | axios稱之為response interceptors
+----------+
|
▼
+----------+
|transform | 返回值進行一次轉換
+----------+
|
▼
請求結束
複製程式碼
3. 中介軟體
axios可以通過axios.interceptors來擴充套件request/response的中介軟體:
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});
複製程式碼
最後排查結果是某一箇中介軟體出了問題導致的bug,下文再詳細展開,先聚焦在中介軟體相關的原始碼上:
// core/Axios.js
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());
}
複製程式碼
核心程式碼不長,它的目的是,轉換出一個Promise陣列:
[
ReqInterceptor_1_success, ReqInterceptor_1_error,
ReqInterceptor_2_success, ReqInterceptor_2_error,
...,
dispatchRequest, undefined,
ResInterceptor_1_success, ResInterceptor_1_error,
...,
]
複製程式碼
再將該陣列轉換為鏈式的Promise:
return Promise.resolve(
config,
).then(
ReqInterceptor_1_success, ReqInterceptor_1_error,
).then(
ReqInterceptor_2_success, ReqInterceptor_2_error,
).then(
dispatchRequest, undefined,
).then(
ResInterceptor_1_success, ResInterceptor_1_error,
)
複製程式碼
4. 請求取消
先貼一下主要原始碼:
// cancel/CancelToken.js
function CancelToken(executor) {
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
複製程式碼
這是CancelToken類的建構函式,它的入參需要是一個函式,該函式的第一個入參會返回cancel(message) => void
函式,該函式的作用是給CancelToken例項新增一個CancelError型別的reason屬性。
axios有兩個時機來取消請求。
第一種,在dispatchRequest方法中,在發起請求之前,如果cancel函式執行,throwIfCancellationRequested
會直接把cancelToken.reason丟擲。
// core/dispatchRequest.js
function dispatchRequest(config) {
throwIfCancellationRequested(config);
// ...
}
複製程式碼
官網示例中的cancel示例就是這第一種取消方式。實際上,請求並沒有在呼叫諸如axios.get方法時立刻發出,而是在microtask中執行(Event Loop相關文件可查閱此處)。具體原始碼參看上文中介軟體部分,即使沒有任何request中介軟體,請求也是在Promise.resolve(config)
的後續中觸發。
第二種,在請求發出以後,如果cancel函式執行,在實際的xhr模組中會觸發abort。
// adapters/xhr.js
config.cancelToken.promise.then(function onCanceled(cancel) {
// 此處then會在CancelToken的resolvePromise執行後觸發
request.abort();
reject(cancel);
});
複製程式碼
四、問題排查
1. 大致思路
確認原始碼以後,CancelError理論上都會被正確throw,並沒有犯比較低階的return new Error('*')
問題。(可以想想為什麼~)
既然如此,Error被丟擲,那就一定是半路被捕獲了。
那最有可能的原因是中介軟體出了問題,把CancelError給吞了。
2. 真相
最後確認,的確是有一個responseInterceptor:
axiosInstance.interceptors.response.use((resp: AxiosResponse) => {
//
}, (error: AxiosError): void => {
onResponseError(error);
});
// 而onResponseError是一個空方法
function onResponseError() {};
這會導致整個Promise鏈路變為:
Promise.resolve().then(() => {
return dispatch();
})
// response中介軟體
.then(data => {
return transform(data);
}, err => {
catchError(err); // 1. 沒有繼續丟擲錯誤
}).then(data => {
// 2. 錯誤被中介軟體捕獲後,進入後續resolved邏輯
}).catch(err => {
// 3. 無法捕獲cancel錯誤
});
複製程式碼