Axios 功能擴(kuò)展之Axios-Retry 源碼閱讀筆記
前兩天分析了 Axios 的源碼設(shè)計(jì)🔗,其中的攔截器(interceptor)為擴(kuò)展 Axios 留下了入口,在工作中我們也時(shí)常會(huì)擴(kuò)展 Axios,例如:取消重復(fù)請(qǐng)求、權(quán)限驗(yàn)證、失敗重試等。
那么如何設(shè)計(jì)實(shí)現(xiàn)一個(gè)好的攔截器來擴(kuò)展 Axios?
通過對(duì) axios-retry 這一周下載量 100w+ 的三方庫(kù)來學(xué)習(xí)下其功能設(shè)計(jì),工具庫(kù)項(xiàng)目的發(fā)包策略,并借此拋磚引玉,以提升我們的編碼設(shè)計(jì)能力!
- Github: https://github.com/softonic/axios-retry
- NPM: https://www.npmjs.com/package/axios-retry
一、工具庫(kù)的 package.json 寫法
看一個(gè)模塊的源碼,首先先看 README.md 和 package.json 文件。
參考如上,未來我們也應(yīng)該在開發(fā)工具庫(kù)的時(shí)候需要關(guān)注以下字段:
- files:在發(fā)包的時(shí)候發(fā)布將 es、lib 兩文件夾,以及 index.js 和 index.d.ts 文件。
- typings:TypeScript 類型定義文件,用于在 TypeScript 編碼環(huán)境下智能類型提示,該字段亦可寫作 types。
- main:主要入口文件,表明在項(xiàng)目中引入當(dāng)前庫(kù)時(shí)候,默認(rèn)指向的文件是 index.js
- module:并非官方字段,打包工具約定的如果有該字段,則在例如 Rollup 和 Webpack 打包時(shí),處理指定導(dǎo)入我們庫(kù)的 ESM 版本的文件路徑。
- exports:提供了一種方法來為不同的環(huán)境和 JavaScript 風(fēng)格顯示聲明如何引入模塊,同時(shí)限制對(duì)其內(nèi)部部分的訪問,該字段提案來自:Bare Module Specifier Resolution in node.js[1]
通過依賴字段以及 scripts 字段:
開發(fā)依賴和使用依賴
可以得知,當(dāng)前項(xiàng)目直接使用 Babel 作為打包編譯工具,通過執(zhí)行 npm run release 發(fā)包,并結(jié)合 npm scripts 的 pre 和 post 執(zhí)行生命周期依次執(zhí)行完成如下任務(wù):
npm run release 執(zhí)行的任務(wù)流程(原文鏈接可查看大圖)
更多關(guān)于 package.json 字段的功能/作用描述,可參考 package.json - NPM[2]
二、源碼分析
根據(jù) package.json 文件中關(guān)于“發(fā)包”命令相關(guān)解讀之后,可以得知 ./es/ 文件夾下的 index.mjs 為功能實(shí)現(xiàn)文件。
2.1 為什么是 .mjs 文件名后綴
Node.js 原本的模塊系統(tǒng)是 CommonJs (使用 require 和 module.exports 語(yǔ)法)。
自 Node.js 創(chuàng)建后, ECMAScript 模塊系統(tǒng) (使用 import 和 export 語(yǔ)法) 已經(jīng)變成一種標(biāo)準(zhǔn),并且 Node.js 已經(jīng)加入并實(shí)現(xiàn)支持 ES 模塊系統(tǒng)。
Node.js 將 *.cjs 文件當(dāng)作 CommonJS 模塊, *.mjs 文件當(dāng)作 ECMAScript 模塊。它會(huì)將 .js 文件視為項(xiàng)目的默認(rèn)模塊系統(tǒng),除非 package.json 聲明 "type": "module",否則就是 CommonJS。
2.2 axios-retry 的用法
axios-retry 對(duì)外導(dǎo)出 axiosRetry() 方法:
注入攔截器
通過對(duì) axios 單例添加“攔截器”,來擴(kuò)展實(shí)現(xiàn)自動(dòng)重試網(wǎng)絡(luò)請(qǐng)求功能。
axios-retry 主要接受兩個(gè)參數(shù),第一個(gè)是 axios 實(shí)例,第二個(gè)是 axios-retry 的配置 defaultOptions:
- defaultOptions: {
- retries?: number; // 自動(dòng)重試次數(shù)
- shouldResetTimeout?: boolean; // 是否重置“超時(shí)時(shí)間”
- retryCondition?: Function; // 重試的條件,可傳入自定義判斷函數(shù)
- retryDelay?: Function; // 重試請(qǐng)求的間隔時(shí)間的函數(shù)
- }
功能配置看起來挺完善的,難怪那么受歡迎。
2.3 請(qǐng)求攔截器設(shè)計(jì)&實(shí)現(xiàn)
在請(qǐng)求攔截器中會(huì)做狀態(tài)初始化,更新請(qǐng)求次數(shù):
- axios.interceptors.request.use((config) => {
- const currentState = getCurrentState(config);
- // 設(shè)置上次請(qǐng)求的時(shí)間
- // 思考🤔:為什么不放到 getCurrentState() 函數(shù)內(nèi)一起設(shè)置?
- currentState.lastRequestTime = Date.now();
- return config;
- });
- /**
- * 初始化并返回給定“請(qǐng)求”和“配置”的重試狀態(tài)
- * @param {AxiosRequestConfig} config
- * @return {Object}
- */
- function getCurrentState(config) {
- // 從 config 獲取狀態(tài)
- const currentState = config[namespace] || {};
- // 記錄當(dāng)前請(qǐng)求的次數(shù)
- currentState.retryCount = currentState.retryCount || 0;
- // 更新/寫入 config 中當(dāng)前請(qǐng)求狀態(tài)
- config[namespace] = currentState;
- return currentState;
- }
通過對(duì) axios config 注入 axios-retry 字段作為存儲(chǔ)請(qǐng)求狀態(tài)的字段,在 axios 的請(qǐng)求執(zhí)行鏈中,可隨時(shí)從 axios config 中拿到當(dāng)前請(qǐng)求狀態(tài)。
另外,我們看到請(qǐng)求攔截器中并沒有設(shè)置 reject 的函數(shù),或許這里可以添加針對(duì) reject 響應(yīng)函數(shù),用于在發(fā)生請(qǐng)求異常后,可直接不需要重試請(qǐng)求,因?yàn)殄e(cuò)誤的請(qǐng)求配置必然是無意義的網(wǎng)絡(luò)請(qǐng)求,重試請(qǐng)求也是無意義的,直接中斷退出請(qǐng)求執(zhí)行鏈。
關(guān)于退出 Promise 執(zhí)行鏈,提供幾個(gè)參考的討論:
- 從如何停掉 Promise 鏈說起[3]
- Promise 的鏈?zhǔn)秸{(diào)用與中止[4]
2.4 響應(yīng)攔截器設(shè)計(jì)&實(shí)現(xiàn)
在攔截器中,只響應(yīng) reject 函數(shù),也就是只在 axios 響應(yīng)階段發(fā)生錯(cuò)誤(拋出異常)的時(shí)候,才會(huì)執(zhí)行當(dāng)前攔截器。
- axios.interceptors.response.use(null, async (error) => {
- const { config } = error;
- // 讀取不到 config,則退出,可能是一些其他異常情況
- // 例如:主動(dòng)取消請(qǐng)求,是直接拋出的錯(cuò)誤
- if (!config) {
- return Promise.reject(error);
- }
- // 從 defaultOptions 讀取并設(shè)置默認(rèn)值
- const {
- retries = 3, // 默認(rèn)自動(dòng)重試 3 次
- retryCondition = isNetworkOrIdempotentRequestError,
- retryDelay = noDelay,
- shouldResetTimeout = false
- } = getRequestOptions(config, defaultOptions);
- const currentState = getCurrentState(config);
- // 判斷是否需要重試
- if (await shouldRetry(retries, retryCondition, currentState, error)) {
- // 需要的話,則 currentState 需要更新重試次數(shù)
- currentState.retryCount += 1;
- const delay = retryDelay(currentState.retryCount, error);
- // Axios 合并默認(rèn)配置失敗,因?yàn)檠h(huán)結(jié)構(gòu)
- // 參考 issue: https://github.com/mzabriskie/axios/issues/370
- fixConfig(axios, config);
- // shouldResetTimeout 默認(rèn)為 false
- // 根據(jù)實(shí)際請(qǐng)求的時(shí)間,并比較 config.timeout,選最大值來設(shè)置的超時(shí)時(shí)間
- if (!shouldResetTimeout && config.timeout && currentState.lastRequestTime) {
- const lastRequestDuration = Date.now() - currentState.lastRequestTime;
- // Minimum 1ms timeout (passing 0 or less to XHR means no timeout)
- // 設(shè)置超時(shí)時(shí)間最小 1ms(認(rèn)為 <= 0 的 XHR 請(qǐng)求不算超時(shí))
- config.timeout = Math.max(config.timeout - lastRequestDuration - delay, 1);
- }
- config.transformRequest = [(data) => data];
- // 常見的 Promise 延時(shí)的寫法(sleep)
- // 重新發(fā)起請(qǐng)求,調(diào)用 axios(config)
- // 因?yàn)闊o論何種類型請(qǐng)求,都會(huì)被標(biāo)準(zhǔn)化為 axios(config)
- // 在應(yīng)用層 axios.prototye.request 做了兼容轉(zhuǎn)換
- return new Promise((resolve) => setTimeout(() => resolve(axios(config)), delay));
- }
- return Promise.reject(error);
- });
總結(jié)
這是針對(duì) axios 源碼分析文章的一個(gè)補(bǔ)充,作為常見對(duì)于 axios 的功能擴(kuò)展,失敗重試 axios-retry 算是一個(gè)比較好的例子,可以作為之后擴(kuò)展 axios 功能的一個(gè)模板。
另外,axios-retry 中通過 Babel 直接打包,以及其借助 NPM scripts 的生命周期,將測(cè)試、更新版本,打包構(gòu)建、發(fā)布、Git push串聯(lián)起來,也是值得借鑒之處。
在文中有提到,在請(qǐng)求攔截器中可以,添加針對(duì)“發(fā)起網(wǎng)絡(luò)請(qǐng)求”前的錯(cuò)誤處理,如果發(fā)生錯(cuò)誤,直接中斷重試過程,避免錯(cuò)誤的請(qǐng)求多次發(fā)起,節(jié)省計(jì)算資源,可以動(dòng)手嘗試實(shí)現(xiàn)一下。
當(dāng)然,是否需要重試請(qǐng)求,在響應(yīng)攔截器中通過 shouldRetry() 函數(shù)來保證了,但在 axios 請(qǐng)求執(zhí)行鏈上,響應(yīng)攔截器始終是需要通過發(fā)起網(wǎng)絡(luò)請(qǐng)求(dispachRequest() 事件)后才會(huì)執(zhí)行,所以這個(gè)嘗試還是可以研究研究🧐,對(duì)于搞懂 Promise 執(zhí)行鏈大有裨益。
參考資料
[1]Bare Module Specifier Resolution in node.js:
https://github.com/jkrems/proposal-pkg-exports/
[2]package.json - NPM:
https://docs.npmjs.com/cli/v8/configuring-npm/package-json
[3]從如何停掉 Promise 鏈說起:
https://github.com/xieranmaya/blog/issues/5
[4]Promise 的鏈?zhǔn)秸{(diào)用與中止:
https://cnodejs.org/topic/58385d4927d001d606ac197d