概述
在前端開發過程中,我們經常會遇到需要發送異步請求的情況。而使用一個功能齊全,接口完善的HTTP請求庫,能夠在很大程度上減少我們的開發成本,提高我們的開發效率。
axios是一個在近些年來非常火的一個HTTP請求庫,目前在GitHub中已經擁有了超過40K的star,受到了各位大佬的推薦。
今天,我們就來看下,axios到底是如何設計的,其中又有哪些值得我們學習的地方。我在寫這邊文章時,axios的版本為0.18.0。我們就以這個版本的代碼為例,來進行具體的源碼閱讀和分析。當前axios所有源碼文件都在 lib
文件夾中,因此我們下文中提到的路徑均是指 lib
文件夾中的路徑。
本文的主要內容有:
-
如何使用axios
-
axios的核心模塊是如何設計與實現的(請求、攔截器、撤回)
-
axios的設計有什么值得借鑒的地方
如何使用axios
想要了解axios的設計,我們首先需要來看下axios是如何使用的。我們通過一個簡單示例來介紹以下axios的API。
發送請求
1 axios({ 2 3 method:'get', 4 5 url:'http://bit.ly/2mTM3nY', 6 7 responseType:'stream' 8 9 }) 10 11 .then(function(response) { 12 13 response.data.pipe(fs.createWriteStream('ada_lovelace.jpg')) 14 15 });
這是一個官方的API示例。從上面的代碼中我們可以看到,axios的用法與jQuery的ajax很相似,都是通過返回一個Promise(也可以通過success的callback,不過建議使用Promise或者await)來繼續后面的操作。
這個代碼示例很簡單,我就不過多贅述了,下面讓我們來看下如何添加一個過濾器函數。
增加攔截器(Interceptors)函數
1 // 增加一個請求攔截器,注意是2個函數,一個處理成功,一個處理失敗,后面會說明這種情況的原因 2 3 axios.interceptors.request.use(function (config) { 4 5 // 請求發送前處理 6 7 return config; 8 9 }, function (error) { 10 11 // 請求錯誤后處理 12 13 return Promise.reject(error); 14 15 }); 16 17 18 // 增加一個響應攔截器 19 20 axios.interceptors.response.use(function (response) { 21 22 // 針對響應數據進行處理 23 24 return response; 25 26 }, function (error) { 27 28 // 響應錯誤后處理 29 30 return Promise.reject(error); 31 32 });
通過上面的示例我們可以知道:在請求發送前,我們可以針對請求的config參數進行數據處理;而在請求響應后,我們也能針對返回的數據進行特定的操作。同時,在請求失敗和響應失敗時,我們都可以進行特定的錯誤處理。
取消HTTP請求
在完成搜索相關的功能時,我們經常會需要頻繁的發送請求來進行數據查詢的情況。通常來說,我們在下一次請求發送時,就需要取消上一次請求。因此,取消請求相關的功能也是一個優點。axios取消請求的示例代碼如下:
1 const CancelToken = axios.CancelToken; 2 3 const source = CancelToken.source(); 4 5 6 axios.get('/user/12345', { 7 8 cancelToken: source.token 9 10 }).catch(function(thrown) { 11 12 if (axios.isCancel(thrown)) { 13 14 console.log('Request canceled', thrown.message); 15 16 } else { 17 18 // handle error 19 20 } 21 22 }); 23 24 25 axios.post('/user/12345', { 26 27 name: 'new name' 28 29 }, { 30 31 cancelToken: source.token 32 33 }) 34 35 36 // cancel the request (the message parameter is optional) 37 38 source.cancel('Operation canceled by the user.');
通過上面的示例我們可以看到,axios使用的是基於CancelToken的一個撤回提案。不過,目前該提案已經被撤回,具體詳情可以見此處。具體的撤回實現方法我們會在后面的章節源碼分析的時候進行說明。
axios的核心模塊是如何設計與實現的
通過上面的例子,我相信大家對axios的使用方法都有了一個大致的了解。下面,我們將按照模塊來對axios的設計與實現進行分析。下圖是我們在這篇博客中將會涉及到的相關的axios的文件,如果讀者有興趣的話,可以通過clone相關代碼結合博客進行閱讀,這樣能夠加深對相關模塊的理解。
HTTP請求模塊
作為核心模塊,axios發送請求相關的代碼位於 core/dispatchReqeust.js
文件中。由於篇幅有限,下面我選取部分重點的源碼進行簡單的介紹:
1 module.exports = function dispatchRequest(config) { 2 3 throwIfCancellationRequested(config); 4 5 6 // 其他源碼 7 8 9 // default adapter是一個可以判斷當前環境來選擇使用Node還是XHR進行請求發送的模塊 10 11 var adapter = config.adapter || defaults.adapter; 12 13 14 return adapter(config).then(function onAdapterResolution(response) { 15 16 throwIfCancellationRequested(config); 17 18 19 // 其他源碼 20 21 22 return response; 23 24 }, function onAdapterRejection(reason) { 25 26 if (!isCancel(reason)) { 27 28 throwIfCancellationRequested(config); 29 30 31 // 其他源碼 32 33 34 return Promise.reject(reason); 35 36 }); 37 38 };
通過上面的代碼和示例我們可以知道, dispatchRequest
方法是通過獲取 config.adapter
來得到發送請求的模塊的,我們自己也可以通過傳入符合規范的adapter函數來替換掉原生的模塊(雖然一般不會這么做,不過也算是一個松耦合擴展點)。
在 default.js
文件中,我們能夠看到相關的adapter選擇邏輯,即根據當前容器中特有的一些屬性和構造函數來進行判斷。
1 function getDefaultAdapter() { 2 3 var adapter; 4 5 // 只有Node.js才有變量類型為process的類 6 7 if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { 8 9 // Node.js請求模塊 10 11 adapter = require('./adapters/http'); 12 13 } else if (typeof XMLHttpRequest !== 'undefined') { 14 15 // 瀏覽器請求模塊 16 17 adapter = require('./adapters/xhr'); 18 19 } 20 21 return adapter; 22 23 }
axios中XHR模塊較為簡單,為XMLHTTPRequest對象的封裝,我們在這里就不過多進行介紹了,有興趣的同學可以自行閱讀,代碼位於 adapters/xhr.js
文件中。
攔截器模塊
了解了 dispatchRequest
實現的HTTP請求發送模塊,我們來看下axios是如何處理請求和響應攔截函數的。讓我們看下axios中請求的統一入口 request
函數。
1 Axios.prototype.request = function request(config) { 2 3 4 // 其他代碼 5 6 7 var chain = [dispatchRequest, undefined]; 8 9 var promise = Promise.resolve(config); 10 11 12 this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { 13 14 chain.unshift(interceptor.fulfilled, interceptor.rejected); 15 16 }); 17 18 19 this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { 20 21 chain.push(interceptor.fulfilled, interceptor.rejected); 22 23 }); 24 25 26 while (chain.length) { 27 28 promise = promise.then(chain.shift(), chain.shift()); 29 30 } 31 32 33 return promise; 34 35 };
這個函數是axios發送請求的入口,因為函數實現比較長,我就簡單說一下相關的設計思路:
-
chain是一個執行隊列。這個隊列的初始值,是一個帶有config參數的Promise。
-
在chain執行隊列中,插入了初始的發送請求的函數
dispatchReqeust
和與之對應的undefined
。后面需要增加一個undefined
是因為在Promise中,需要一個success和一個fail的回調函數,這個從代碼promise = promise.then(chain.shift(),chain.shift());
就能夠看出來。因此,dispatchReqeust
和undefined
我們可以成為一對函數。 -
在chain執行隊列中,發送請求的函數
dispatchReqeust
是處於中間的位置。它的前面是請求攔截器,通過unshift
方法放入;它的后面是響應攔截器,通過push
放入。要注意的是,這些函數都是成對的放入,也就是一次放入兩個。
通過上面的 request
代碼,我們大致知道了攔截器的使用方法。接下來,我們來看下如何取消一個HTTP請求。
取消請求模塊
取消請求相關的模塊在 Cancel/
文件夾中。讓我們來看下相關的重點代碼。
首先,讓我們來看下元數據 Cancel
類。它是用來記錄取消狀態一個類,具體代碼如下:
1 function Cancel(message) { 2 3 this.message = message; 4 5 } 6 7 8 Cancel.prototype.toString = function toString() { 9 10 return 'Cancel' + (this.message ? ': ' + this.message : ''); 11 12 }; 13 14 15 Cancel.prototype.__CANCEL__ = true;
而在CancelToken類中,它通過傳遞一個Promise的方法來實現了HTTP請求取消,然我們看下具體的代碼:
1 function CancelToken(executor) { 2 3 if (typeof executor !== 'function') { 4 5 throw new TypeError('executor must be a function.'); 6 7 } 8 9 10 var resolvePromise; 11 12 this.promise = new Promise(function promiseExecutor(resolve) { 13 14 resolvePromise = resolve; 15 16 }); 17 18 19 var token = this; 20 21 executor(function cancel(message) { 22 23 if (token.reason) { 24 25 // Cancellation has already been requested 26 27 return; 28 29 } 30 31 32 token.reason = new Cancel(message); 33 34 resolvePromise(token.reason); 35 36 }); 37 38 } 39 40 41 CancelToken.source = function source() { 42 43 var cancel; 44 45 var token = new CancelToken(function executor(c) { 46 47 cancel = c; 48 49 }); 50 51 return { 52 53 token: token, 54 55 cancel: cancel 56 57 }; 58 59 };
而在 adapter/xhr.js
文件中,有與之相對應的取消請求的代碼:
1 if (config.cancelToken) { 2 3 // 等待取消 4 5 config.cancelToken.promise.then(function onCanceled(cancel) { 6 7 if (!request) { 8 9 return; 10 11 } 12 13 14 request.abort(); 15 16 reject(cancel); 17 18 // 重置請求 19 20 request = null; 21 22 }); 23 24 }
結合上面的取消HTTP請求的示例和這些代碼,我們來簡單說下相關的實現邏輯:
-
在可能需要取消的請求中,我們初始化時調用了source方法,這個方法返回了一個
CancelToken
類的實例A和一個函數cancel。 -
在source方法返回實例A中,初始化了一個在pending狀態的promise。我們將整個實例A傳遞給axios后,這個promise被用於做取消請求的觸發器。
-
當source方法返回的cancel方法被調用時,實例A中的promise狀態由pending變成了fulfilled,立刻觸發了then的回調函數,從而觸發了axios的取消邏輯——
request.abort()
。
axios的設計有什么值得借鑒的地方
發送請求函數的處理邏輯
在之前的章節中有提到過,axios在處理發送請求的 dispatchRequest
函數時,沒有當做一個特殊的函數來對待,而是采用一視同仁的方法,將其放在隊列的中間位置,從而保證了隊列處理的一致性,提高了代碼的可閱讀性。
Adapter的處理邏輯
在adapter的處理邏輯中,axios沒有把http和xhr兩個模塊(一個用於Node.js發送請求,另一個則用於瀏覽器端發送請求)當成自身的模塊直接在 dispatchRequest
中直接飲用,而是通過配置的方法在 default.js
文件中進行默認引入。這樣既保證了兩個模塊間的低耦合性,同時又能夠為今后用戶需要自定義請求發送模塊保留了余地。
取消HTTP請求的處理邏輯
在取消HTTP請求的邏輯中,axios巧妙的使用了一個Promise來作為觸發器,將resolve函數通過callback中參數的形式傳遞到了外部。這樣既能夠保證內部邏輯的連貫性,也能夠保證在需要進行取消請求時,不需要直接進行相關類的示例數據改動,最大程度上避免了侵入其他的模塊。
總結
本文對axios相關的使用方式、設計思路和實現方法進行了詳細的介紹。讀者能夠通過上述文章,了解axios的設計思想,同時能夠在axios的代碼中,學習到關於模塊封裝和交互等相關的經驗。
由於篇幅原因,本文僅針對axios的核心模塊進行了分解和介紹,如果對其他代碼有興趣的同學,可以去GitHub進行查看。
如果有任何疑問或者觀點,歡迎隨時留言討論。
作者:hjava
原文:https://segmentfault.com/a/1190000015747143