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