axios是一個基于promise調(diào)用邏輯的http請求庫,是一個優(yōu)秀的開源項目。了解其實現(xiàn)邏輯有助于深化我們對接口往返的理解,提高Promise的應用能力等样勃。本文會挑幾個axios重點且常用的功能模塊進行原理剖析并使用ts實現(xiàn)嘱函,完整代碼在這里蓉驹。本文對每個模塊將按照功能描述,原理剖析搂橙,ts實現(xiàn)的順序進行歉提。
請求參數(shù)與url的處理
axios會對我們傳入的請求參數(shù)做統(tǒng)一處理,當發(fā)送的是get請求時會根據(jù)不同數(shù)據(jù)類型做不同的拼接處理,總結(jié)有以下幾點
-
基本類型 按基礎規(guī)則拼接
axios({ method: 'get', url: '/test/get', params: { a: 1, b: 2, }, }); // http://localhost:8080/simple/get?a=1&b=2
-
數(shù)組 屬性名拼接數(shù)組符號并依次拼接各元素
axios({ method: 'get', url: '/test/get', params: { arr: [1, 2], }, }); // http://localhost:3000/test/get?arr[]=1&arr[]=2
-
對象 encode后拼接
axios({ method: 'get', url: '/test/get', params: { obj: { name: 'foo', }, }, }); // http://localhost:3000/test/get?obj=%7B%22name%22:%22foo%22%7D
-
Date toString后拼接
axios({ method: 'get', url: '/test/get', params: { date: new Date(), }, }); // http://localhost:3000/test/get?date=2021-11-02T07:58:49.323Z
特殊字符不被encode
@
苔巨、:
版扩、$
、,
侄泽、[
礁芦、]
,允許他們存在url中悼尾。忽略空值 類型為null
或者
undefined的值不會被添加到url中柿扣。忽略哈希標記#.
保存已存在的url參數(shù),傳入的參數(shù)會繼續(xù)拼接在已存在的參數(shù)后闺魏。
接下來具體實現(xiàn)未状,命名為buildUrl
// 聲明兩個工具函數(shù) 用于判斷Date類型和object類型
function isDate(val: any) {
return toString.call(val) === '[object Date]'
}
function isPlainObject(val: any) {
return toString.call(val) === '[object Object]'
}
function buildURL(url: string, params?: any): string {
if (!params) {
return url;
}
// 要拼接的參數(shù)數(shù)組
const parts: string[] = [];
Object.keys(params).forEach(key => {
let val = params[key];
if (val === null || typeof val === 'undefined') {
return;
}
let values = [];
if (Array.isArray(val)) {
values = val;
// 屬性名加'[]'標記
key += '[]';
} else {
values = [val];
}
values.forEach(val => {
if (isDate(val)) {
val = val.toISOString();
} else if (isPlainObject(val)) {
val = JSON.stringify(val);
}
parts.push(`${encode(key)}=${encode(val)}`);
});
});
let serializedParams = parts.join('&');
if (serializedParams) {
// 忽略哈希標記
const markIndex = url.indexOf('#');
if (markIndex !== -1) {
url = url.slice(0, markIndex);
}
// 保留原有的參數(shù)
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
}
return url;
}
默認配置與配置合并策略
axios有默認配置(axios.defaults),我們可以修改默認配置且其能與我們傳入axios的配置合并舷胜,下面分析其合并策略娩践。
由于axios的配置的是個復雜對象,因此默認配置和自定義配置的合并也不是簡單的對象合并烹骨。合并的總體原則是翻伺,對于基本類型屬性(method,timeout沮焕,withCredentials吨岭。。峦树。)的合并優(yōu)先使用自定義屬性辣辫,沒有自定義屬性則用默認屬性,無默認屬性則為null(有些屬性沒有默認值如url魁巩、
params急灭、
data,因為這些屬性與當次請求強相關谷遂。設置默認值無意義)葬馋,對于object類型(如headers)屬性則需要深度合并,即要進行遞歸判斷肾扰。
因此畴嘶,我們要為每一個屬性定制其合并策略,接下來按以下步驟具體實現(xiàn):
- 聲明一個對象strats用來存儲各個屬性的合并策略
const strats = Object.create(null);
-
分別定義默認策略和深度合并策略
默認策略
優(yōu)先用自定義屬性 沒有則用默認屬性 否則返會null
const defMerge = (target: any, source: any) => { return typeof source !== 'undefined' ? source : typeof target !== 'undefined' ? target : null; };
深度合并策略
基本類型直接合并集晚,對象類型值則判斷原屬性是否是對象類型窗悯,如果是,則遞歸合并偷拔。不是蒋院,則用一個空對象與之合并亏钩。const deepMerge = (...objs: any[]): any => { const result = Object.create(null) objs.forEach(obj => { if (obj) { Object.keys(obj).forEach(key => { const val = obj[key] if (isPlainObject(val)) { if (isPlainObject(result[key])) { result[key] = deepMerge(result[key], val) } else { result[key] = deepMerge({}, val) } } else { result[key] = val } }) } }) return result }
為每個屬性指定合并策略
由于只有少數(shù)屬性('headers', 'auth')需要深度合并,因此我們只需將需要深度合并的屬性及其合并策略注冊到strats中悦污,在執(zhí)行合并時判斷當前屬性是否在strats中存在即可铸屉,存在則執(zhí)行其專屬的合并策略,不存在則執(zhí)行默認合并策略切端。
// 需要深度合并的屬性
const stratKeysDeepMerge = ['headers', 'auth'];
// 注冊合并策略
stratKeysDeepMerge.forEach(key => {
strats[key] = deepMergeStrat;
});
- 執(zhí)行合并
我們約定config1代表默認配置 config2代表自定義配置彻坛。
首先聲明一個空對象存儲合并結(jié)果,遍歷自定義配置并執(zhí)行對應合并策略踏枣,會優(yōu)先使用strats中的合并策略昌屉,沒有則用默認合并策略。之后遍歷默認配置茵瀑,只有合并結(jié)果中不存在該屬性時再執(zhí)行其合并策略间驮。
function mergeConfig(config1: AxiosRequestConfig, config2?: AxiosRequestConfig): AxiosRequestConfig {
if (!config2) {
config2 = {};
}
const config = Object.create(null);
// 優(yōu)先合并自定義配置
for (let key in config2) {
mergeField(key);
}
// 合并默認配置 只有在沒有自定義配置時才使用默認配置
for (let key in config1) {
if (!config2[key]) {
mergeField(key);
}
}
function mergeField(key: string): void {
// 優(yōu)先自定義配置合并策略 沒有則用默認策略
const strat = strats[key] || defMerge;
config[key] = strat(config1[key], config2![key]);
}
return config;
}
攔截器
axios的攔截器幾乎是項目中必用的一項配置,它可以在請求前/響應后對請求體/響應體做一些處理马昨,先來回顧一下基本用法竞帽。
使用use方法注冊攔截器,使用類似promise.then鸿捧,接收兩個參數(shù)屹篓,第一個用來添加我們期望攔截器處理的邏輯,第二個參數(shù)用來處理錯誤匙奴。
使用eject方法刪除某個攔截器堆巧。
攔截器可以添加多個,執(zhí)行順序是:請求攔截器先添加的后執(zhí)行泼菌,響應攔截器先添加的先執(zhí)行谍肤。
我們知道axios是基于promise實現(xiàn)的,結(jié)合攔截器的執(zhí)行過程其實不難想到可以用promise的鏈式調(diào)用實現(xiàn)哗伯,先來回顧一下鏈式調(diào)用荒揣。簡單來講Promise.then方法會返回一個Promise,可以繼續(xù)調(diào)用then方法焊刹, 前一個then的回調(diào)返回的數(shù)據(jù)會作為參數(shù)傳入下一個then的回調(diào)乳附。因此我們可以將請求/響應攔截器與請求發(fā)送的調(diào)用使用promise.then方法串聯(lián)起來。
基于上述伴澄,首先實現(xiàn)一個攔截器管理類
interface ResolvedFn<T = any> {
(val: T): T | Promise<T>
}
interface RejectedFn {
(error: any): any
}
interface Interceptor<T> {
resolved: ResolvedFn<T>
rejected?: RejectedFn
}
class InterceptorManager<T> {
private interceptors: Array<Interceptor<T> | null>
constructor() {
// 用于存放攔截器
this.interceptors = []
}
// 注冊攔截器,返回其索引可用于刪除
use(resolved: ResolvedFn<T>, rejected?: RejectedFn): number {
this.interceptors.push({
resolved,
rejected
})
return this.interceptors.length - 1
}
// 遍歷所有攔截器阱缓,將每個攔截器作為傳入函數(shù)的參數(shù)并執(zhí)行
// 將來用于將攔截器推入promise的調(diào)用鏈中
forEach(fn: (interceptor: Interceptor<T>) => void): void {
this.interceptors.forEach(interceptor => {
if (interceptor !== null) {
fn(interceptor)
}
})
}
// 刪除攔截器
eject(id: number): void {
if (this.interceptors[id]) {
this.interceptors[id] = null
}
}
}
接下來為Axios類添加interceptors屬性非凌,他有兩個值分別是請求和響應攔截器
export default class Axios {
constructor() {
this.interceptors = {
request: new InterceptorManager<AxiosRequestConfig>(),
response: new InterceptorManager<AxiosResponse>()
}
}
......
}
攔截器攔截的是請求,因此最后需要對發(fā)送請求的方法進行處理荆针。具體如下:
聲明一個chain數(shù)組用于存放promise調(diào)用鏈敞嗡,并首先將請求發(fā)送方法放入颁糟。
分別調(diào)用請求/響應攔截器的foreach方法,將請求攔截器倒序插入數(shù)組前部喉悴,將響應攔截器順序插入數(shù)組尾部棱貌。
聲明一個resolved狀態(tài)的Promise用于啟動鏈式調(diào)用,最后循環(huán)chain數(shù)組箕肃,取出每個攔截器婚脱,使用then方法調(diào)用即可。
request(url: any, config?: any): AxiosPromise {
// 其他邏輯
const chain: PromiseChain[] = [{
resolved: dispatchRequest,
rejected: undefined
}]
// 將請求攔截器倒序插入數(shù)組前部
this.interceptors.request.forEach(interceptor => {
chain.unshift(interceptor)
})
// 將響應攔截器插入數(shù)組尾部
this.interceptors.response.forEach(interceptor => {
chain.push(interceptor)
})
// 初始化一個reslove狀態(tài)的promise
let promise = Promise.resolve(config)
while (chain.length) {
// 鏈式調(diào)用
const { resolved, rejected } = chain.shift()!
promise = promise.then(resolved, rejected)
}
return promise
}
請求取消
請求取消也是axios在項目中的一個常用功能勺像,一個典型場景就是當接口響應慢且會多次觸發(fā)時(如點擊按鈕提交障贸,搜索輸入框等),由于每次響應時間不定吟宦,因此可能出現(xiàn)后發(fā)出的請求比先發(fā)出的請求的響應速度快的情況篮洁,此時就可以使用請求取消,即判斷前一次請求結(jié)果未返回時殃姓,取消當次請求袁波。
回顧下如何使用請求取消,有兩種使用方式:
- 方式一 使用axios.CancelToken的source方法蜗侈,調(diào)用后返回token和cancel兩個屬性篷牌,token用于請求時傳給配置對象中的cancelToken屬性,在請求發(fā)出后宛篇,可以使用cancel方法取消請求娃磺。
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/test/get', {
cancelToken: source.token
})
source.cancel('Operation canceled by the user.');
- 方式二 直接new一個CancelToken的實例賦給配置對象的cancelToken屬性并傳入一個函數(shù),該函數(shù)接收一個處理取消邏輯的函數(shù)參數(shù)叫倍,在函數(shù)體內(nèi)將其賦給在外手動聲明的cancel變量偷卧,通過執(zhí)行cancel函數(shù)取消請求。
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/test/get', {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
})
});
cancel();
接下來分析其實現(xiàn)思路吆倦。
我們知道http請求取消是通過調(diào)用xhr對象的abort方法實現(xiàn)的√睿現(xiàn)在的問題是當想要取消請求時我們往往無法直接訪問xhr對象。要想操作xhr對象我們只有在請求發(fā)送的過程中蚕泽,也即在new XMLHttpRequest()后才能訪問xhr對象晌梨。而要實現(xiàn)請求取消,我們只能事先將請求取消的邏輯寫好但不執(zhí)行须妻,等將來需要取消時再執(zhí)行這段邏輯仔蝌。說到這里就又想到了promise,我們知道promise.then方法中指定的回調(diào)是會等promise的狀態(tài)改變后再執(zhí)行的荒吏,因此實現(xiàn)思路也就有啦敛惊。我們可以聲明一個pending狀態(tài)的promise,在then方法的成功回調(diào)中添加請求取消的邏輯绰更,即xhr.abort瞧挤。等需要取消時锡宋,將該promise的狀態(tài)改變即可。換句話說我們把請求取消的邏輯"寄托"在了一個promise上特恬。那么這個promise從何而來执俩?觀察攔截器的兩種使用方式,其實配置對象中的cancelToken屬性就是那個promise癌刽。
既然有兩種使用方式役首,相應的也就有兩種方式可以得到這個promise,分別是直接實例化axios.CancelToken類以及調(diào)用CancelToken類的source方法得到妒穴。axios得到這個promise后就可以將取消邏輯‘寄托’在其then方法上宋税,接下來看一下實現(xiàn):
export default (config: AxiosRequestConfig): AxiosPromise => {
return new Promise((resolve,reject)=>{
const {
......
cancelToken,// 傳入的promise
} = config
const request = new XMLHttpRequest()
// ...其他邏輯
if (cancelToken) {
cancelToken.promise.then(reason => {
// 調(diào)用取消方法
request.abort()
// 改變Axiospromise狀態(tài)為失敗
reject(reason)
})
}
})
}
接下里分析如何改變該promise的狀態(tài)讼油。觀察第二種使用方式杰赛,executor函數(shù)接收的參數(shù)就是用來處理取消邏輯的函數(shù),即改變promise的狀態(tài)矮台。因此在實現(xiàn)CancelToken類時首先需要聲明一個pending狀態(tài)的promise乏屯,即暫不執(zhí)行resolve函數(shù),而是將其暫存起來(賦給外部變量)瘦赫。之后執(zhí)行executor函數(shù)并傳入一個函數(shù)辰晕,函數(shù)內(nèi)部執(zhí)行暫存的resolve函數(shù)即可改變上述promise的狀態(tài),接下來具體實現(xiàn)
interface ResolvePromise {
(reason?: string): void
}
class CancelToken {
promise: Promise<string>
reason?: string
constructor(executor) {
let resolvePromise: ResolvePromise
// 聲明一個pending狀態(tài)的promise
this.promise = new Promise<string>(resolve => {
resolvePromise = resolve
})
// executor函數(shù)傳入的參數(shù)會被賦值給外部變量cancel用于取消請求
executor(message => {
if (this.reason) {
return
}
this.reason = message
resolvePromise(this.reason)
})
}
}
至此第二種使用方式已經(jīng)實現(xiàn)确虱,再來看第一種含友。容易看出source函數(shù)返回的token就是一個pending狀態(tài)的promise,返回的cancel函數(shù)可直接調(diào)用校辩,無需我們手動聲明窘问,對比一下不難發(fā)現(xiàn)其實這就是相對于第二種方式做了一層封裝,將CancelToken的實例化和取消函數(shù)的處理邏輯放在source方法內(nèi)部實現(xiàn)宜咒,接下來實現(xiàn)source方法惠赫。
class CancelToken {
...
//靜態(tài)方法source 實例化CancelToken 聲明cancel變量 接收取消函數(shù)并返回
static source(): CancelTokenSource {
let cancel
const token = new CancelToken(c => {
cancel = c
})
return {
cancel,
token
}
}
constructor(executor) {...}
}
至此請求取消的主體邏輯已經(jīng)實現(xiàn)。有一點需要特別區(qū)分故黑,即axios.CancelToken和config的cancelToken這兩個屬性儿咱,事實上他們是類與實例的關系。
源碼地址场晶,如有錯誤懇請指正混埠。