介紹axios
一直在使用axios庫,在享受它帶來的便利的同時,總感覺不讀讀它的原始碼有點對不起它,剛好網上介紹axios原始碼的文章較少,所以寫下這篇文章,權當拋磚引玉。
axios是同構的JavaScript的非同步請求庫,它可以在瀏覽器端和NodeJS環境裡使用。
VueJS的作者尤大也推薦這個工具,它除了非同步請求網路資源功能,還有如下功能:
- 提供代理功能
- 提供了攔截器(類似中介軟體),可以註冊在請求發出去之前和收到響應之後的操作
- 可以獲取上傳進度和下載進度
- 提供的adapter選項可以模擬響應資料
- 自定義引起報錯的響應碼範圍
- 提供了取消請求的功能
axios的GitHub地址。
那麼,它是怎麼辦到的呢?
首先說說為什麼它可以在瀏覽器端和NodeJS環境中使用
在axios中,使用介面卡設計模式來遮蔽平臺的差異性,讓使用者可以在瀏覽器端和NodeJS環境中使用同一套API發起http請求。
axios的預設配置裡的adapter是通過getDefaultAdapter()
方法來獲取的,它的邏輯如下:
function getDefaultAdapter() {
var adapter;
// Only Node.JS has a process variable that is of [[Class]] process
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
}
return adapter;
}
複製程式碼
如上面程式碼,通過判斷執行環境的特徵來選擇不同的API發起http請求。
接下來分別介紹這兩個檔案——http和xhr。
http.js
這個檔案裡,引用了NodeJS的http和https庫,用於發出http請求,並使用Promise接收請求結果。
程式碼的細節不介紹了,就講個大概的思路,我們都知道發起http請求,最重要的是遵守http協議,書寫正確的請求頭,而axios就是通過傳入config
接收使用者的一些定製引數,其中包括請求頭,請求引數等等,然後在內部使用(http/https).request(options, callback)發起http請求。
具體如何整合、處理傳入的引數,還請下載原始碼看看。
xhr.js
類似http的邏輯,只不過是呼叫了WebAPI的XMLHTTPRequest介面發起http請求。
攔截器的實現
axios提供了攔截器的功能,可以在請求發起前處理傳入的config或者其它操作,也可以在接收完響應後處理response。
我們可以看看Axios的建構函式,很簡單:
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
複製程式碼
其中的InterceptorManager維護一個陣列,用以收集攔截器函式,有fulfilled
和rejected
,分別對應Promise的onSuccess和onFail的回撥,接下來看看攔截器和發起http請求是如何結合在一起的,我們看看Axios的原型上的request方法:
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
config.method = config.method ? config.method.toLowerCase() : 'get';
// Hook up interceptors middleware
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;
};
複製程式碼
從上面可以看出,它們結合的方式是使用Promise把攔截器和發起http請求的操作結合起來的,interceptors.request
會安排在發起http請求的操作前,interceptors.response
會安排在發起http請求的操作後。
上傳和下載的進度
axios提供了觀察上傳和下載進度的功能,不過僅支援在瀏覽器環境中,核心程式碼如下:
// Handle progress if needed
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// Not all browsers support upload events
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
複製程式碼
從上面可以看出,下載進度回撥其實就是監聽XMLHTTPRequest物件的progress事件,上傳進度回撥其實就是XMLHTTPRequest物件的upload屬性的progress事件。
模擬響應資料
官方文件裡指出這個功能需要開發者返回一個Promise物件並且在Promise裡返回一個有效的Response物件:
// `adapter` allows custom handling of requests which makes testing easier.
// Return a promise and supply a valid response (see lib/adapters/README.md).
adapter: function (config) {
/* ... */
}
複製程式碼
我們可以在原始碼中找到這個功能的實現方式:
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// Transform response data
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
複製程式碼
從上面可以看出,如果我們在使用axios發出http請求時,如果傳入的config物件有adapter屬性,這個屬性會頂替了預設的adapter(NodeJS的http.request()或XMLHTTPRequest),所以我們需要在config的adapter屬性中返回一個Promise,並且這個Promise會返回一個有效的Response物件。
自定義引起報錯的響應碼範圍
axios提供了一個功能,可以自定義報錯的響應碼的範圍,可以通過config.validateStatus
來配置。
預設的範圍是200到300之間:
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
複製程式碼
而在原始碼中,這個方法是通過lib\core\settle.js
來呼叫的:
module.exports = function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
if (!validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(createError(
'Request failed with status code ' + response.status,
response.config,
null,
response.request,
response
));
}
};
複製程式碼
從上面可以看出,settle的入參很像Promise的resolve和reject,接下來,我們看看settle又是在哪裡被呼叫的。
果不其然,在lib\adapters\http.js
和lib\adapters\xhr.js
中都看到settle的身影。
細節就不說了,我大致說一下思路,就是axios使用Promise發起http請求後,會把傳入Promise物件的函式中的resolve和reject再次傳遞給settle中,讓它來決定Promise的狀態是onResolved還是onRejected。
取消請求的功能
axios官方文件指出axios提供了取消已經發出去的請求的功能。
The axios cancel token API is based on the withdrawn cancelable promises proposal.
上面引用的話裡指出這是一個promise的提議,不過已經被撤回了。
在這裡,筆者想說的是,其實不依賴這個提議,我們也可以寫一個簡單取消請求的功能,只要你熟悉閉包就可以了。
思路是這樣的:我們可以使用閉包的方式維護一個是否取消請求的狀態,然後在處理Promise的onResolved回撥的時候判斷一下這個狀態,如果狀態是需要取消請求的話,就reject結果,大致如下:
function dispatchRequest(config) {
let hasCancled = false;
return Promise((resolve, reject) => {
if (hasCancled) {
reject({ hasCancled: true })
} else {
/** 處理正常響應 **/
}
})
.then(/** 其他業務操作 **/)
.catch(err => {
if (err.hasCancled) {
/** 處理請求被取消 */
}
})
}
複製程式碼
總結
最後,我們可以大致瞭解了axios強大的背後原因:使用介面卡模式遮蔽了平臺差異性,並提供統一的API,使用Promise的鏈式呼叫來保證整個請求過程的有序性和增強一些額外的功能。
axios庫是一個很精美的第三庫,值得我們去讀讀它的原始碼。你也會收穫很多的。很感謝你能堅持看到這裡。
不僅文章裡提到的,還有好幾個有趣的課題值得大夥去研究,比如:
- axios是如何設定請求超時的
- axios是如何實現代理的