Axios 是一個基於 promise 網路請求庫,作用於 node.js
和瀏覽器中。 它是 isomorphic 的(即同一套程式碼可以執行在瀏覽器和 node.js
中)。在服務端它使用原生 node.js
http
模組, 而在客戶端 (瀏覽端) 則使用 XMLHttpRequests
。
從 Axios 的官方介紹可以得知,這是一個可以同時執行在瀏覽器客戶端 + Node 服務端的網路請求庫,在瀏覽器執行時,使用 XMLHttpRequests
構建請求,在 Node
環境時使用 node
的 http
模組構建網路請求。
今天,我們圍繞著 axios
的原始碼實現進行解讀,解讀完成後,再實現一個簡易的 axios
庫。
我們先來看看 axios
庫的專案目錄結構。(如下圖)
從上圖可以得到兩個資訊:
- axios 的核心檔案是
lib/axios
,所以我們如果只關注axios
執行時的話,只需要看lib
這個目錄下的檔案即可。 - axios 執行只依賴一個第三方庫
follow-redirects
,這個庫是用於處理HTTP
重定向請求的,axios
的預設行為是跟隨重定向的,可以猜測是用這個庫來做重定向跟隨的。 —— 如果你不想要自動跟隨重定向,需要顯式宣告maxRedirects=0
。
我在百度一直沒找到 axios
的官方文件,所以這裡貼一份 axios 官方文件,大家可以參考使用。
lib/axios
我們開啟 lib/axios.js
檔案看看。(如下圖)
重點關注這幾行核心就可以了。
| 行數 | 描述 |
| ---- | ---- |
| 第 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
這個設定,我們就知道文件中關於修改配置的這部分內容緣由了。(如下圖)
我們也能看出 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
。(如下圖)
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
方法中對於攔截器和請求的處理非常優雅,我會重點介紹。
相對比較簡單的部分我直接用表格介紹(如下)
| 行數 | 描述 |
| ---- | ---- |
| 第 32 ~ 41
行 | 判斷首個引數,組裝 config
配置,禁止無 url
的請求 |
| 第 46 ~ 52
行 | 設定請求方法,如果未宣告則使用預設配置的請求方法,如果未設定預設的請求方法,則使用 get
請求 |
下面我們要著重介紹一下 攔截器
的處理,我們先看 請求攔截器
。
這裡有兩個文件中未說明的引數,這裡我們做一下解釋:
- 第
58
行:使用use
註冊攔截器時,第三個引數中的options.runWhen
方法將會先被呼叫,如果該方法返回false
,則跳過該請求攔截器。 - 第
62
行:使用use
註冊攔截器時,第三個引數中的options.synchronous
引數將顯式宣告該攔截器為同步
,否則預設為非同步
攔截器,將會通過promise
進行呼叫。 —— 其實我覺得這個引數沒什麼意義了,統一非同步
就好了。可能作者還考慮了其他的某些同步場景,我暫時還沒有想到。
重點注意:第 64
行使用了 unshift
方法將 請求攔截器
按註冊的逆序新增到 requestInterceptorChain
中,供後續執行。
這也就意味著 請求攔截器
對同一份配置的修改,後面加的攔截器是無法覆蓋前置攔截器的。
我們看看下面這個請求攔截器案例就知道了。
後設定的攔截器看起來並未生效,看過原始碼我們就知道了,其實是執行順序導致的。
axios
這麼設計的原因可能是防止 請求攔截器
濫用導致配置被後續處理人覆蓋。—— 但這點沒有在文件說明,如果正好碰上這種場景,難免會造成一些困惑。
而 響應攔截器
的處理就簡單多了,相信我應該不用多做解釋了。(如下圖)
稍微值得注意的是,攔截器
將成功處理和錯誤處理都新增到了內部攔截器陣列中,也就是說陣列內部是這樣的:
['攔截器成功處理處理函式', '攔截器出錯處理函式', '攔截器成功處理處理函式', '攔截器出錯處理函式', ...]
瞭解這個資料結構,對最後這一段核心程式碼的實現理解是有幫助的(如下圖)
我們需要對每一行程式碼進行逐行解析:
| 行數 | 描述 |
| ---- | ---- |
| 第 74
行 | 判斷是否為非同步請求攔截器(預設:是) |
| 第 75
行 | 宣告 chain
陣列,陣列第一個元素是發起請求的方法(可以簡單理解為 fetch
方法),第二個元素是為了 82
行湊數的 undefined
|
| 第 77
行| 將所有的 請求攔截器
新增到 chain
的開頭位置 |
| 第 78
行| 將所有的 響應攔截器
新增到 chain
的尾部位置 |
| 第 80 ~ 83
行| 用 config
構建第一個 Promise
,然後按順序依次執行 chain
—— 請求攔截器
-> 真實請求
-> 響應攔截器
,每次執行傳入的就是 成功處理函式
(作為 resolve
) 和 失敗處理函式
(作為 reject
) |
最後一段 chain
的執行,非常優雅的闡述了 axios
內部的工作流程,也就是 請求攔截器
-> 真實請求
-> 響應攔截器
這一套核心工作流。
建議大家可以仔細再看看最後一段函式的處理,仔細品一品。
下面還有一段關於 同步請求攔截器
的處理,基本上是大同小異的,感興趣的童鞋可以自行閱讀一下。
小結
好了,到這裡,axios
的基本結構和核心工作流程就解析完了。
下一章,我會針對 真實請求
—— dispatchRequest
進行詳細解析,請大家繼續關注。
最後一件事
如果您已經看到這裡了,希望您還是點個贊再走吧~
您的點贊是對作者的最大鼓勵,也可以讓更多人看到本篇文章!
如果覺得本文對您有幫助,請幫忙在 github 上點亮 star
鼓勵一下吧!