原載於 TutorialDocs 網站的文章《How to Implement an HTTP Request Library with Axios》。
概述
前端開發中,經常會遇到傳送非同步請求的場景。一個功能齊全的 HTTP 請求庫可以大大降低我們的開發成本,提高開發效率。
axios 就是這樣一個 HTTP 請求庫,近年來非常熱門。目前,它在 GitHub 上擁有超過 40,000 的 Star,許多權威人士都推薦使用它。
因此,我們有必要了解下 axios 是如何設計,以及如何實現 HTTP 請求庫封裝的。撰寫本文時,axios 當前版本為 0.18.0,我們以該版本為例,來閱讀和分析部分核心原始碼。axios 的所有原始檔都位於 lib
資料夾中,下文中提到的路徑都是相對於 lib
來說的。
本文我們主要討論:
-
怎樣使用 axios。
-
axios 的核心模組(請求、攔截器、撤銷)是如何設計和實現的?
-
axios 的設計優點是什麼?
如何使用 axios
要理解 axios 的設計,首先需要看一下如何使用 axios。我們舉一個簡單的例子來說明下 axios API 的使用。
傳送請求
axios({
method:'get',
url:'http://bit.ly/2mTM3nY',
responseType:'stream'
})
.then(function(response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});
複製程式碼
這是一個官方示例。從上面的程式碼中可以看到,axios 的用法與 jQuery 的 ajax
方法非常類似,兩者都返回一個 Promise 物件(在這裡也可以使用成功回撥函式,但還是更推薦使用 Promise
或 await
),然後再進行後續操作。
這個例項很簡單,不需要我解釋了。我們再來看看如何新增一個攔截器函式。
新增攔截器函式
// 新增一個請求攔截器。注意,這裡面有 2 個函式——分別是成功和失敗時的回撥函式,這樣設計的原因會在之後介紹
axios.interceptors.request.use(function (config) {
// 發起請求前執行一些處理任務
return config; // 返回配置資訊
}, function (error) {
// 請求錯誤時的處理
return Promise.reject(error);
});
// 新增一個響應攔截器
axios.interceptors.response.use(function (response) {
// 處理響應資料
return response; // 返回響應資料
}, function (error) {
// 響應出錯後所做的處理工作
return Promise.reject(error);
});
複製程式碼
從上面的程式碼,我們可以知道:傳送請求之前,我們可以對請求的配置引數(config
)做處理;在請求得到響應之後,我們可以對返回資料做處理。當請求或響應失敗時,我們還能指定對應的錯誤處理函式。
撤銷 HTTP 請求
在開發與搜尋相關的模組時,我們經常要頻繁地傳送資料查詢請求。一般來說,當我們傳送下一個請求時,需要撤銷上個請求。因此,能撤銷相關請求功能非常有用。axios 撤銷請求的示例程式碼如下:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
// 例子一
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('請求撤銷了', thrown.message);
} else {
// 處理錯誤
}
});
// 例子二
axios.post('/user/12345', {
name: '新名字'
}, {
cancelToken: source.token
}).
// 撤銷請求 (資訊引數是可選的)
source.cancel('使用者撤銷了請求');
複製程式碼
從上例中可以看到,在 axios 中,使用基於 CancelToken
的撤銷請求方案。然而,該提案現已撤回,詳情如 點這裡。具體的撤銷請求的實現方法,將在後面的原始碼分析的中解釋。
axios 核心模組的設計和實現
通過上面的例子,我相信每個人都對 axios 的使用有一個大致的瞭解了。下面,我們將根據模組分析 axios 的設計和實現。下面的圖片,是我在本文中會介紹到的原始碼檔案。如果您感興趣,最好在閱讀時克隆相關的程式碼,這能加深你對相關模組的理解。
HTTP 請求模組
請求模組的程式碼放在了 core/dispatchRequest.js
檔案中,這裡我只展示了一些關鍵程式碼來簡單說明:
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// 其他原始碼
// 預設介面卡是一個模組,可以根據當前環境選擇使用 Node 或者 XHR 傳送請求。
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// 其他原始碼
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// 其他原始碼
return Promise.reject(reason);
});
};
複製程式碼
上面的程式碼中,我們能夠知道 dispatchRequest
方法是通過 config.adapter
,獲得傳送請求模組的。我們還可以通過傳遞,符合規範的介面卡函式來替代原來的模組(一般來說,我們不會這樣做,但它是一個鬆散耦合的擴充套件點)。
在 defaults.js
檔案中,我們可以看到相關介面卡的選擇邏輯——根據當前容器的一些獨特屬性和建構函式,來確定使用哪個介面卡。
function getDefaultAdapter() {
var adapter;
// 只有在 Node.js 中包含 process 型別物件時,才使用它的請求模組
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// Node.js 請求模組
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// 瀏覽器請求模組
adapter = require('./adapters/xhr');
}
return adapter;
}
複製程式碼
axios 中的 XHR 模組相對簡單,它是對 XMLHTTPRequest
物件的封裝,這裡我就不再解釋了。有興趣的同學,可以自己閱讀源原始碼看看,原始碼位於 adapters/xhr.js
檔案中。
攔截器模組
現在讓我們看看 axios 是如何處理,請求和響應攔截器函式的。這就涉及到了 axios 中的統一介面 ——request
函式。
Axios.prototype.request = function request(config) {
// 其他原始碼
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());
}
return promise;
};
複製程式碼
這個函式是 axios 傳送請求的介面。因為函式實現程式碼相當長,這裡我會簡單地討論相關設計思想:
-
chain
是一個執行佇列。佇列的初始值是一個攜帶配置(config
)引數的 Promise 物件。 -
在執行佇列中,初始函式
dispatchRequest
用來傳送請求,為了與dispatchRequest
對應,我們新增了一個undefined
。新增undefined
的原因是需要給 Promise 提供成功和失敗的回撥函式,從下面程式碼裡的promise = promise.then(chain.shift(), chain.shift());
我們就能看出來。因此,函式dispatchRequest
和undefiend
可以看成是一對函式。 -
在執行佇列
chain
中,傳送請求的dispatchReqeust
函式處於中間位置。它前面是請求攔截器,使用unshift
方法插入;它後面是響應攔截器,使用push
方法插入,在dispatchRequest
之後。需要注意的是,這些函式都是成對的,也就是一次會插入兩個。
瀏覽上面的 request
函式程式碼,我們大致知道了怎樣使用攔截器。下一步,來看看怎樣撤銷一個 HTTP 請求。
撤銷請求模組
與撤銷請求相關的模組位於 Cancel/
資料夾下,現在我們來看下相關核心程式碼。
首先,我們來看下基礎 Cancel
類。它是一個用來記錄撤銷狀態的類,具體程式碼如下:
function Cancel(message) {
this.message = message;
}
Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};
Cancel.prototype.__CANCEL__ = true;
複製程式碼
使用 CancelToken
類時,需要向它傳遞一個 Promise 方法,用來實現 HTTP 請求的撤銷,具體程式碼如下:
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// 已經被撤銷了
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
複製程式碼
adapters/xhr.js
檔案中,撤銷請求的地方是這樣寫的:
if (config.cancelToken) {
// 等待撤銷
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// 重置請求
request = null;
});
}
複製程式碼
通過上面的撤銷 HTTP請求的例子,讓我們簡要地討論一下相關的實現邏輯:
-
在需要撤銷的請求中,呼叫
CancelToken
類的source
方法類進行初始化,會得到一個包含CancelToken
類例項 A 和cancel
方法的物件。 -
當 source 方法正在返回例項 A 的時候,一個處於 pending 狀態的
promise
物件初始化完成。在將例項 A 傳遞給 axios 之後,promise
就可以作為撤銷請求的觸發器使用了。 -
當呼叫通過
source
方法返回的cancel
方法後,例項 A 中promise
狀態從 pending 變成 fulfilled,然後立即觸發then
回撥函式。於是 axios 的撤銷方法——request.abort()
被觸發了。
axios 這樣設計的好處是什麼?
傳送請求函式的處理邏輯
如前幾章所述,axios 不將用來傳送請求的 dispatchRequest
函式看做一個特殊函式。實際上,dispatchRequest
會被放在佇列的中間位置,以便保證佇列處理的一致性和程式碼的可讀性。
介面卡的處理邏輯
在介面卡的處理邏輯上,http
和 xhr
模組(一個是在 Node.js 中用來傳送請求的,一個是在瀏覽器裡用來傳送請求的)並沒有在 dispatchRequest
函式中使用,而是各自作為單獨的模組,預設通過 defaults.js
檔案中的配置方法引入的。因此,它不僅確保了兩個模組之間的低耦合,而且還為將來的使用者提供了定製請求傳送模組的空間。
撤銷 HTTP 請求的邏輯
在撤銷 HTTP 請求的邏輯中,axios 設計使用 Promise 來作為觸發器,將 resolve
函式暴露在外面,並在回撥函式裡使用。它不僅確保了內部邏輯的一致性,而且還確保了在需要撤銷請求時,不需要直接更改相關類的樣例資料,以避免在很大程度上入侵其他模組。
總結
本文詳細介紹了 axios 的用法、設計思想和實現方法。在閱讀之後,您可以瞭解 axios 的設計,並瞭解模組的封裝和互動。
本文只介紹了 axios 的核心模組,如果你對其他模組程式碼感興趣,可以到 GitHub 上檢視。
(完)