Axios 原始碼解讀 —— request 篇

曬兜斯發表於2022-01-10

Axios 是一個基於 promise 網路請求庫,作用於 node.js 和瀏覽器中。 它是 isomorphic 的(即同一套程式碼可以執行在瀏覽器和 node.js 中)。在服務端它使用原生 node.js http 模組, 而在客戶端 (瀏覽端) 則使用 XMLHttpRequests

從 Axios 的官方介紹可以得知,這是一個可以同時執行在瀏覽器客戶端 + Node 服務端的網路請求庫,在瀏覽器執行時,使用 XMLHttpRequests 構建請求,在 Node 環境時使用 nodehttp 模組構建網路請求。

今天,我們圍繞著 axios 的原始碼實現進行解讀,解讀完成後,再實現一個簡易的 axios 庫。

我們先來看看 axios 庫的專案目錄結構。(如下圖)

image

從上圖可以得到兩個資訊:

  1. axios 的核心檔案是 lib/axios,所以我們如果只關注 axios 執行時的話,只需要看 lib 這個目錄下的檔案即可。
  2. axios 執行只依賴一個第三方庫 follow-redirects,這個庫是用於處理 HTTP 重定向請求的,axios 的預設行為是跟隨重定向的,可以猜測是用這個庫來做重定向跟隨的。 —— 如果你不想要自動跟隨重定向,需要顯式宣告 maxRedirects=0

我在百度一直沒找到 axios 的官方文件,所以這裡貼一份 axios 官方文件,大家可以參考使用。

lib/axios

我們開啟 lib/axios.js 檔案看看。(如下圖)

image

重點關注這幾行核心就可以了。

| 行數 | 描述 |
| ---- | ---- |
| 第 26 行 | 文件中的 axios.create 呼叫的是 createInstance 函式,這個函式將會新建一個 Axios 例項 |
| 第 34 行 | 新建了一個預設的 axios 例項 |
| 第 37 ~ 52 行 | 預設的 axios 例項新增了大量的屬性和方法 |
| 第 57 行 | 將預設的 axios 例項匯出 |

Axios

接下來,我們來看看 Axios 類,這是 axios 原始碼最核心的部分,位於 lib/core/Axios.js

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios 接收配置,將 instanceConfig 配置存在 axios.defaults 屬性中,用於後續的請求。

同時,通過 InterceptorManager 來管理請求攔截器和響應攔截器,在後面我們會展開聊聊這個攔截管理器 —— InterceptorManager

axios 預設的例項將會使用 lib/defaults.js 中的配置進行建立。

Axios 這個設定,我們就知道文件中關於修改配置的這部分內容緣由了。(如下圖)

image

我們也能看出 Axios 例項是 axios 對於 網路請求-預設配置 的最小單位,而不存在所有例項共享的一套 “全域性預設配置”

但是我們可以通過兩個方法來實現。

其中一個方法就是我們寫兩套配置,一套全域性預設配置,一套各例項的個性化配置,在建立 axios 例項的時候做手動合併。

當然,還有個更聰明的方法,我們先來看看之前 P1 的 createInstance 方法。

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);
  
  //...

  instance.create = function create(instanceConfig) {
    // 這裡通過閉包繼承了 defaultConfig 配置,新建立的例項會繼承原例項的配置
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

從程式碼中可以看出,createInstance 方法內部通過閉包繼承了 defaultConfig 配置,新建立的例項會繼承原例項的配置。

這樣的話,我們還可以通過先用一套全域性預設配置建立一個 axios 例項,然後再使用這個 axios 例項,呼叫 axios/instance.create 方法建立其他的 axios 例項,這樣所有的 axios 例項都可以繼承全域性預設配置。

攔截管理器 —— InterceptorManager

我們現在來看看 axios 內部的攔截管理器 InterceptorManager。(如下圖)

image

InterceptorManager 的總體實現還是挺簡單的,內部有個 handlers 陣列,存放了所有通過 use 方法註冊的攔截器。

use 方法返回了 handlers.length - 1 作為攔截器 id,在呼叫 eject 方法時,會將對應 ID 下標的攔截器設定為 null

forEach 方法對所有的攔截器方法進行遍歷,執行傳入的 fn 回撥函式,將攔截器作為引數傳入 fn 中。

從這裡可以看出,axios 內部的職責劃分還是比較清晰的,InterceptorManager 只負責收集管理攔截器,而不關心攔截器的執行邏輯。

不過我感覺 forEach 方法設計的有些冗餘,如果是我來設計的話,我可能只會暴露一個 getter 方法讓外部獲取 handlers。這裡作者的設計可能有一些別的考慮,我暫時還沒有想到。

request

接下來,我們看看 Axios 類的核心方法,也就是 request 方法,axios 通過 request 方法來發起真實網路請求。

這一段程式碼比較長,我會對這一大段程式碼進行逐行解析。request 方法中對於攔截器和請求的處理非常優雅,我會重點介紹。

相對比較簡單的部分我直接用表格介紹(如下)

image

| 行數 | 描述 |
| ---- | ---- |
| 第 32 ~ 41 行 | 判斷首個引數,組裝 config 配置,禁止無 url 的請求 |
| 第 46 ~ 52 行 | 設定請求方法,如果未宣告則使用預設配置的請求方法,如果未設定預設的請求方法,則使用 get 請求 |

下面我們要著重介紹一下 攔截器 的處理,我們先看 請求攔截器

image

這裡有兩個文件中未說明的引數,這裡我們做一下解釋:

  • 58 行:使用 use 註冊攔截器時,第三個引數中的 options.runWhen 方法將會先被呼叫,如果該方法返回 false,則跳過該請求攔截器。
  • 62 行:使用 use 註冊攔截器時,第三個引數中的 options.synchronous 引數將顯式宣告該攔截器為 同步,否則預設為 非同步 攔截器,將會通過 promise 進行呼叫。 —— 其實我覺得這個引數沒什麼意義了,統一 非同步 就好了。可能作者還考慮了其他的某些同步場景,我暫時還沒有想到。

重點注意:第 64 行使用了 unshift 方法將 請求攔截器 按註冊的逆序新增到 requestInterceptorChain 中,供後續執行。

這也就意味著 請求攔截器 對同一份配置的修改,後面加的攔截器是無法覆蓋前置攔截器的。

我們看看下面這個請求攔截器案例就知道了。

image

後設定的攔截器看起來並未生效,看過原始碼我們就知道了,其實是執行順序導致的。

axios 這麼設計的原因可能是防止 請求攔截器 濫用導致配置被後續處理人覆蓋。—— 但這點沒有在文件說明,如果正好碰上這種場景,難免會造成一些困惑。

響應攔截器 的處理就簡單多了,相信我應該不用多做解釋了。(如下圖)

image

稍微值得注意的是,攔截器 將成功處理和錯誤處理都新增到了內部攔截器陣列中,也就是說陣列內部是這樣的:

['攔截器成功處理處理函式', '攔截器出錯處理函式', '攔截器成功處理處理函式', '攔截器出錯處理函式', ...]

瞭解這個資料結構,對最後這一段核心程式碼的實現理解是有幫助的(如下圖)

image

我們需要對每一行程式碼進行逐行解析:

| 行數 | 描述 |
| ---- | ---- |
| 第 74 行 | 判斷是否為非同步請求攔截器(預設:是) |
| 第 75 行 | 宣告 chain 陣列,陣列第一個元素是發起請求的方法(可以簡單理解為 fetch 方法),第二個元素是為了 82 行湊數的 undefined |
| 第 77 行| 將所有的 請求攔截器 新增到 chain 的開頭位置 |
| 第 78 行| 將所有的 響應攔截器 新增到 chain 的尾部位置 |
| 第 80 ~ 83 行| 用 config 構建第一個 Promise,然後按順序依次執行 chain —— 請求攔截器 -> 真實請求 -> 響應攔截器,每次執行傳入的就是 成功處理函式(作為 resolve) 和 失敗處理函式(作為 reject) |

最後一段 chain 的執行,非常優雅的闡述了 axios 內部的工作流程,也就是 請求攔截器 -> 真實請求 -> 響應攔截器 這一套核心工作流。

建議大家可以仔細再看看最後一段函式的處理,仔細品一品。

下面還有一段關於 同步請求攔截器 的處理,基本上是大同小異的,感興趣的童鞋可以自行閱讀一下。

小結

好了,到這裡,axios 的基本結構和核心工作流程就解析完了。

下一章,我會針對 真實請求 —— dispatchRequest 進行詳細解析,請大家繼續關注。

最後一件事

如果您已經看到這裡了,希望您還是點個贊再走吧~

您的點贊是對作者的最大鼓勵,也可以讓更多人看到本篇文章!

如果覺得本文對您有幫助,請幫忙在 github 上點亮 star 鼓勵一下吧!

相關文章