記一次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錯誤 }); 複製程式碼