如何實(shí)現(xiàn)一個(gè)HTTP請求庫——axios源碼閱讀與分析

作者:hjava
原文:https://segmentfault.com/a/1190000015747143
概述
在前端開發(fā)過程中,我們經(jīng)常會(huì)遇到需要發(fā)送異步請求的情況。而使用一個(gè)功能齊全,接口完善的HTTP請求庫,能夠在很大程度上減少我們的開發(fā)成本,提高我們的開發(fā)效率。
axios是一個(gè)在近些年來非常火的一個(gè)HTTP請求庫,目前在GitHub中已經(jīng)擁有了超過40K的star,受到了各位大佬的推薦。
今天,我們就來看下,axios到底是如何設(shè)計(jì)的,其中又有哪些值得我們學(xué)習(xí)的地方。我在寫這邊文章時(shí),axios的版本為0.18.0。我們就以這個(gè)版本的代碼為例,來進(jìn)行具體的源碼閱讀和分析。當(dāng)前axios所有源碼文件都在lib文件夾中,因此我們下文中提到的路徑均是指lib文件夾中的路徑。
本文的主要內(nèi)容有:
如何使用axios axios的核心模塊是如何設(shè)計(jì)與實(shí)現(xiàn)的(請求、攔截器、撤回) axios的設(shè)計(jì)有什么值得借鑒的地方
如何使用axios
想要了解axios的設(shè)計(jì),我們首先需要來看下axios是如何使用的。我們通過一個(gè)簡單示例來介紹以下axios的API。
發(fā)送請求
axios({
??method:'get',
??url:'http://bit.ly/2mTM3nY',
??responseType:'stream'
})
??.then(function(response)?{
??response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});
這是一個(gè)官方的API示例。從上面的代碼中我們可以看到,axios的用法與jQuery的ajax很相似,都是通過返回一個(gè)Promise(也可以通過success的callback,不過建議使用Promise或者await)來繼續(xù)后面的操作。
這個(gè)代碼示例很簡單,我就不過多贅述了,下面讓我們來看下如何添加一個(gè)過濾器函數(shù)。
增加攔截器(Interceptors)函數(shù)
//?增加一個(gè)請求攔截器,注意是2個(gè)函數(shù),一個(gè)處理成功,一個(gè)處理失敗,后面會(huì)說明這種情況的原因
axios.interceptors.request.use(function?(config)?{
????//?請求發(fā)送前處理
????return?config;
??},?function?(error)?{
????//?請求錯(cuò)誤后處理
????return?Promise.reject(error);
??});
//?增加一個(gè)響應(yīng)攔截器
axios.interceptors.response.use(function?(response)?{
????//?針對響應(yīng)數(shù)據(jù)進(jìn)行處理
????return?response;
??},?function?(error)?{
????//?響應(yīng)錯(cuò)誤后處理
????return?Promise.reject(error);
??});
通過上面的示例我們可以知道:在請求發(fā)送前,我們可以針對請求的config參數(shù)進(jìn)行數(shù)據(jù)處理;而在請求響應(yīng)后,我們也能針對返回的數(shù)據(jù)進(jìn)行特定的操作。同時(shí),在請求失敗和響應(yīng)失敗時(shí),我們都可以進(jìn)行特定的錯(cuò)誤處理。
取消HTTP請求
在完成搜索相關(guān)的功能時(shí),我們經(jīng)常會(huì)需要頻繁的發(fā)送請求來進(jìn)行數(shù)據(jù)查詢的情況。通常來說,我們在下一次請求發(fā)送時(shí),就需要取消上一次請求。因此,取消請求相關(guān)的功能也是一個(gè)優(yōu)點(diǎn)。axios取消請求的示例代碼如下:
const?CancelToken?=?axios.CancelToken;
const?source?=?CancelToken.source();
axios.get('/user/12345',?{
??cancelToken:?source.token
}).catch(function(thrown)?{
??if?(axios.isCancel(thrown))?{
????console.log('Request?canceled',?thrown.message);
??}?else?{
????//?handle?error
??}
});
axios.post('/user/12345',?{
??name:?'new?name'
},?{
??cancelToken:?source.token
})
//?cancel?the?request?(the?message?parameter?is?optional)
source.cancel('Operation?canceled?by?the?user.');
通過上面的示例我們可以看到,axios使用的是基于CancelToken的一個(gè)撤回提案。不過,目前該提案已經(jīng)被撤回,具體詳情可以見此處。具體的撤回實(shí)現(xiàn)方法我們會(huì)在后面的章節(jié)源碼分析的時(shí)候進(jìn)行說明。
axios的核心模塊是如何設(shè)計(jì)與實(shí)現(xiàn)的
通過上面的例子,我相信大家對axios的使用方法都有了一個(gè)大致的了解。下面,我們將按照模塊來對axios的設(shè)計(jì)與實(shí)現(xiàn)進(jìn)行分析。下圖是我們在這篇博客中將會(huì)涉及到的相關(guān)的axios的文件,如果讀者有興趣的話,可以通過clone相關(guān)代碼結(jié)合博客進(jìn)行閱讀,這樣能夠加深對相關(guān)模塊的理解。

HTTP請求模塊
作為核心模塊,axios發(fā)送請求相關(guān)的代碼位于core/dispatchReqeust.js文件中。由于篇幅有限,下面我選取部分重點(diǎn)的源碼進(jìn)行簡單的介紹:
module.exports?=?function?dispatchRequest(config)?{
????throwIfCancellationRequested(config);
????//?其他源碼
????//?default?adapter是一個(gè)可以判斷當(dāng)前環(huán)境來選擇使用Node還是XHR進(jìn)行請求發(fā)送的模塊
????var?adapter?=?config.adapter?||?defaults.adapter;?
????return?adapter(config).then(function?onAdapterResolution(response)?{
????????throwIfCancellationRequested(config);
????????//?其他源碼
????????return?response;
????},?function?onAdapterRejection(reason)?{
????????if?(!isCancel(reason))?{
????????????throwIfCancellationRequested(config);
????????????//?其他源碼
????????????return?Promise.reject(reason);
????????});
};
通過上面的代碼和示例我們可以知道,dispatchRequest方法是通過獲取config.adapter來得到發(fā)送請求的模塊的,我們自己也可以通過傳入符合規(guī)范的adapter函數(shù)來替換掉原生的模塊(雖然一般不會(huì)這么做,不過也算是一個(gè)松耦合擴(kuò)展點(diǎn))。
在default.js文件中,我們能夠看到相關(guān)的adapter選擇邏輯,即根據(jù)當(dāng)前容器中特有的一些屬性和構(gòu)造函數(shù)來進(jìn)行判斷。
function?getDefaultAdapter()?{
????var?adapter;
????//?只有Node.js才有變量類型為process的類
????if?(typeof?process?!==?'undefined'?&&?Object.prototype.toString.call(process)?===?'[object?process]')?{
????????//?Node.js請求模塊
????????adapter?=?require('./adapters/http');
????}?else?if?(typeof?XMLHttpRequest?!==?'undefined')?{
????????//?瀏覽器請求模塊
????????adapter?=?require('./adapters/xhr');
????}
????return?adapter;
}
axios中XHR模塊較為簡單,為XMLHTTPRequest對象的封裝,我們在這里就不過多進(jìn)行介紹了,有興趣的同學(xué)可以自行閱讀,代碼位于adapters/xhr.js文件中。
攔截器模塊
了解了dispatchRequest實(shí)現(xiàn)的HTTP請求發(fā)送模塊,我們來看下axios是如何處理請求和響應(yīng)攔截函數(shù)的。讓我們看下axios中請求的統(tǒng)一入口request函數(shù)。
Axios.prototype.request?=?function?request(config)?{
????//?其他代碼
????var?chain?=?[dispatchRequest,?undefined];
????var?promise?=?Promise.resolve(config);
????this.interceptors.request.forEach(function?unshiftRequestInterceptors(interceptor)?{
????????chain.unshift(interceptor.fulfilled,?interceptor.rejected);
????});
????this.interceptors.response.forEach(function?pushResponseInterceptors(interceptor)?{
????????chain.push(interceptor.fulfilled,?interceptor.rejected);
????});
????while?(chain.length)?{
????????promise?=?promise.then(chain.shift(),?chain.shift());
????}
????return?promise;
};
這個(gè)函數(shù)是axios發(fā)送請求的入口,因?yàn)楹瘮?shù)實(shí)現(xiàn)比較長,我就簡單說一下相關(guān)的設(shè)計(jì)思路:
chain是一個(gè)執(zhí)行隊(duì)列。這個(gè)隊(duì)列的初始值,是一個(gè)帶有config參數(shù)的Promise。 在chain執(zhí)行隊(duì)列中,插入了初始的發(fā)送請求的函數(shù) dispatchReqeust和與之對應(yīng)的undefined。后面需要增加一個(gè)undefined是因?yàn)樵赑romise中,需要一個(gè)success和一個(gè)fail的回調(diào)函數(shù),這個(gè)從代碼promise = promise.then(chain.shift(), chain.shift());就能夠看出來。因此,dispatchReqeust和undefined我們可以成為一對函數(shù)。在chain執(zhí)行隊(duì)列中,發(fā)送請求的函數(shù) dispatchReqeust是處于中間的位置。它的前面是請求攔截器,通過unshift方法放入;它的后面是響應(yīng)攔截器,通過push放入。要注意的是,這些函數(shù)都是成對的放入,也就是一次放入兩個(gè)。
通過上面的request代碼,我們大致知道了攔截器的使用方法。接下來,我們來看下如何取消一個(gè)HTTP請求。
取消請求模塊
取消請求相關(guān)的模塊在Cancel/文件夾中。讓我們來看下相關(guān)的重點(diǎn)代碼。
首先,讓我們來看下元數(shù)據(jù)Cancel類。它是用來記錄取消狀態(tài)一個(gè)類,具體代碼如下:
????function?Cancel(message)?{
??????this.message?=?message;
????}
????Cancel.prototype.toString?=?function?toString()?{
??????return?'Cancel'?+?(this.message???':?'?+?this.message?:?'');
????};
????Cancel.prototype.__CANCEL__?=?true;
而在CancelToken類中,它通過傳遞一個(gè)Promise的方法來實(shí)現(xiàn)了HTTP請求取消,然我們看下具體的代碼:
function?CancelToken(executor)?{
????if?(typeof?executor?!==?'function')?{
????????throw?new?TypeError('executor?must?be?a?function.');
????}
????var?resolvePromise;
????this.promise?=?new?Promise(function?promiseExecutor(resolve)?{
????????resolvePromise?=?resolve;
????});
????var?token?=?this;
????executor(function?cancel(message)?{
????????if?(token.reason)?{
????????????//?Cancellation?has?already?been?requested
????????????return;
????????}
????????token.reason?=?new?Cancel(message);
????????resolvePromise(token.reason);
????});
}
CancelToken.source?=?function?source()?{
????var?cancel;
????var?token?=?new?CancelToken(function?executor(c)?{
????????cancel?=?c;
????});
????return?{
????????token:?token,
????????cancel:?cancel
????};
};
而在adapter/xhr.js文件中,有與之相對應(yīng)的取消請求的代碼:
if?(config.cancelToken)?{
????//?等待取消
????config.cancelToken.promise.then(function?onCanceled(cancel)?{
????????if?(!request)?{
????????????return;
????????}
????????request.abort();
????????reject(cancel);
????????//?重置請求
????????request?=?null;
????});
}
結(jié)合上面的取消HTTP請求的示例和這些代碼,我們來簡單說下相關(guān)的實(shí)現(xiàn)邏輯:
在可能需要取消的請求中,我們初始化時(shí)調(diào)用了source方法,這個(gè)方法返回了一個(gè) CancelToken類的實(shí)例A和一個(gè)函數(shù)cancel。在source方法返回實(shí)例A中,初始化了一個(gè)在pending狀態(tài)的promise。我們將整個(gè)實(shí)例A傳遞給axios后,這個(gè)promise被用于做取消請求的觸發(fā)器。 當(dāng)source方法返回的cancel方法被調(diào)用時(shí),實(shí)例A中的promise狀態(tài)由pending變成了fulfilled,立刻觸發(fā)了then的回調(diào)函數(shù),從而觸發(fā)了axios的取消邏輯—— request.abort()。
axios的設(shè)計(jì)有什么值得借鑒的地方
發(fā)送請求函數(shù)的處理邏輯
在之前的章節(jié)中有提到過,axios在處理發(fā)送請求的dispatchRequest函數(shù)時(shí),沒有當(dāng)做一個(gè)特殊的函數(shù)來對待,而是采用一視同仁的方法,將其放在隊(duì)列的中間位置,從而保證了隊(duì)列處理的一致性,提高了代碼的可閱讀性。
Adapter的處理邏輯
在adapter的處理邏輯中,axios沒有把http和xhr兩個(gè)模塊(一個(gè)用于Node.js發(fā)送請求,另一個(gè)則用于瀏覽器端發(fā)送請求)當(dāng)成自身的模塊直接在dispatchRequest中直接飲用,而是通過配置的方法在default.js文件中進(jìn)行默認(rèn)引入。這樣既保證了兩個(gè)模塊間的低耦合性,同時(shí)又能夠?yàn)榻窈笥脩粜枰远x請求發(fā)送模塊保留了余地。
取消HTTP請求的處理邏輯
在取消HTTP請求的邏輯中,axios巧妙的使用了一個(gè)Promise來作為觸發(fā)器,將resolve函數(shù)通過callback中參數(shù)的形式傳遞到了外部。這樣既能夠保證內(nèi)部邏輯的連貫性,也能夠保證在需要進(jìn)行取消請求時(shí),不需要直接進(jìn)行相關(guān)類的示例數(shù)據(jù)改動(dòng),最大程度上避免了侵入其他的模塊。
總結(jié)
本文對axios相關(guān)的使用方式、設(shè)計(jì)思路和實(shí)現(xiàn)方法進(jìn)行了詳細(xì)的介紹。讀者能夠通過上述文章,了解axios的設(shè)計(jì)思想,同時(shí)能夠在axios的代碼中,學(xué)習(xí)到關(guān)于模塊封裝和交互等相關(guān)的經(jīng)驗(yàn)。
由于篇幅原因,本文僅針對axios的核心模塊進(jìn)行了分解和介紹,如果對其他代碼有興趣的同學(xué),可以去GitHub進(jìn)行查看。
如果有任何疑問或者觀點(diǎn),歡迎隨時(shí)留言討論。
