扣丁書屋

Axios 功能擴展之 axios-retry 源碼閱讀筆記

前兩天分析了 [Axios 的源碼設計] ,其中的攔截器(interceptor)為擴展 Axios 留下了入口,在工作中我們也時常會擴展 Axios,例如:取消重復請求、權限驗證、失敗重試等。

那么如何設計實現一個好的攔截器來擴展 Axios?

通過對 axios-retry 這一周下載量 100w+ 的三方庫來學習下其功能設計,工具庫項目的發包策略,并借此拋磚引玉,以提升我們的編碼設計能力!

  • Github: https://github.com/softonic/axios-retry
  • NPM: https://www.npmjs.com/package/axios-retry

一、工具庫的 package.json 寫法

看一個模塊的源碼,首先先看 README.mdpackage.json 文件。

package.json

參考如上,未來我們也應該在開發工具庫的時候需要關注以下字段

  • files:在發包的時候發布將 es、lib 兩文件夾,以及 index.jsindex.d.ts 文件。
  • typings:TypeScript 類型定義文件,用于在 TypeScript 編碼環境下智能類型提示,該字段亦可寫作 types。
  • main:主要入口文件,表明在項目中引入當前庫時候,默認指向的文件是 index.js
  • module:并非官方字段,打包工具約定的如果有該字段,則在例如 RollupWebpack 打包時,處理指定導入我們庫的 ESM 版本的文件路徑。
  • exports:提供了一種方法來為不同的環境和 JavaScript 風格顯示聲明如何引入模塊,同時限制對其內部部分的訪問,該字段提案來自:Bare Module Specifier Resolution in node.js[1]

通過依賴字段以及 scripts 字段:

開發依賴和使用依賴

可以得知,當前項目直接使用 Babel 作為打包編譯工具,通過執行 npm run release 發包,并結合 npm scriptsprepost 執行生命周期依次執行完成如下任務:

npm run release 執行的任務流程 (原文鏈接可查看大圖)

更多關于 package.json 字段的功能/作用描述,可參考 package.json - NPM[2]

二、源碼分析

根據 package.json 文件中關于“發包”命令相關解讀之后,可以得知 ./es/ 文件夾下的 index.mjs 為功能實現文件。

2.1 為什么是 .mjs 文件名后綴

Node.js 原本的模塊系統是 CommonJs (使用 requiremodule.exports 語法)。

自 Node.js 創建后, ECMAScript 模塊系統 (使用 importexport 語法) 已經變成一種標準,并且 Node.js 已經加入并實現支持 ES 模塊系統。

Node.js 將 *.cjs 文件當作 CommonJS 模塊, *.mjs 文件當作 ECMAScript 模塊。它會將 .js 文件視為項目的默認模塊系統,除非 package.json 聲明 "type": "module",否則就是 CommonJS。

2.2 axios-retry 的用法

axios-retry 對外導出 axiosRetry() 方法:

注入攔截器

通過對 axios 單例添加“攔截器”,來擴展實現自動重試網絡請求功能。

axios-retry 主要接受兩個參數,第一個是 axios 實例,第二個是 axios-retry 的配置 defaultOptions


defaultOptions: {
    retries?: number; // 自動重試次數
    shouldResetTimeout?: boolean; // 是否重置“超時時間”
    retryCondition?: Function; // 重試的條件,可傳入自定義判斷函數
    retryDelay?: Function;  // 重試請求的間隔時間的函數
}

功能配置看起來挺完善的,難怪那么受歡迎。

2.3 請求攔截器設計&實現

在請求攔截器中會做狀態初始化,更新請求次數:

axios.interceptors.request.use((config) => {
  const currentState = getCurrentState(config);
  // 設置上次請求的時間
  // 思考:為什么不放到 getCurrentState() 函數內一起設置?
  currentState.lastRequestTime = Date.now();
  return config;
});
/**
 * 初始化并返回給定“請求”和“配置”的重試狀態
 * @param  {AxiosRequestConfig} config
 * @return {Object}
 */
function getCurrentState(config) {
  // 從 config 獲取狀態
  const currentState = config[namespace] || {};
  // 記錄當前請求的次數
  currentState.retryCount = currentState.retryCount || 0;
  // 更新/寫入 config 中當前請求狀態
  config[namespace] = currentState;
  return currentState;
}

通過對 axios config 注入 axios-retry 字段作為存儲請求狀態的字段,在 axios 的請求執行鏈中,可隨時從 axios config 中拿到當前請求狀態。

另外,我們看到請求攔截器中并沒有設置 reject 的函數,或許這里可以添加針對 reject 響應函數,用于在發生請求異常后,可直接不需要重試請求,因為錯誤的請求配置必然是無意義的網絡請求,重試請求也是無意義的,直接中斷退出請求執行鏈。

關于退出 Promise 執行鏈,提供幾個參考的討論:

  • 從如何停掉 Promise 鏈說起[3]
  • Promise 的鏈式調用與中止[4]

2.4 響應攔截器設計&實現

在攔截器中,只響應 reject 函數,也就是只在 axios 響應階段發生錯誤(拋出異常)的時候,才會執行當前攔截器。


axios.interceptors.response.use(null, async (error) => {
  const { config } = error;

  // 讀取不到 config,則退出,可能是一些其他異常情況
  // 例如:主動取消請求,是直接拋出的錯誤
  if (!config) {
    return Promise.reject(error);
  }

  // 從 defaultOptions 讀取并設置默認值
  const {
    retries = 3, // 默認自動重試 3 次
    retryCondition = isNetworkOrIdempotentRequestError,
    retryDelay = noDelay,
    shouldResetTimeout = false
  } = getRequestOptions(config, defaultOptions);

  const currentState = getCurrentState(config);

  // 判斷是否需要重試
  if (await shouldRetry(retries, retryCondition, currentState, error)) {
    // 需要的話,則 currentState 需要更新重試次數
    currentState.retryCount += 1;
    const delay = retryDelay(currentState.retryCount, error);

    // Axios 合并默認配置失敗,因為循環結構
    // 參考 issue: https://github.com/mzabriskie/axios/issues/370
    fixConfig(axios, config);

    // shouldResetTimeout 默認為 false
    // 根據實際請求的時間,并比較 config.timeout,選最大值來設置的超時時間
    if (!shouldResetTimeout && config.timeout && currentState.lastRequestTime) {
      const lastRequestDuration = Date.now() - currentState.lastRequestTime;
      // Minimum 1ms timeout (passing 0 or less to XHR means no timeout)
      // 設置超時時間最小 1ms(認為 <= 0 的 XHR 請求不算超時)
      config.timeout = Math.max(config.timeout - lastRequestDuration - delay, 1);
    }

    config.transformRequest = [(data) => data];

    // 常見的 Promise 延時的寫法(sleep)
    // 重新發起請求,調用 axios(config)
    // 因為無論何種類型請求,都會被標準化為 axios(config)
    // 在應用層 axios.prototye.request 做了兼容轉換
    return new Promise((resolve) => setTimeout(() => resolve(axios(config)), delay));
  }

  return Promise.reject(error);
});

總結

這是針對 axios 源碼分析文章的一個補充,作為常見對于 axios 的功能擴展,失敗重試 axios-retry 算是一個比較好的例子,可以作為之后擴展 axios 功能的一個模板。

另外,axios-retry 中通過 Babel 直接打包,以及其借助 NPM scripts 的生命周期,將測試、更新版本,打包構建、發布、Git push串聯起來,也是值得借鑒之處。

在文中有提到,在請求攔截器中可以,添加針對“發起網絡請求”前的錯誤處理,如果發生錯誤,直接中斷重試過程,避免錯誤的請求多次發起,節省計算資源,可以動手嘗試實現一下。

當然,是否需要重試請求,在響應攔截器中通過 shouldRetry() 函數來保證了,但在 axios 請求執行鏈上,響應攔截器始終是需要通過發起網絡請求(dispachRequest() 事件)后才會執行,所以這個嘗試還是可以研究研究,對于搞懂 Promise 執行鏈大有裨益。

參考資料

[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 的鏈式調用與中止: https://cnodejs.org/topic/58385d4927d001d606ac197d


https://mp.weixin.qq.com/s/MYAcgXDrR2b6rtxaMxxuOA

最多閱讀

iOS 性能檢測新方式?——AnimationHitches 7月以前  |  17212次閱讀
快速配置 Sign In with Apple 2年以前  |  5469次閱讀
APP適配iOS11 3年以前  |  4427次閱讀
App Store 審核指南[2017年最新版本] 3年以前  |  4256次閱讀
所有iPhone設備尺寸匯總 3年以前  |  4178次閱讀
使用 GPUImage 實現一個簡單相機 3年以前  |  3907次閱讀
開篇 關于iOS越獄開發 3年以前  |  3790次閱讀
在越獄的iPhone設置上使用lldb調試 3年以前  |  3715次閱讀
給數組NSMutableArray排序 3年以前  |  3639次閱讀
UITableViewCell高亮效果實現 3年以前  |  3344次閱讀
使用ssh訪問越獄iPhone的兩種方式 3年以前  |  3343次閱讀
關于Xcode不能打印崩潰日志 3年以前  |  3240次閱讀
使用ssh 訪問越獄iPhone的兩種方式 3年以前  |  3080次閱讀
為對象添加一個釋放時觸發的block 3年以前  |  2853次閱讀
使用最高權限操作iPhone手機 3年以前  |  2825次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>