axios 源碼解析:十分鐘帶你實(shí)現(xiàn)一個(gè) mini-axios
共 30035字,需瀏覽 61分鐘
·
2024-07-10 08:49
大廠技術(shù) 高級(jí)前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
原文:https://juejin.cn/post/7388316163578363916
整個(gè)實(shí)現(xiàn)流程分為 5 個(gè)大部分:
-
準(zhǔn)備測(cè)試環(huán)境 -
axios 核心請(qǐng)求構(gòu)建 -
多宿主環(huán)境(瀏覽器 || node)適配思想 -
攔截器的實(shí)現(xiàn)原理 -
如何取消請(qǐng)求
1、準(zhǔn)備基礎(chǔ)的測(cè)試環(huán)境
1.1 基于 Koa 準(zhǔn)備一個(gè)最簡(jiǎn)單的服務(wù)程序:
import Koa from 'koa';
const app = new Koa();
// 一個(gè)簡(jiǎn)單的路由處理函數(shù)
app.use(async ctx => {
ctx.body = 'Hello, World!';
});
// 啟動(dòng)服務(wù)器
app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});
因?yàn)槲覀冃枰跒g覽器中測(cè)試請(qǐng)求,所以服務(wù)端還需要支持瀏覽器跨域,所以我們添加一個(gè)支持跨域的中間件:
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*'); // 允許所有來(lái)源
ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (ctx.request.method === 'OPTIONS') {
ctx.status = 200;
return;
}
await next();
});
1.2 準(zhǔn)備瀏覽器和node端測(cè)試環(huán)境:
我們初始化基礎(chǔ)的測(cè)試 html文件以及 node 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./index.js"></script>
</body>
</html>
console.log('基礎(chǔ)的瀏覽器測(cè)試環(huán)境')
基礎(chǔ)的 node 測(cè)試環(huán)境比較簡(jiǎn)單,就是一個(gè)普通的 js 文件,只要我們上述的 js 文件不包含瀏覽器端的宿主 api 那么也可以直接在 node 端進(jìn)行測(cè)試。整個(gè)結(jié)構(gòu)搭建完成之后應(yīng)該就是下面的文件格式:
2、axios 核心請(qǐng)求構(gòu)建
2.1 開(kāi)發(fā) axios 的入口模塊:
我們?cè)跍y(cè)試文件夾下面新建一個(gè) axios.js 的文件,入口內(nèi)容開(kāi)發(fā)比較簡(jiǎn)單,我們就不再過(guò)多贅述了,主要就是開(kāi)發(fā)一個(gè) Axios 類(lèi),初始化 axios 工廠函數(shù)以及導(dǎo)出 axios 實(shí)例:
// util.js
/**
*
* @param {Object} config1
* @param {Object} config2
*/
export const mergeConfig = (config1, config2) => {
return Object.assign(config1, config2)
}
import { mergeConfig } from './utils.js'
class Axios {
constructor(defaultConfig) {
this.defaultConfig = defaultConfig
}
/**
*
* @param {string} url
* @param {Object} options
*/
requiest(url, options) {
try {
this._requiest(url, options)
} catch (error) {
}
}
/**
*
* @param {string} url
* @param {Object} options
*/
_requiest(url, options) {
console.log('開(kāi)始發(fā)送請(qǐng)求', url, options)
}
}
/**
*
* @param {Object} defaultConfig axios 的基礎(chǔ)配置
*/
function createInstance(defaultConfig) {
// 初始化 axios 實(shí)例
const context = new Axios(defaultConfig)
const instance = Axios.prototype.requiest.bind(context)
// 實(shí)例上掛手動(dòng)掛載一個(gè) create 方法
instance.create = function create(instanceConfig) {
// 將用戶傳入的配置和默認(rèn)配置進(jìn)行合并
return createInstance(mergeConfig(defaultConfig, instanceConfig))
};
return instance
}
// 基于默認(rèn)配置,利用工廠函數(shù)創(chuàng)建出一個(gè)默認(rèn)的 axios 實(shí)例
const axios = createInstance({
// 默認(rèn)的網(wǎng)絡(luò)延遲時(shí)間
timeout: 0,
// adapter: 默認(rèn)的適配器配置
adapter: ["xhr", "http", "fetch"],
// 基礎(chǔ)路徑
beseURL: "",
headers: {}
});
// 給 axios 添加一系列其他配置
axios.Axios = Axios;
axios.default = axios;
export default axios
axios 入口核心代碼其實(shí)比較簡(jiǎn)單,最核心的就是利用工廠函數(shù)創(chuàng)建出一個(gè)最基礎(chǔ)的 request 請(qǐng)求方法。
如果我們不需要進(jìn)行額外的自定義配置,那么 axios 本身就已經(jīng)可以開(kāi)箱即用了。如果我們調(diào)用 create,本質(zhì)上就是合并用戶自定義的 axios 配置然后重新產(chǎn)生一個(gè) requiest 方法。
開(kāi)發(fā)完畢之后,我們就可以在之前已經(jīng)準(zhǔn)備好的測(cè)試文件中導(dǎo)入 axios 實(shí)例來(lái)進(jìn)行測(cè)試了:
import axios from './axios.js'
axios('http://localhost:3000/')
瀏覽器中最基礎(chǔ)的測(cè)試已經(jīng)可以了。
我們看一下node環(huán)境:
至此基礎(chǔ)的開(kāi)發(fā)和測(cè)試就完畢了。
2.2 利用參數(shù)歸一化的技巧處理 _requiest 的參數(shù)問(wèn)題:
參數(shù)歸一化是 js 這種弱類(lèi)型語(yǔ)言中一種常見(jiàn)的統(tǒng)一函數(shù)入?yún)⒌姆椒ǎ锰幘褪?減少主干函數(shù)中對(duì)于參數(shù)校驗(yàn)的判斷邏輯,統(tǒng)一函數(shù)參數(shù)的類(lèi)型,讓主干函數(shù)的代碼更加清爽
/**
* 參數(shù)歸一化的輔助函數(shù)
* @param {string} url
* @param {Object} options
*/
requestHelper(url, options) {
let config = {}
if (typeof url === 'string') {
config = options || {};
config.url = url;
} else if (typeof url === 'object') {
config = url;
}
return config
}
/**
*
* @param {string} url
* @param {Object} options
*/
_requiest(url, options) {
// 首先進(jìn)行參數(shù)歸一化,將所有的參數(shù)全部統(tǒng)一為一個(gè)配置對(duì)象
const config = this.requestHelper(url, options)
console.log('config', config)
}
我們來(lái)測(cè)試一下輸出:
我們可以看到參數(shù)就已經(jīng)統(tǒng)一成為一個(gè)對(duì)象了。在統(tǒng)一完畢 _requiest 這個(gè)函數(shù)的參數(shù)之后,因?yàn)楝F(xiàn)在 Axios 這個(gè)類(lèi)中存在一個(gè) defaultConfig 的默認(rèn)配置,而 _requiest 本身又可以接收一個(gè)配置對(duì)象,所以我們可以將將這兩個(gè)配置進(jìn)行簡(jiǎn)單的合并:
/**
*
* @param {string} url
* @param {Object} options
*/
_requiest(url, options) {
// 首先進(jìn)行參數(shù)歸一化,將所有的參數(shù)全部統(tǒng)一為一個(gè)配置對(duì)象
const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
console.log('最終的配置', config)
}
3、多環(huán)境請(qǐng)求發(fā)送的問(wèn)題處理:
前端工程師接觸的更多的環(huán)境一般是瀏覽器環(huán)境,瀏覽器環(huán)境中兩個(gè)發(fā)送請(qǐng)求的方案:
-
xhr -
fetch
但是如果是比較舊的node環(huán)境的話這兩種請(qǐng)求方案都不支持,node環(huán)境中原生支持的請(qǐng)求庫(kù)是 http 以及 https 模塊。
axios作為一個(gè)通用的http請(qǐng)求庫(kù)就必須要解決這個(gè)問(wèn)題。也就是它必須能夠適應(yīng)不同環(huán)境的請(qǐng)求方案。針對(duì)這個(gè)問(wèn)題,axios 提出了 適配器 的概念,axios中所有的請(qǐng)求的發(fā)送都是基于這個(gè)適配器來(lái)進(jìn)行發(fā)送的,源碼中專門(mén)有一個(gè)模塊來(lái)處理請(qǐng)求適配的問(wèn)題:
適配器的思想其實(shí)極其簡(jiǎn)單,就是根據(jù)判斷哪一套請(qǐng)求 api 存在,那么就使用那一套請(qǐng)求 api。這個(gè)和 vue 內(nèi)部 nextTick 異步模塊的處理方案是一致的。大家如果感興趣可以去查閱一下。
我們來(lái)簡(jiǎn)單實(shí)現(xiàn)一下適配器的核心邏輯:
我們新建一個(gè) Adapte.js 的文件:
export default {
/**
* 獲取請(qǐng)求適配器的方法
* @param {Function | Function[]} adapters
*/
getAdapter(adapters) {
}
}
我們同樣可以進(jìn)行參數(shù)歸一化:
// getAdapte 參數(shù)歸一化
/**
* 獲取請(qǐng)求適配器的方法
* @param {Function | Function[]} adapters
*/
const getAdapteHandlers = (adapters) => {
return Array.isArray(adapters) ? adapters : [adapters]
}
export default {
/**
* 獲取請(qǐng)求適配器的方法
* @param {Function | Function[] | string[]} adapters
*/
getAdapter(adapters) {
// 參數(shù)歸一化
adapters = getAdapteHandlers(adapters)
},
}
我們?cè)傩陆ㄒ粋€(gè) dispatchRequest 模塊,并且在這個(gè)模塊中統(tǒng)一發(fā)送 axios 請(qǐng)求:
/**
*
* axios 統(tǒng)一進(jìn)行 http 請(qǐng)求發(fā)送的模塊
*
* @param {Object} config
*/
export default function dispatchRequest(config) {
console.log('開(kāi)始請(qǐng)求發(fā)送', config)
}
我們將這個(gè)模塊導(dǎo)入到 axios 主模塊中,并且在 _request 函數(shù)中進(jìn)行調(diào)用:
import dispatchRequest from './dispatchRequest.js'
/**
*
* @param {string} url
* @param {Object} options
*/
_requiest(url, options) {
// 首先進(jìn)行參數(shù)歸一化,將所有的參數(shù)全部統(tǒng)一為一個(gè)配置對(duì)象
const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
// 調(diào)用 dispatchRequest 方法進(jìn)行請(qǐng)求的發(fā)送和處理,并且將合并之后的配置傳入
dispatchRequest(config)
}
看一下控制臺(tái)我們就可以發(fā)現(xiàn)請(qǐng)求方法被成功調(diào)用了:
接著我們就需要在 dispatchRequest 方法中處理多環(huán)境請(qǐng)求適配的問(wèn)題了
首先我們要明白一個(gè)點(diǎn),和vue3中的渲染器同樣的設(shè)計(jì)理念,axios中的請(qǐng)求器也是允許用戶自定義的,用戶只需要在配置中指定 adapter 配置,傳入一個(gè) () => Promise 類(lèi)型的函數(shù)就可以了 ,因此 mini axios 也需要支持這個(gè)設(shè)計(jì):
用戶只需要將這個(gè)配置替換掉,比如我們可以傳入如下的配置:
那么原本默認(rèn)的適配器就會(huì)被替換:
然后我們將 adapter 配置傳入到 adapate.getAdapter 函數(shù)中:
export default function dispatchRequest(config) {
// 利用適配器獲取當(dāng)前環(huán)境的請(qǐng)求 api
adapate.getAdapter(config.adapter)
}
此時(shí)我們就可以在 adapate.getAdapter 函數(shù)中獲取到最終的適配器的配置了。
因?yàn)閰?shù)歸一的緣故,已經(jīng)被統(tǒng)一為一個(gè)數(shù)組結(jié)構(gòu)了:
我們目前先不測(cè)試自定義適配器的情況,所以我們先將基礎(chǔ)配置復(fù)原:
那么我們拿到的適配器配置就是基礎(chǔ)的配置:
我們先針對(duì)基礎(chǔ)配置進(jìn)行處理,我們先梳理一下適配器中需要處理的一些基礎(chǔ)問(wèn)題:
-
首先需要提供出 xhr, http, fetch 這三種請(qǐng)求方案對(duì)應(yīng)的請(qǐng)求方法。 -
查找當(dāng)前環(huán)境支持的第一個(gè)請(qǐng)求類(lèi)型,使用基于該請(qǐng)求類(lèi)型封裝的請(qǐng)求方法來(lái)作為請(qǐng)求方案。
我們?cè)谶@里就不把所有的請(qǐng)求方案都寫(xiě)出來(lái)了,我們先以 xhr 為例子來(lái)封裝一個(gè)請(qǐng)求模塊:我們新建一個(gè) xhr 模塊,先添加上如下代碼:
// 首先判斷當(dāng)前環(huán)境是否存在 xhr 模塊
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined'
這個(gè)判定就是為了判斷當(dāng)前宿主環(huán)境是否支持 XMLHttpRequest 這個(gè)構(gòu)造函數(shù)
緊接著如果支持的話,那么我們就可以返回一個(gè)Promise函數(shù),在函數(shù)中封裝 xhr 的邏輯:
export default isXHRAdapterSupported && function (config) {
return Promise((resplved, rejected) => {
})
}
至于里面細(xì)枝末節(jié)的內(nèi)容我就不過(guò)多贅述了,大家感興趣可以去查閱代碼,總體上比較簡(jiǎn)單。
封裝好了 xhr 模塊之后,我們就在 adapate 適配器中導(dǎo)入該模塊,并且進(jìn)行簡(jiǎn)單配置:
import xhr from './xhr.js'
// 假如目前的瀏覽器比較古老,只支持 xhr
const adapteConfig = {
xhr,
fetch: undefined,
http: undefined
}
緊接著,我們需要來(lái)編寫(xiě)查找當(dāng)前環(huán)境支持的第一個(gè)請(qǐng)求方法的邏輯:
/**
* 獲取請(qǐng)求適配器的方法
* @param {Function | Function[] | string[]} adapters
*/
getAdapter(adapters) {
// 參數(shù)歸一化
adapters = getAdapteHandlers(adapters)
let handler = null
for (const adapter of adapters) {
handler = adapteConfig[adapter]
if (handler) {
// 當(dāng)前已經(jīng)找到合適的適配器,那么不需要繼續(xù)查找了
break
}
}
return handler
},
至此我們就處理完畢默認(rèn)處理器的情況了,但如果是用戶自定義了處理器呢,我們還需要進(jìn)一步適配這種情況,其實(shí)總體很簡(jiǎn)單:
/**
* 獲取請(qǐng)求適配器的方法
* @param {Function | Function[] | string[]} adapters
*/
getAdapter(adapters) {
// 參數(shù)歸一化
adapters = getAdapteHandlers(adapters)
let handler = null
for (const adapter of adapters) {
if (typeof adapter === 'function') {
// 支持用戶自定義處理器的情況
handler = adapter
} else {
handler = adapteConfig[adapter]
}
if (handler) {
// 當(dāng)前已經(jīng)找到合適的適配器,那么不需要繼續(xù)查找了
break
}
}
return handler
},
至此我們就把 axios 中的適配器的實(shí)現(xiàn)方案的核心邏輯探討完畢了。
4、axios 攔截器實(shí)現(xiàn)方案:
axios 攔截器實(shí)現(xiàn)方案其實(shí)并沒(méi)有太多特別的地方,和大部分開(kāi)源庫(kù)中實(shí)現(xiàn)異步任務(wù)調(diào)度是類(lèi)似的方案,總體上是以下的思路:
-
實(shí)現(xiàn)攔截器的注冊(cè)邏輯:
class InterceptorManager {
constructor() {
this.handlers = [];
}
use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled,
rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : null
})
return this.handlers.length - 1
}
}
export default InterceptorManager
在 Axios 類(lèi)中導(dǎo)入該模塊,初始化攔截器存儲(chǔ)容器
class Axios {
/**
*
* @param {Object} defaultConfig
*/
constructor(defaultConfig) {
this.defaultConfig = defaultConfig;
// 初始化攔截器容器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
}
function createInstance(defaultConfig) {
// 初始化 axios 實(shí)例
const context = new Axios(defaultConfig);
const instance = Axios.prototype.requiest.bind(context);
// 實(shí)例上掛手動(dòng)掛載一個(gè) create 方法
instance.create = function create(instanceConfig) {
// 將用戶傳入的配置和默認(rèn)配置進(jìn)行合并
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
// 獲取到當(dāng)前 axios 對(duì)象上的攔截器
instance.interceptors = context.interceptors;
return instance;
}
核心就是 在 Axios 實(shí)例上掛載了攔截器對(duì)象 。
然后在使用的過(guò)程中注冊(cè)請(qǐng)求和響應(yīng)攔截器
const instance = axios.create({
uri: 'http://localhost:3000/',
// adapter: () => new Promise((resolve, reject) => {
// resolve('自定義內(nèi)容')
// })
})
// console.log('instance', instance.interceptors)
// 添加請(qǐng)求攔截器
instance.interceptors.request.use(
function (config) {
// 在發(fā)送請(qǐng)求之前做些什么,例如添加 token 到請(qǐng)求頭
config.headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`
return config;
},
function (error) {
// 對(duì)請(qǐng)求錯(cuò)誤做些什么
return Promise.reject(error)
}
)
// 添加響應(yīng)攔截器
instance.interceptors.response.use(
function (response) {
// 對(duì)響應(yīng)數(shù)據(jù)做點(diǎn)什么
return response
},
function (error) {
// 對(duì)響應(yīng)錯(cuò)誤做點(diǎn)什么,例如處理 401 錯(cuò)誤
if (error.response && error.response.status === 401) {
// 清除本地存儲(chǔ)的 token,并跳轉(zhuǎn)到登錄頁(yè)面
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
緊接著我們開(kāi)始在 _request 中處理請(qǐng)求和響應(yīng)攔截器的內(nèi)容:我們先將請(qǐng)求和響應(yīng)攔截器打印出來(lái):
/**
*
* @param {string} url
* @param {Object} options
*/
_requiest(url, options) {
// 首先進(jìn)行參數(shù)歸一化,將所有的參數(shù)全部統(tǒng)一為一個(gè)配置對(duì)象
const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
// 開(kāi)始處理請(qǐng)求和響應(yīng)攔截器的內(nèi)容
console.log('獲取到的請(qǐng)求和響應(yīng)攔截器', this.interceptors)
// 調(diào)用 dispatchRequest 方法進(jìn)行請(qǐng)求的發(fā)送和處理,并且將合并之后的配置傳入
dispatchRequest(config)
}
我們可以看到請(qǐng)求和響應(yīng)攔截器注冊(cè)進(jìn)來(lái)了。
-
開(kāi)始進(jìn)行攔截器和請(qǐng)求方法的任務(wù)編排:
所謂任務(wù)編排其實(shí)很簡(jiǎn)單,就是在底層維護(hù)一個(gè)任務(wù)隊(duì)列來(lái)處理一系列任務(wù),隊(duì)列類(lèi)似于下面這樣:
[
請(qǐng)求攔截器1成功方法,
請(qǐng)求攔截器1失敗方法,
請(qǐng)求方法,
undefined,
響應(yīng)攔截器1成功方法,
響應(yīng)攔截器1失敗方法
]
然后從頭到尾循環(huán)這個(gè)隊(duì)列,每一次循環(huán)都取出當(dāng)前隊(duì)列的頭兩位,并且使用 Promise.then 將其注冊(cè)為當(dāng)前任務(wù)階段成功和失敗的處理函數(shù)。
此時(shí)瀏覽器主線程任務(wù)執(zhí)行完畢之后會(huì)依次執(zhí)行 Promise.then 注冊(cè)的微任務(wù)。
理解了思路,代碼實(shí)現(xiàn)就非常簡(jiǎn)單了:
/**
*
* @param {string} url
* @param {Object} options
*/
_requiest(url, options) {
// 首先進(jìn)行參數(shù)歸一化,將所有的參數(shù)全部統(tǒng)一為一個(gè)配置對(duì)象
const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
// 開(kāi)始處理請(qǐng)求和響應(yīng)攔截器的內(nèi)容
console.log('獲取到的請(qǐng)求和響應(yīng)攔截器', this.interceptors)
// 調(diào)用 dispatchRequest 方法進(jìn)行請(qǐng)求的發(fā)送和處理,并且將合并之后的配置傳入
const chain = [dispatchRequest, undefined]
// 開(kāi)始進(jìn)行任務(wù)編排
this.interceptors.request.handlers.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
})
this.interceptors.response.handlers.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected)
})
// 開(kāi)始注冊(cè)異步任務(wù), 注意這里很重要,將 config 配置對(duì)象按照 Promise 鏈?zhǔn)秸{(diào)用的參數(shù)一直傳遞給后續(xù)的任務(wù)去處理
let promise = Promise.resolve(config)
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift())
}
}
調(diào)整 dispatchRequest 函數(shù),調(diào)用適配器發(fā)送請(qǐng)求:
import adapate from './Adapte.js'
/**
*
* axios 統(tǒng)一進(jìn)行 http 請(qǐng)求發(fā)送的模塊
*
* @param {Object} config
*/
export default function dispatchRequest(config) {
// 利用適配器獲取當(dāng)前環(huán)境的請(qǐng)求 api
const adapter = adapate.getAdapter(config.adapter)
return adapter(config)
}
調(diào)整 xhr 適配器,簡(jiǎn)單返回一個(gè)結(jié)果:
// 首先判斷當(dāng)前環(huán)境是否存在 xhr 模塊
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined'
export default isXHRAdapterSupported && function (config) {
return new Promise((resolved, rejected) => {
resolved('請(qǐng)求結(jié)果')
})
}
我們?cè)跍y(cè)試代碼中加入一些測(cè)試log:
instance.interceptors.request.use(
function (config) {
// 在發(fā)送請(qǐng)求之前做些什么,例如添加 token 到請(qǐng)求頭
config.headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`
console.log('請(qǐng)求攔截器', config)
return config;
},
function (error) {
// 對(duì)請(qǐng)求錯(cuò)誤做些什么
return Promise.reject(error)
}
)
// 添加響應(yīng)攔截器
instance.interceptors.response.use(
function (response) {
console.log('響應(yīng)攔截器', response)
// 對(duì)響應(yīng)數(shù)據(jù)做點(diǎn)什么
return response
},
function (error) {
// 對(duì)響應(yīng)錯(cuò)誤做點(diǎn)什么,例如處理 401 錯(cuò)誤
if (error.response && error.response.status === 401) {
// 清除本地存儲(chǔ)的 token,并跳轉(zhuǎn)到登錄頁(yè)面
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
可以看到整個(gè)流程按照預(yù)期的執(zhí)行了
至此我們就搞清楚了 axios 攔截器的設(shè)計(jì)哲學(xué)了。
5、axios 取消請(qǐng)求的實(shí)現(xiàn)方案
axios 取消請(qǐng)求的實(shí)現(xiàn)我們?cè)谶@里就不一行一行的寫(xiě)代碼了,我們直接去源碼中一探究竟:
-
首先我們簡(jiǎn)單梳理一下 axios 的源碼結(jié)構(gòu):
axios 的核心代碼就分模塊存在放 lib 目錄下面:
lib目錄下面幾個(gè)核心文件夾的作用分別是:
adapters:適配器相關(guān)邏輯
cancel: 請(qǐng)求取消相關(guān)功能
core: axios 核心代碼
Axios.js: Axios 核心類(lèi)
dispatchRequest: 請(qǐng)求發(fā)送的核心模塊,和我們手寫(xiě)的核心邏輯類(lèi)似
helpers: 工具方法
-
回顧 axios 請(qǐng)求取消的使用方式(以 ### CancelToken為例,其他方式大家自行查閱):
import axios from 'axios';
const source = axios.CancelToken.source();
axios.get('/api/data', {
cancelToken: source.token
})
.catch(thrown => { if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // handle error } }); // 取消請(qǐng)求
source.cancel('取消請(qǐng)求的原因');
核心其實(shí)就是三步:
-
創(chuàng)建 CancelToken.source 對(duì)象 -
將該對(duì)象中的 token 配置到 cancelToken 上 -
在指定的時(shí)候調(diào)用 source.cancel 方法
我們先來(lái)看一下 CancelToken 這個(gè)的核心邏輯:
class CancelToken {
construcoer(sxsc) {
// 緩存改變 promise 狀態(tài)的方法
let resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
// 注冊(cè)異步微任務(wù)
this.promise.then(cancel => {
// 沒(méi)有訂閱任務(wù),那么直接退出
if (!token._listeners) return;
let i = token._listeners.length;
// 依次執(zhí)行訂閱任務(wù)
while (i-- > 0) {
token._listeners[i](cancel);
}
token._listeners = null;
})
// 注冊(cè)任務(wù)
subscribe(listener) {
if (this.reason) {
listener(this.reason);
return;
}
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
}
// 執(zhí)行回調(diào)方法
executor(function cancel(message, config, request) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new CanceledError(message, config, request);
resolvePromise(token.reason);
});
}
static source() {
let cancel;
const token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token,
cancel
};
}
從以上的核心代碼我們可以看出
-
cancelToken 本質(zhì)上就是一個(gè) CancelToken 的實(shí)例,因此我們可以調(diào)用 CancelToken 原型上的 subscribe 方法來(lái)注冊(cè)一個(gè)異步任務(wù)。 -
CancelToke 內(nèi)部通過(guò) Promise 微任務(wù)的方式來(lái)管理了一個(gè)任務(wù)隊(duì)列,任何通過(guò) subscribe 注冊(cè)的任務(wù)都會(huì)在該 Promise 對(duì)象的狀態(tài)完成之后得到執(zhí)行。 -
cancel 方法做的最核心的一件事情就是 resolvePromise(token.reason) 將上面聊到的 Promise 對(duì)象的狀態(tài)變成已完成,從而將 subscribe 注冊(cè)的任務(wù)全部推入微任務(wù)隊(duì)列去進(jìn)行執(zhí)行。 -
CancelToken 底層實(shí)際上就是實(shí)現(xiàn)了一個(gè)典型的發(fā)布訂閱模式,外部的模塊可以通過(guò) subscribe 方法來(lái)注冊(cè)一系列時(shí)間,然后通過(guò)調(diào)用 cancel 執(zhí)行這些事件。
梳理了 CancelToken 模塊的實(shí)現(xiàn)邏輯,我們?cè)賮?lái)看請(qǐng)求取消的實(shí)現(xiàn)方案就很好理解了, 我們?cè)趧?chuàng)建 axios 請(qǐng)求任務(wù)的時(shí)候傳入了 cancelToken: source.token 實(shí)際上就是把 CancelToken 對(duì)象傳入到了配置中,然后 axios 內(nèi)部會(huì)在某一個(gè)模塊中通過(guò) cancelToken.subscribe 的方法注冊(cè)一個(gè)請(qǐng)求取消的事件,最后我們?cè)谛枰牡胤秸{(diào)用cannal方法觸發(fā)事件的調(diào)用從而取消請(qǐng)求。
實(shí)際上我們查看 http 模塊就可以看到在 http 模塊中 axios 通過(guò) cancelToken.subscrib 注冊(cè)了請(qǐng)求取消事件:
另外如果我們查看 dispatchReques 源碼,我們可以看到,如果我們進(jìn)行了請(qǐng)求取消相關(guān)配置,在請(qǐng)求發(fā)布的每一個(gè)階段,axios都會(huì)調(diào)用 throwIfCancellationRequested 方法來(lái)檢查請(qǐng)求的取消狀態(tài),如果發(fā)現(xiàn)某一個(gè)階段請(qǐng)求和已經(jīng)取消了,那么這個(gè)階段以及以后的額任務(wù)都不會(huì)繼續(xù)執(zhí)行了:
另外我們?cè)跀U(kuò)展一個(gè)點(diǎn),就是在瀏覽環(huán)境,我們可以使用 controller.signal; 方式來(lái)配置請(qǐng)求取消,這樣的話,那么 axios 就真的可以利用這個(gè) api 來(lái)真正的取消指定的 http 請(qǐng)求傳輸并且回收相關(guān)資源了。
至此我們就從提上了解完畢了 axios 中最核心的設(shè)計(jì)原理。
擴(kuò)展:ts 中模板模式實(shí)現(xiàn)通用請(qǐng)求處理的方案:
在企業(yè)中我們目前一般使用 ts 來(lái)編寫(xiě)底層的庫(kù),而 ts 為我們提供了接口和類(lèi)型更加強(qiáng)大的抽象類(lèi),基于這些高級(jí)語(yǔ)法,我們可以使用模板模式更好的實(shí)現(xiàn)跨平臺(tái)的通用請(qǐng)求邏輯。總體實(shí)現(xiàn)方案如下:
比如:
我們現(xiàn)在需要封裝一個(gè)通用的業(yè)務(wù)請(qǐng)求庫(kù),這個(gè)庫(kù)底層的依賴的基礎(chǔ)請(qǐng)求庫(kù)可能是 axios,也可能是傳統(tǒng)的 fetch,也可能是針對(duì) Vue 框架的 VueRequest,也可能是針對(duì)react的 useSWR 等等,我們可能需要根據(jù)不同的業(yè)務(wù)場(chǎng)景進(jìn)行靈活的切換。
但是在切換的過(guò)程中,我們可能希望做到在使用層面無(wú)感,使用層面的 api 統(tǒng)一。
比如在基于 axios 來(lái)進(jìn)行請(qǐng)求的時(shí)候我們這樣用:
import { request } from 'my-request'
request('xxx', {
...xxx
})
在基于 vueRequest 時(shí)候我們也希望可以直接這樣使用:
import { request } from 'my-request'
request('xxx', {
...xxx
})
也就是這個(gè)庫(kù)底層需要對(duì)用戶完全屏蔽各種底層請(qǐng)求庫(kù)存的差異,不管是基于什么樣的底層請(qǐng)求庫(kù),api還是照樣的調(diào)用,代碼還是照樣的寫(xiě)。為了實(shí)現(xiàn)這一點(diǎn),我們就可以這樣去進(jìn)行設(shè)計(jì):
-
暴露一個(gè)標(biāo)準(zhǔn)的 request 方法,不論在怎樣的情況下,發(fā)送請(qǐng)求都是基于這一個(gè)統(tǒng)一的請(qǐng)求方法,并且存在統(tǒng)一側(cè)參數(shù)類(lèi)型。 -
暴露一個(gè)標(biāo)準(zhǔn)的 interface RequestHandler 接口,任何需要注冊(cè)到請(qǐng)求庫(kù)中的模塊都需要按照標(biāo)準(zhǔn)統(tǒng)一的接口進(jìn)行設(shè)計(jì)。 -
按照 RequestHandler 接口的約束來(lái)封裝對(duì)應(yīng)的請(qǐng)求模塊。 -
request上暴露一個(gè)對(duì)外的 use 方法,用戶調(diào)用這個(gè)方法可以將指定的請(qǐng)求模塊進(jìn)行注冊(cè)。 -
用戶需要將默認(rèn)的請(qǐng)求庫(kù) AxiosFetch替換為指定的請(qǐng)求庫(kù)使用指定的 請(qǐng)求庫(kù) VueRequestFetch的時(shí)候,只需要導(dǎo)入基于統(tǒng)一的接口封裝的 VueRequestFetch 模塊,然后調(diào)用 use 函數(shù)將該模塊進(jìn)行注冊(cè)就可以了,其他的業(yè)務(wù)代碼完全不需要替換。
request 庫(kù)最核心的代碼類(lèi)似于下面這樣:
import AxiosFetch from '@/xxx/AxiosFetch'
// 定義接口
export interface RequestHandler {
get(xxx) {
},
post(xxx) {
}
}
let useHandler: RequestHandler = AxiosFetch
export interface RequestOptions {
....
}
export const request(options: RequestOptions) {
// 利用 useHandler 處理請(qǐng)求
useHandler.get()
}
request.use = (handler: RequestHandler) {
// 注冊(cè)
useHandler = handler
}
在使用的時(shí)候極其簡(jiǎn)單
import { request } from '@xxx/core'
import VueRequestFetch from '@xxx/VueRequestFetch'
// 注冊(cè)自定義請(qǐng)求庫(kù)
const { use } = request
use(VueRequestFetch)
request('xxx', {})
request.get('xxx', {})
-
Node 社群
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
最后不要忘了點(diǎn)個(gè)贊再走噢
