axios 是一個通用的寶藏請求庫,此次探究了 axios 中三個基礎(chǔ)能力的實現(xiàn),并將過程記錄于此.
零. 前置
- axios項目地址:https://github.com/axios/axios
- 閱讀代碼commit hash:fe52a611efe756328a93709bbf5265756275d70d
- 最近 Release 版本:v0.21.1
一. 目標(biāo)
閱讀源碼肯定是帶著問題來學(xué)習(xí)的,所以以下是本次源碼閱讀準(zhǔn)備探究的問題:
- Q1. 如何實現(xiàn)同時支持
axios(config)及axios.get(config)語法 - Q2. 瀏覽器和 NodeJS 請求能力的兼容實現(xiàn)
- Q3. 請求 & 響應(yīng)攔截器實現(xiàn)
二. 項目結(jié)構(gòu)
簡單提煉下項目中比較重要的部分:
三. 從 axios 對象開始分析
當(dāng)在項目中使用 axios 庫時,總是要通過 axios 對象調(diào)用相關(guān)能力,在項目的 test/typescipt/axios.ts 中有比較清晰的測試用例(以下為簡化掉 then 和 catch 后的代碼):
axios(config)
axios.get('/user?id=12345')
axios.get('/user', { params: { id: 12345 } })
axios.head('/user')
axios.options('/user')
axios.delete('/user')
axios.post('/user', { foo: 'bar' })
axios.post('/user', { foo: 'bar' }, { headers: { 'X-FOO': 'bar' } })
axios.put('/user', { foo: 'bar' })
axios.patch('/user', { foo: 'bar' })
從上面代碼可以看出 axios 庫 同時支持 axios(config) 和 axios.get(config) 語法, 并且支持多種請求方法;
接下來逐個分析每個能力的實現(xiàn)過程~
3.1. 弄清 axios 對象
同時支持 axios(config) 和 axios.get(config) 語法,說明 axios 是個函數(shù),同時也是一個具備方法屬性的對象. 所以接下來我們來分析一下 axios 庫暴露的 axios對象.
從項目根目錄可以找到入口文件 index.js,其指向 lib 目錄下的 axios.js, 這里做了三件事:
-
1)使用工廠方法創(chuàng)建實例axios
function createInstance(defaultConfig) { // 對 Axios 類傳入配置對象得到實例,并作為 Axios.prototype.request 方法的上下文 // 作用:支持通過 axios('https://xxx/xx') 語法實現(xiàn)請求 var context = new Axios(defaultConfig); var instance = bind(Axios.prototype.request, context); // 手?jǐn)]版本的 `Function.prototype.bind` // 為了實例具備 Axios 的能力,故將 Axios的原型 & Axios實例的屬性 復(fù)制給 instance utils.extend(instance, Axios.prototype, context); utils.extend(instance, context); return instance; // instance 的真面目是 request 方法 } var axios = createInstance(defaults); -
2)給實例axios 掛載操作方法
axios.Axios = Axios; // 可以通過實例訪問 Axios 類 axios.create = function create(instanceConfig) { // 使用新配置對象,通過工廠類獲得一個新實例 // 作用:如果項目中有固定的配置可以直接 cosnt newAxios = axios.create({xx: xx}) // 然后使用 newAxios 按照 axios API 實現(xiàn)功能 return createInstance(mergeConfig(axios.defaults, instanceConfig)); }; // 取消請求三件套 axios.Cancel = require('./cancel/Cancel'); axios.CancelToken = require('./cancel/CancelToken'); axios.isCancel = require('./cancel/isCancel'); axios.all = function all(promises) { return Promise.all(promises); // 相當(dāng)直接,其實就是 Promise.all }; axios.spread = require('./helpers/spread'); // `Function.prototype.apply` 語法糖 // 判定是否為 createError 創(chuàng)建的 Error 對象 axios.isAxiosError = require('./helpers/isAxiosError'); -
3)暴露實例axios
module.exports = axios; module.exports.default = axios;
從以上分析可以得到結(jié)論,之所以能夠 "同時支持 axios(config) 和 axios.get(config) 語法" ,是因為:
- 暴露的 axios 實例本來就是 request 函數(shù),所以支持
axios(config)語法 - 工廠方法
createInstance最終返回的對象,具備復(fù)制得的 Axios的原型 & Axios實例的屬性,所以也能像 Axios 實例一樣直接使用axios.get(config)語法
3.2. 支持多種請求方法
axios 支持 get | head | options | delete | post | put | patch 當(dāng)然并不是笨笨地逐個實現(xiàn)的,閱讀文件 lib/core/Axios.js 可以看到如下結(jié)構(gòu):
function function Axios(instanceConfig) {} // Axios 構(gòu)造函數(shù)
// 原型上只有兩個方法
Axios.prototype.request = function request(config) {};
Axios.prototype.getUri = function getUri(config) {};
// 為 request 方法提供別名
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config...)); // 這里省略了 mergeConfig 對入?yún)⒌恼? };
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
// 基本同上
});
module.exports = Axios;
其中核心實現(xiàn)自然是通用請求方法 Axios.prototype.request,其他函數(shù)通過調(diào)用 request 方法并傳入不同的配置參數(shù)來實現(xiàn)差異化.
四、封裝請求實現(xiàn)--"適配器"
總所周知,瀏覽器端請求一般是通過 XMLHttpRequest 或者 fetch 來實現(xiàn)請求,而 NodeJS 端則是通過內(nèi)置模塊 http 等實現(xiàn). 那么 axios 是如何實現(xiàn)封裝請求實現(xiàn)使得一套 API 可以在瀏覽器端和 NodeJS 端通用的呢?讓我們繼續(xù)看看 Axios.prototype.request 的實現(xiàn),簡化代碼結(jié)構(gòu)如下:
Axios.prototype.request = function request(config) {
// 此處省略請求配置合并代碼,最終得到包含請求信息的 config 對象
// chain 可以視為請求流程數(shù)組,當(dāng)不添加攔截器時 chain 如下
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
// 下方是攔截器部分,暫時忽略
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {});
// 執(zhí)行請求
while (chain.length) {
// 請求流程數(shù)組前兩個出棧,當(dāng)前分別為 dispatchRequest 和 undefined
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
當(dāng)沒有添加任何攔截器的時候,請求流程數(shù)組 chain 中就只有 [dispatchRequest, undefined] ,此時在下方的 while 只運行一輪,dispatchRequest 作為 then 邏輯接收 config 參數(shù)并運行,繼續(xù)找到 dispatchRequest 的簡化實現(xiàn)(/lib/core/dispatchRequest.js):
module.exports = function dispatchRequest(config) {
// 取消請求邏輯,稍后分析
throwIfCancellationRequested(config);
// 此處略去了對 config 的預(yù)處理
// 嘗試獲取 config 中的適配器,如果沒有則使用默認(rèn)的適配器
var adapter = config.adapter || defaults.adapter;
// 將 config 傳入 adapte 執(zhí)行,得到一個 Promise 結(jié)果
// 如果成功則將數(shù)據(jù)后放入返回對象的 data 屬性,失敗則放入返回結(jié)果的 response.data 屬性
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
response.data = transformData(...); // 此處省略入?yún)? return response; // 等同于 Promise.resolve(response)
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
if (reason && reason.response) {
reason.response.data = transformData(...); // 此處省略入?yún)? }
}
return Promise.reject(reason);
});
};
簡單過完一遍 dispatchRequest 看到重點在于 adapter(config) 之后發(fā)生了什么,于是找到默認(rèn)配置的實現(xiàn)(/lib/default.js):
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// 提供給瀏覽器用的 XHR 適配器
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// 提供給 NodeJS 用的內(nèi)置 HTTP 模塊適配器
adapter = require('./adapters/http');
}
return adapter;
}
var defaults = {
adapter: getDefaultAdapter(),
}
顯而易見,適配器通過判斷 XMLHttpRequest 和 process 對象來判斷當(dāng)前平臺并獲得對應(yīng)的實現(xiàn). 接下來繼續(xù)進入 /lib/adapters 目錄,里面的 xhr.js 和 http.js 分別對應(yīng)適配器的瀏覽器實現(xiàn)和NodeJS 實現(xiàn),而 README.md 介紹了實現(xiàn) adapter 的規(guī)范:
// /lib/adapters/README.md
module.exports = function myAdapter(config) {
// 使用 config 參數(shù)構(gòu)建請求并實現(xiàn)派發(fā),獲得返回后則交給 settle 處理得到 Promise
return new Promise(function(resolve, reject) {
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
// 根據(jù) response 的狀態(tài),成功則執(zhí)行 resolve,否則執(zhí)行 reject并傳入一個 AxiosError
settle(resolve, reject, response);
});
}
// /lib/core/settle.js
module.exports = function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
// 省略參數(shù),createError 創(chuàng)建一個 Error 對象并為其添加 response 相關(guān)屬性方便讀取
reject(createError(...));
}
};
實現(xiàn)自定義適配器要先接收 config , 并基于 config 參數(shù)構(gòu)建請求并實現(xiàn)派發(fā),獲得結(jié)果后返回 Promise,接下來的邏輯控制權(quán)就交回給 /lib/core/dispatchRequest.js 繼續(xù)處理了.
五. 攔截器實現(xiàn)
5.1. 探究"請求|響應(yīng)攔截器"的實現(xiàn)
axios 的一個常用特性就是攔截器,只需要簡單的一行 axios.interceptors.request.use(config => return config)就能實現(xiàn)請求/響應(yīng)的攔截,在項目有鑒權(quán)需求或者返回值需要預(yù)處理時相當(dāng)常用. 現(xiàn)在就來看看這個特性是如何實現(xiàn)的,回到剛剛看過的 Axios.js:
// /lib/core/Axios.js
Axios.prototype.request = function request(config) {
// 此處省略請求配置合并代碼,最終得到包含請求信息的 config 對象
// chain 可以視為請求流程數(shù)組,當(dāng)不添加攔截器時 chain 如下
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
// 攔截器實現(xiàn)
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 往 chain 數(shù)組頭部插入 then & catch邏輯
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected); // 往 chain 尾部插入處理邏輯
});
// 執(zhí)行請求
while (chain.length) {
// 請求流程數(shù)組前兩個出棧,當(dāng)前分別為 dispatchRequest 和 undefined
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
之前說過了項目中使用的 axios 其實就是 Axios.prototype.request,所以當(dāng) Axios.prototype.request 觸發(fā)時,會遍歷 axios.interceptors.request 及 axios.interceptors.response 并將其中的攔截邏輯添加到"請求流程數(shù)組 chain" 中.
在 Axios.prototype.request 中并沒有 interceptors 屬性的實現(xiàn),于是回到 Axios 構(gòu)造函數(shù)中尋找對應(yīng)邏輯(之前說過,工廠函數(shù) createInstance 會將 Axios的原型 & Axios實例的屬性 復(fù)制給生成的 axios 對象):
// /lib/core/Axios.js
function Axios(instanceConfig) {
this.defaults = instanceConfig;
// 實例化 Axios 時也為其添加了 interceptors 對象,其攜帶了 request & response 兩個實例
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
// /lib/core/InterceptorManager.js
function InterceptorManager() {
this.handlers = []; // 數(shù)組,用于管理攔截邏輯
}
InterceptorManager.prototype.use = function use(fulfilled, rejected) {} // 添加攔截器
InterceptorManager.prototype.eject = function eject(id) {} // 刪除攔截器
InterceptorManager.prototype.forEach = function forEach(fn) {} // 遍歷攔截器
Axios 構(gòu)造函數(shù)在創(chuàng)建實例時會完成 interceptors 屬性的創(chuàng)建,實現(xiàn) axios.interceptors.request 及 axios.interceptors.response 對于攔截邏輯的管理.
5.2. 實驗:同 axios 內(nèi)多個攔截器的執(zhí)行順序
由于 axios.interceptors.request 遍歷添加到 "請求流程數(shù)組 chain" 向數(shù)組頭插入 request 攔截器,所以越后 use 的 request 攔截器會越早執(zhí)行. 相反,越后 use 的 response 攔截器會越晚執(zhí)行.
現(xiàn)在假設(shè)為 axios 添加兩個請求攔截器和兩個響應(yīng)攔截器,那么 "請求流程數(shù)組 chain" 就會變成這樣(請求攔截器越后 use 的會越先執(zhí)行):
[
request2_fulfilled, request2_rejected,
request1_fulfilled, request1_rejected,
dispatchRequest, undefined
response3_fulfilled, response3_rejected,
response4_fulfilled, response4_rejected,
]
為此編寫個單測用于打印驗證:
it('should add multiple request & response interceptors', function (done) {
var response;
axios.interceptors.request.use(function (data) {
console.log('request1_fulfilled, request1_rejected')
return data;
});
axios.interceptors.request.use(function (data) {
console.log('request2_fulfilled, request2_rejected')
return data;
});
axios.interceptors.response.use(function (data) {
console.log('response3_fulfilled, response3_rejected')
return data;
});
axios.interceptors.response.use(function (data) {
console.log('response4_fulfilled, response4_rejected')
return data;
});
axios('/foo').then(function (data) {
response = data;
});
getAjaxRequest().then(function (request) {
request.respondWith({
status: 200,
responseText: 'OK'
});
setTimeout(function () {
expect(response.data).toBe('OK');
done();
}, 100);
});
});
結(jié)果如下,攔截器的執(zhí)行打印與期望的結(jié)果一致:
5.3. 探究"取消攔截器"實現(xiàn)
使用以下與 setTimeout 和 clearTimeout 相似的語法,即可將一個定義好的攔截器給取消掉:
var intercept = axios.interceptors.response.use(function (data) { return data; }); // 定義攔截器
axios.interceptors.response.eject(intercept); // 取消攔截器
這里用到了 use 和 eject 兩個方法,所以到 InterceptorManager.js 中找找相關(guān)實現(xiàn):
// /lib/core/InterceptorManager.js
function InterceptorManager() {
this.handlers = []; // 數(shù)組,用于管理攔截邏輯
}
// 添加攔截器
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
// 將攔截邏輯包裝為對象,推入管理數(shù)組 handles 中
this.handlers.push({ fulfilled, rejected });
return this.handlers.length - 1; // 返回當(dāng)前下標(biāo)
};
// 通過取消攔截器
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null; // 根據(jù)下標(biāo)判斷,存在則置空掉
}
};
這里邏輯就比較簡單了,由 InterceptorManager 創(chuàng)建一個內(nèi)置數(shù)組的實例來管理所有攔截器,use 推入一個數(shù)組并返回其數(shù)組下標(biāo),eject 時也用數(shù)組下標(biāo)來置空,這樣就起到了攔截器管理的效果了~
小結(jié)
至此文章就告一段落了,還記得開始提出的三個問題嗎?
Q1. 如何實現(xiàn)同時支持
axios(config)及axios.get(config)語法A1. axios 庫暴露的 axios 對象本來就是一個具備 Axios 實例屬性的 Axios.prototype.request 函數(shù). 詳見"第三節(jié).從 axios 對象開始分析".
Q2. 瀏覽器和 NodeJS 請求能力的兼容實現(xiàn)
A2. 通過判斷平臺后選擇對應(yīng)平臺的適配器實現(xiàn). 詳見"第四節(jié). 封裝請求實現(xiàn)--'適配器'"
Q3. 請求 & 響應(yīng)攔截器實現(xiàn)
A3. 通過數(shù)組的形式管理, 將請求攔截器、請求、響應(yīng)攔截器都放在"請求流程數(shù)組 chain" 中,請求時依次執(zhí)行直到 "請求流程數(shù)組 chain" 為空. 詳見"第五節(jié). 攔截器實現(xiàn)".
歡迎拍磚,覺得還行也歡迎點贊收藏~
新開公號:「無夢的冒險譚」歡迎關(guān)注(搜索 Nodreame 也可以~)
旅程正在繼續(xù) ??ヽ(°▽°)ノ?