Axios 跨端架構是如何實現的?
我們都知道,axios 是是一個跨平臺請求方案,在瀏覽器端采用 XMLHttpRequest API 進行封裝,而在 Node.js 端則采用 http/https 模塊進行封裝。axios 內部采用適配器模式將二者合二為一,在隱藏了底層的實現的同時,又對外開放了一套統一的開放接口。
那么本文,我們將來探討這個話題:axios 的跨端架構是如何實現的?
從 axios 發送請求說起
我們先來看看 axios 是如何發送請求的。
// 發送一個 GET 請求
axios({
method: 'get',
url: 'https://jsonplaceholder.typicode.com/comments'
params: { postId: 1 }
})
// 發送一個 POST 請求
axios({
method: 'post'
url: 'https://jsonplaceholder.typicode.com/posts',
data: {
title: 'foo',
body: 'bar',
userId: 1,
}
})
dispatchRequest() 方法
當使用 axios 請求時,實際上內部是由 Axios[3] 實例的 .request() 方法處理的。
// /v1.6.8/lib/core/Axios.js#L38
async request(configOrUrl, config) {
try {
return await this._request(configOrUrl, config);
} catch (err) {}
}
而 ._request() 方法內部會先將 configOrUrl, config 2 個參數處理成 config 參數。
// /v1.6.8/lib/core/Axios.js#L62
_request(configOrUrl, config) {
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}
// ...
}
這里是為了同時兼容下面 2 種調用方法。
// 調用方式一
axios('https://jsonplaceholder.typicode.com/posts/1')
// 調用方式二
axios({
method: 'get',
url: 'https://jsonplaceholder.typicode.com/posts/1'
})
當然,這不是重點。在 ._request() 方法內部請求最終會交由 dispatchRequest() 處理。
// /v1.6.8/lib/core/Axios.js#L169-L173
try {
promise = dispatchRequest.call(this, newConfig);
} catch (error) {
return Promise.reject(error);
}
dispatchRequest() 是實際調用請求的地方,而實際調用是采用 XMLHttpRequest API(瀏覽器)還是http/https 模塊(Node.js),則需要進一步查看。
// /v1.6.8/lib/core/dispatchRequest.js#L34
export default function dispatchRequest(config) { /* ... */ }
dispatchRequest() 接收的是上一步合并之后的 config 參數,有了這個參數我們就可以發送請求了。
跨端適配實現
// /v1.6.8/lib/core/dispatchRequest.js#L49
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
這里就是我們所說的 axios 內部所使用的適配器模式了。
axios 支持從外出傳入 adapter 參數支持自定義請求能力的實現,不過很少使用。大部分請求下,我們都是使用內置的適配器實現。
defaults.adapter
defaults.adapter 的值如下:
// /v1.6.8/lib/defaults/index.js#L40
adapter: ['xhr', 'http'],
adapters.getAdapter(['xhr', 'http']) 又是在做什么事情呢?
適配器實現
首先,adapters 位于 lib/adapters/adapters.js[4]。
所屬的目錄結構如下:
圖片
可以看到針對瀏覽器和 Node.js 2 個環境的適配支持:http.js、xhr.js。
adapters 的實現如下。
首先,將內置的 2 個適配文件引入。
// /v1.6.8/lib/adapters/adapters.js#L2-L9
import httpAdapter from './http.js';
import xhrAdapter from './xhr.js';
const knownAdapters = {
http: httpAdapter,
xhr: xhrAdapter
}
knownAdapters 的屬性名正好是和 defaults.adapter 的值 ['xhr', 'http'] 是一一對應的。
而 adapters.getAdapter(['xhr', 'http']) 的實現是這樣的:
// /v1.6.8/lib/adapters/adapters.js#L27-L75
export default {
getAdapter: (adapters) => {
// 1)
adapters = Array.isArray(adapters) ? adapters : [adapters];
let nameOrAdapter;
let adapter;
// 2)
for (let i = 0; i < adapters.length; i++) {
nameOrAdapter = adapters[i];
adapter = nameOrAdapter;
// 3)
if (!isResolvedHandle(nameOrAdapter)) {
adapter = knownAdapters[String(nameOrAdapter).toLowerCase()];
}
if (adapter) {
break;
}
}
// 4)
if (!adapter) {
throw new AxiosError(
`There is no suitable adapter to dispatch the request `,
'ERR_NOT_SUPPORT'
);
}
return adapter;
}
}
內容比較長,我們會按照代碼標準的序號分 4 個部分來講。
1)這里是為了兼容調用 axios() 時傳入 adapter 參數的情況。
// `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) {
/* ... */
},
因為接下來 adapters 是作為數組處理,所以這種場景下,我們將 adapter 封裝成數組 [adapters]。
// /v1.6.8/lib/adapters/adapters.js#L28
adapters = Array.isArray(adapters) ? adapters : [adapters];
2)接下來,就是遍歷 adapters 找到要用的那個適配器。
到目前為止,adapters[i](也就是下面的 nameOrAdapter)既可能是字符串('xhr'、'http'),也可能是函數(function (config) {})。
// /v1.6.8/lib/adapters/adapters.js#L37
let nameOrAdapter = adapters[i];
adapter = nameOrAdapter;
3)那么,我們還要檢查 nameOrAdapter 的類型。
// /v1.6.8/lib/adapters/adapters.js#L42-L48
if (!isResolvedHandle(nameOrAdapter)) {
adapter = knownAdapters[(id = String(nameOrAdapter)).toLowerCase()];
}
isResolvedHandle() 是一個工具函數,其目的是為了判斷是否要從 knownAdapters 獲取適配器。
// /v1.6.8/lib/adapters/adapters.js#L24
const isResolvedHandle = (adapter) => typeof adapter === 'function' || adapter === null || adapter === false;
簡單理解,只有 adapter 是字符串的情況('xhr' 或 'http'),isResolvedHandle(nameOrAdapter) 才返回 false,才從 knownAdapters 獲得適配器。
typeof adapter === 'function' || adapter === null 這個判斷條件我們容易理解,這是為了排除自定義 adapter 參數(傳入函數或 null)的情況。
而 adapter === false 又是對應什么情況呢?
那是因為我們的代碼只可能是在瀏覽器或 Node.js 環境下運行。這個時候 httpAdapter 和 xhrAdapter 具體返回是有差異的。
// /v1.6.8/lib/adapters/xhr.js#L48
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
export default isXHRAdapterSupported && function (config) {/* ...*/}
// /v1.6.8/lib/adapters/http.js#L160
const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
export default isHttpAdapterSupported && function httpAdapter(config) {/* ... */}
也就是說:在瀏覽器環境 httpAdapter 返回 false,xhrAdapter 返回函數;在 Node.js 環境 xhrAdapter 返回 false,httpAdapter 返回函數。
因此,一旦 isResolvedHandle() 邏輯執行完成后。
if (!isResolvedHandle(nameOrAdapter)) {/* ... */}
會檢查 adapter 變量的值,一旦有值(非 false)就說明找到適配器了,結束遍歷。
if (adapter) {
break;
}
4)最終在返回適配器前做空檢查
// 4)
if (!adapter) {
throw new AxiosError(
`There is no suitable adapter to dispatch the request `,
'ERR_NOT_SUPPORT'
);
}
return adapter;
如此,就完成了跨端架構的實現。
總結
本文我們講述了 axios 的跨端架構原理。axios 內部實際發出請求是通過 dispatchRequest() 方法處理的,再往里看則是通過適配器模式取得適應于當前環境的適配器函數。
axios 內置了 2 個適配器支持:httpAdapter 和 xhrAdapter。httpAdapter 是 Node.js 環境實現,通過 http/https 模塊;xhrAdapter 這是瀏覽器環境實現,通過 XMLHttpRequest API 實現。Node.js 環境 xhrAdapter 返回 false,瀏覽器環境 httpAdapter 返回 false——這樣總是能返回正確的適配器。
參考資料
[1]axios 是如何實現取消請求的?: https://juejin.cn/post/7359444013894811689
[2]你知道嗎?axios 請求是 JSON 響應優先的: https://juejin.cn/post/7359580605320036415
[3]Axios: https://github.com/axios/axios/blob/v1.6.8/lib/core/Axios.js
[4]lib/adapters/adapters.js: https://github.com/axios/axios/blob/v1.6.8/lib/adapters/adapters.js