自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

剛出鍋的 Axios 網(wǎng)絡(luò)請(qǐng)求源碼閱讀筆記,你會(huì)嗎?

開發(fā) 開發(fā)工具
項(xiàng)目中一直都有用到 Axios 作為網(wǎng)絡(luò)請(qǐng)求工具,用它更要懂它,因此為了更好地發(fā)揮 Axios 在項(xiàng)目的價(jià)值,以及日后能夠得心應(yīng)手地使用它,筆者決定從源碼層面好好欣賞一下它的美貌!

[[435862]]

項(xiàng)目中一直都有用到 Axios 作為網(wǎng)絡(luò)請(qǐng)求工具,用它更要懂它,因此為了更好地發(fā)揮 Axios 在項(xiàng)目的價(jià)值,以及日后能夠得心應(yīng)手地使用它,筆者決定從源碼層面好好欣賞一下它的美貌!

Axios是一款基于 Promise 并可用于瀏覽器和 Node.js 的網(wǎng)絡(luò)請(qǐng)求庫。

  • Github:https://github.com/axios/axios
  • NPM:https://www.npmjs.com/package/axios
  • Docs:https://axios-http.com/docs/intro

最近,Axios 官方文檔終于變好看了,支持多語言切換,閱讀更清晰,使用起來也更加舒適!作為一款受全球歡迎的網(wǎng)絡(luò)請(qǐng)求庫,有必要偷學(xué)一下其中的架構(gòu)設(shè)計(jì)、編碼方式。

本篇文章從源碼層面主要分析 Axios 的功能實(shí)現(xiàn)、設(shè)計(jì)模式、以及分享 Axios 中一些筆者認(rèn)為比較“精彩”的地方!

本文主要內(nèi)容結(jié)構(gòu)如下,大家按需食用:

一、Axios 項(xiàng)目概況

本次分析的 Axios 版本是:v0.24.0

通過簡(jiǎn)單的瀏覽 package.json、文件及目錄,可以得知 axios 工程采用了如下三方依賴:

名稱 說明
Grunt[1] JavaScript 任務(wù)運(yùn)行器
dtslint[2] TypeScript 類型聲明&樣式校驗(yàn)工具
TypeScript[3] 支持TS環(huán)境下開發(fā)
Webpack[4] JavaScript 模塊打包工具
karma[5] 測(cè)試用例檢查器
mocha[6] 多功能的 JavaScript 測(cè)試框架
sinojs[7] 提供spies, stub, mock,推薦文章《Sinon 入門,看這篇文章就夠了[8]
follow-redirects[9] http(s)重定向,NodeJS模塊

這里省略了對(duì)一些工具介紹,但可以發(fā)現(xiàn),Axios 開發(fā)項(xiàng)目的主功能依賴并不多,換句話說是只有 follow-redirects作為了“使用依賴”,其他都是編譯、測(cè)試、框架層面的東西,可以看出官方團(tuán)隊(duì)在對(duì)于 Axios 有多么注質(zhì)量和穩(wěn)定性,畢竟是全球都在用的工具。

Axios 中相關(guān)代碼都在 lib/ 目錄下(建議逐行閱讀):

  1. ├── adapters  // 網(wǎng)絡(luò)請(qǐng)求,NodeJS 環(huán)境使用 NodeJS 的 http 模塊,瀏覽器使用 XHR 
  2. │   ├── README.md 
  3. │   ├── http.js  // Node.js 環(huán)境使用 
  4. │   └── xhr.js  // 瀏覽器環(huán)境使用 
  5. ├── helpers  // 一些功能輔助工具函數(shù),看文件名可基本知道干啥的 
  6. │   ├── README.md 
  7. │   ├── bind.js 
  8. │   ├── buildURL.js 
  9. │   ├── combineURLs.js 
  10. │   ├── cookies.js 
  11. │   ├── deprecatedMethod.js 
  12. │   ├── isAbsoluteURL.js 
  13. │   ├── isAxiosError.js 
  14. │   ├── isURLSameOrigin.js 
  15. │   ├── normalizeHeaderName.js 
  16. │   ├── parseHeaders.js 
  17. │   ├── spread.js 
  18. │   └── validator.js 
  19. ├── cancel  // 取消網(wǎng)絡(luò)請(qǐng)求的處理 
  20. │   ├── Cancel.js  // 取消請(qǐng)求 
  21. │   ├── CancelToken.js  // 取消 Token 
  22. │   └── isCancel.js  // 判斷是否取消請(qǐng)求的函數(shù)方法 
  23. ├── core  // 核心功能 
  24. │   ├── Axios.js  // Axios 對(duì)象 
  25. │   ├── InterceptorManager.js  // 攔截器管理 
  26. │   ├── README.md 
  27. │   ├── buildFullPath.js  // 構(gòu)造完成的請(qǐng)求 URL 
  28. │   ├── createError.js  // 創(chuàng)建錯(cuò)誤,拋出異常 
  29. │   ├── dispatchRequest.js  // 請(qǐng)求分發(fā),用于區(qū)分調(diào)用 http 還是 xhr 
  30. │   ├── enhanceError.js  // 增強(qiáng)錯(cuò)誤??????????????? 
  31. │   ├── mergeConfig.js  // 合并配置參數(shù) 
  32. │   ├── settle.js  // 根據(jù)請(qǐng)求響應(yīng)狀態(tài),改變 Promise 狀態(tài) 
  33. │   └── transformData.js  // 數(shù)據(jù)格式轉(zhuǎn)換 
  34. ├── env  // 無關(guān)緊要,沒啥用,與發(fā)包版本有關(guān) 
  35. │   ├── README.md 
  36. │   └── data.js 
  37. ├── defaults.js  // 默認(rèn)參數(shù)/初始化參數(shù)配置 
  38. ├── utils.js  // 提供簡(jiǎn)單的通用的工具函數(shù) 
  39. └── axios.js  // 入口文件,初始化并導(dǎo)出 axios 對(duì)象 

有了一個(gè)簡(jiǎn)單的代碼功能組織架構(gòu)熟悉后,對(duì)于串聯(lián) Axios 的功能很有好處,另外,從上述文件和文件夾的命名,很容易讓人意識(shí)到這是一個(gè)什么功能的文件。

“高內(nèi)聚、低耦合”的真言,在 Axios 中應(yīng)該算是一個(gè)運(yùn)用得很好的例子。

二、Axios 網(wǎng)絡(luò)請(qǐng)求流程圖

梳理了一張 Axios 發(fā)起請(qǐng)求、響應(yīng)請(qǐng)求的執(zhí)行流程圖,希望可以給大家一個(gè)完整流程的概念,便于理解后續(xù)的源碼分析。

Axios 網(wǎng)絡(luò)請(qǐng)求流程圖

三、Axios API 設(shè)計(jì)

我們?cè)谑褂?Axios 的時(shí)候,會(huì)覺得 Axios 的使用特別方便,其原因就是 Axios 中針對(duì)同一功能實(shí)現(xiàn)了不同的 API,便于大家在各種場(chǎng)景下的變通擴(kuò)展使用。

例如,發(fā)起一個(gè) GET 請(qǐng)求的寫法有:

  1. // 第一種 
  2. axios('https://xxx.com/api/userInfo?uid=1'
  3.  
  4. // 第二種 
  5. axios.get('https://xxx.com/api/userInfo?uid=1'
  6.  
  7. // 第三種 
  8. axios({ 
  9.   method: 'GET'
  10.   url: 'https://xxx.com/api/userInfo?uid=1' 
  11. }) 

Axios 請(qǐng)求的核心方法僅兩種:

  1. axios(config) 
  2.  
  3. // or 
  4.  
  5. axios(url[, config]) 

我們知道一個(gè)網(wǎng)絡(luò)請(qǐng)求的方式會(huì)有 GET、POST、PUT、DELETE 等,為了使用更加語義化,Axios 對(duì)外暴露了別名 API:

  1. axios.request(config) 
  2. axios.get(url[, config]) 
  3. axios.delete(url[, config]) 
  4. axios.head(url[, config]) 
  5. axios.options(url[, config]) 
  6.  
  7. axios.post(url[, data[, config]]) 
  8. axios.put(url[, data[, config]]) 
  9. axios.patch(url[, data[, config]]) 

通過遍歷擴(kuò)展axios對(duì)象原型鏈上的方法:

  1. // Provide aliases for supported request methods 
  2. utils.forEach(['delete''get''head''options'], function forEachMethodNoData(method) { 
  3.   /*eslint func-names:0*/ 
  4.   Axios.prototype[method] = function(url, config) { 
  5.     return this.request(mergeConfig(config || {}, { 
  6.       method: method, 
  7.       url: url, 
  8.       data: (config || {}).data 
  9.     })); 
  10.   }; 
  11. }); 
  12.  
  13. utils.forEach(['post''put''patch'], function forEachMethodWithData(method) { 
  14.   /*eslint func-names:0*/ 
  15.   Axios.prototype[method] = function(url, data, config) { 
  16.     return this.request(mergeConfig(config || {}, { 
  17.       method: method, 
  18.       url: url, 
  19.       data: data 
  20.     })); 
  21.   }; 
  22. }); 

能夠如上的直接循環(huán)列表賦值,得益于 Axios 將核心的請(qǐng)求功能單獨(dú)放到了 Axios.prototype.request 方法中,該方法的 TS 定義為:

  1. Axios.request(config: any, ...args: any[]): any 

在其方法(Axios.request())內(nèi)會(huì)對(duì)外部傳參數(shù)類型做判斷,并選擇組裝正確的請(qǐng)求參數(shù):

  1. // 生成規(guī)范的 config,抹平 API(函數(shù)入?yún)ⅲ┎町?nbsp;
  2. if (typeof config === 'string') { 
  3.   // 處理了第一個(gè)參數(shù)是 url 字符串的情況 request(url[, config]) 
  4.   config = arguments[1] || {}; 
  5.   config.url = arguments[0]; 
  6. else { 
  7.   config = config || {}; 
  8.  
  9. // 合并默認(rèn)配置 
  10. config = mergeConfig(this.defaults, config); 
  11.  
  12. // 將請(qǐng)求方法轉(zhuǎn)小寫字母,默認(rèn)為 get 方法 
  13. if (config.method) { 
  14.   config.method = config.method.toLowerCase(); 
  15. else if (this.defaults.method) { 
  16.   config.method = this.defaults.method.toLowerCase(); 
  17. else { 
  18.   config.method = 'get'

以此來抹平了各種類型請(qǐng)求以及所需傳入?yún)?shù)之間的差異性!

四、Axios 工廠模式創(chuàng)建實(shí)例

默認(rèn) Axios 導(dǎo)出了一個(gè)單例,導(dǎo)出了一個(gè)實(shí)例化后的單例,所以我們可以直接引入后就可以調(diào)用 Axios 的方法。

在某些場(chǎng)景下,我們的項(xiàng)目中可能對(duì)接了多個(gè)業(yè)務(wù)方,那么請(qǐng)求中的 base URL 就不一樣,因此有沒有辦法創(chuàng)建多個(gè) Axios 實(shí)例?

那就是使用 axios.create([config]) 方法創(chuàng)建多個(gè)實(shí)例。

考慮到多實(shí)例這樣的實(shí)際需求,Axios 對(duì)外暴露了 create() 方法,在 Axios 內(nèi)部中,往導(dǎo)出的 axios 實(shí)例上綁定了用于創(chuàng)建本身實(shí)例的工廠方法:

  1. /** 
  2.  * Create an instance of Axios 
  3.  * 
  4.  * @param {Object} defaultConfig The default config for the instance 
  5.  * @return {Axios} A new instance of Axios 
  6.  */ 
  7. function createInstance(defaultConfig) { 
  8.   var context = new Axios(defaultConfig); 
  9.   var instance = bind(Axios.prototype.request, context); 
  10.  
  11.   // Copy axios.prototype to instance 
  12.   utils.extend(instance, Axios.prototype, context); 
  13.  
  14.   // Copy context to instance 
  15.   utils.extend(instance, context); 
  16.  
  17.   // Factory for creating new instances 
  18.   instance.create = function create(instanceConfig) { 
  19.     return createInstance(mergeConfig(defaultConfig, instanceConfig)); 
  20.   }; 
  21.  
  22.   return instance; 

這里的實(shí)現(xiàn)值得一說的地方在于:

  1. instance.create = function create(instanceConfig) { 
  2.  
  3. return createInstance(mergeConfig(defaultConfig, instanceConfig)); 
  4.  
  5. }; 

在創(chuàng)建 axios 實(shí)例的工廠方法內(nèi),綁定工廠方法到實(shí)例的 create 屬性上。為什么不是在工廠方法外綁定吶?這是我們可能的習(xí)慣做法,Axios 之前確實(shí)也是這么做的。

為什么挪到了內(nèi)部?可以看看這條 PR: Allow axios.create(options) to be used recursively[10]

原因簡(jiǎn)單來說就是,用戶自己創(chuàng)建的實(shí)例依然可以調(diào)用 create 方法創(chuàng)建新的實(shí)例,例如:

  1. const axios = require('axios'); 
  2.  
  3. const jsonClient = axios.create({ 
  4.   responseType: 'json' // 該項(xiàng)配置可以在后續(xù)創(chuàng)建的實(shí)例中復(fù)用,而不必重復(fù)編碼 
  5. }); 
  6.  
  7. const serviceOne = jsonClient.create({ 
  8.   baseURL: 'https://service.one/' 
  9. }); 
  10.  
  11. const serviceTwo = jsonClient.create({ 
  12.   baseURL: 'https://service.two/' 
  13. }); 

這樣有助于復(fù)用實(shí)例的公共參數(shù)復(fù)用,減少重復(fù)編碼。

五、網(wǎng)絡(luò)請(qǐng)求適配器

在文件 ./defaults.js 中生成了默認(rèn)完整的 Request Config 參數(shù)。

其中 config.adapter 字段表明當(dāng)前應(yīng)該使用 ./adapters/目錄下的 http.js 還是 xhr.js 模塊。

  1. // 根據(jù)當(dāng)前使用環(huán)境,選擇使用的網(wǎng)絡(luò)請(qǐng)求適配器 
  2. function getDefaultAdapter() { 
  3.   var adapter; 
  4.   if (typeof XMLHttpRequest !== 'undefined') { 
  5.     // For browsers use XHR adapter 
  6.     adapter = require('./adapters/xhr'); 
  7.   } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { 
  8.     // For node use HTTP adapter 
  9.     adapter = require('./adapters/http'); 
  10.   } 
  11.   return adapter; 

這里使用了設(shè)計(jì)模式中的適配器模式,通過判斷不同環(huán)境下是否支持方法的方式,選擇正確的網(wǎng)絡(luò)請(qǐng)求模塊,便可以實(shí)現(xiàn)官網(wǎng)所說的支持 NodeJS 和瀏覽器環(huán)境。

六、轉(zhuǎn)換請(qǐng)求體和響應(yīng)體數(shù)據(jù)

這是 Axios 貼在官網(wǎng)的核心功能之一,且提到了可以自動(dòng)轉(zhuǎn)換響應(yīng)體內(nèi)容為 JSON 數(shù)據(jù)。

默認(rèn)請(qǐng)求配置中初始化的請(qǐng)求/響應(yīng)轉(zhuǎn)換器數(shù)組。

自動(dòng)嘗試轉(zhuǎn)換響應(yīng)數(shù)據(jù)為 JSON 格式

transformRequest 和 transformResponse 字段是一個(gè)數(shù)組類型,因此我們還可以向其中增加自定義的轉(zhuǎn)換器。

一般來講我們只會(huì)通過復(fù)寫 transitional 字段來控制響應(yīng)數(shù)據(jù)的轉(zhuǎn)換與否,但可以作為擴(kuò)展 Axios 的一個(gè)點(diǎn),留了口子,這一點(diǎn)考慮得也很到位。

七、請(qǐng)求攔截器&響應(yīng)攔截器

可以通過攔截器來提前處理請(qǐng)求前和收到響應(yīng)前的一些處理方法。

7.1 攔截器的使用

攔截器用于在 .then() 和 .catch() 前注入并執(zhí)行的一些方法。

  1. // 通過 use 方法,添加一個(gè)請(qǐng)求攔截器 
  2. axios.interceptors.request.use(function (config) { 
  3.     // 在發(fā)送請(qǐng)求前干點(diǎn)啥,.then() 處理之前,比如修改 request config 
  4.     return config; 
  5.   }, function (error) { 
  6.     // 在發(fā)起請(qǐng)求發(fā)生錯(cuò)誤后,.catch() 處理之前干點(diǎn)啥 
  7.     return Promise.reject(error); 
  8.   }); 
  9.  
  10. // 通過 use 方法,添加一個(gè)響應(yīng)攔截器 
  11. axios.interceptors.response.use(function (response) { 
  12.     // 只要響應(yīng)網(wǎng)絡(luò)狀態(tài)碼是 2xx 的都會(huì)觸發(fā) 
  13.     // 干點(diǎn)啥 
  14.     return response; 
  15.   }, function (error) { 
  16.     // 狀態(tài)碼不是 2xx 的會(huì)觸發(fā) 
  17.     // 發(fā)生錯(cuò)誤了,干點(diǎn)啥 
  18.     return Promise.reject(error); 
  19.   }); 

7.2 攔截管理器

Axios 將請(qǐng)求和響應(yīng)的過程包裝成了 Promise,那么 Axios 是如何實(shí)現(xiàn)攔截器在 .then() 和 .catch() 執(zhí)行前執(zhí)行吶?

可以很容易猜到通過組裝一條 Promise 執(zhí)行鏈即可!

來看看 Axios 在請(qǐng)求函數(shù)中如何實(shí)現(xiàn):

首先是 Axios 對(duì)象中初始化了 攔截管理器:

  1. function Axios(instanceConfig) { 
  2.   this.defaults = instanceConfig; 
  3.   this.interceptors = { 
  4.     request: new InterceptorManager(), 
  5.     response: new InterceptorManager() 
  6.   }; 

來到 ./lib/core/InterceptorManager.js 文件下,對(duì)于攔截管理器:

  1. // 攔截管理器對(duì)象 
  2. function InterceptorManager() { 
  3.   this.handlers = []; 
  4.  
  5. /** 
  6.  * 添加新的管理器,定義了 use 方法 
  7.  * 
  8.  * @param {Function} fulfilled 處理 `Promise` 執(zhí)行 `then` 的函數(shù)方法 
  9.  * @param {Function} rejected 處理 `Promise` 執(zhí)行 `reject` 的函數(shù)方法 
  10.  * 
  11.  * @return {Number} 返回一個(gè) ID 值用于移除攔截器 
  12.  */ 
  13. InterceptorManager.prototype.use = function use(fulfilled, rejected, options) { 
  14.   this.handlers.push({ 
  15.     fulfilled: fulfilled, 
  16.     rejected: rejected, 
  17.     // 默認(rèn)不同步 
  18.     synchronous: options ? options.synchronous : false
  19.     // 定義是否執(zhí)行當(dāng)前攔截器的函數(shù)或布爾值 
  20.     runWhen: options ? options.runWhen : null  
  21.   }); 
  22.   return this.handlers.length - 1; // ID 值實(shí)際就是當(dāng)前攔截器的數(shù)組索引 
  23. }; 
  24.  
  25. /** 
  26.  * 從棧中移除指定 id 的攔截器 
  27.  * 
  28.  * @param {Number} id use 方法返回的 id 值 
  29.  */ 
  30. InterceptorManager.prototype.eject = function eject(id) { 
  31.   if (this.handlers[id]) { 
  32.     this.handlers[id] = null; // 刪除攔截器,但索引會(huì)保留 
  33.   } 
  34. }; 
  35.  
  36. /** 
  37.  * 迭代所有注冊(cè)的攔截器 
  38.  * 該方法會(huì)跳過因攔截器被刪除而值為 null 的索引 
  39.  * 
  40.  * @param {Function} 調(diào)用每個(gè)有效攔截器的函數(shù) 
  41.  */ 
  42. InterceptorManager.prototype.forEach = function forEach(fn) { 
  43.   utils.forEach(this.handlers, function forEachHandler(h) { 
  44.     if (h !== null) { 
  45.       fn(h); 
  46.     } 
  47.   }); 
  48. }; 

迭代所有注冊(cè)的攔截器是一個(gè) FIFS(first come first served,先到先服務(wù))隊(duì)列執(zhí)行順序的方法。

7.3 組裝攔截器與請(qǐng)求執(zhí)行鏈

在 ./lib/core/Axios.js 文件中,Axios 對(duì)象定義了 request 方法,其中將網(wǎng)絡(luò)請(qǐng)求、請(qǐng)求攔截器和響應(yīng)攔截器組裝。

默認(rèn)返回一個(gè)還未執(zhí)行網(wǎng)絡(luò)請(qǐng)求的 Promise 執(zhí)行鏈,如果設(shè)置了同步,則會(huì)立即執(zhí)行請(qǐng)求過程,并返回請(qǐng)求結(jié)果的 Promise 對(duì)象,也就是官方文檔中提到的 Axios 還支持 Promise API。

函數(shù)詳細(xì)的分析,都已經(jīng)注釋在如下代碼中:

  1. /** 
  2.  * Dispatch a request 
  3.  * 
  4.  * @param {Object} config 傳入的用戶自定義配置,并和默認(rèn)配置 merge 
  5.  */ 
  6. Axios.prototype.request = function request(config) { 
  7.   // 省略 ... 
  8.  
  9.   // 請(qǐng)求攔截器執(zhí)行鏈 
  10.   var requestInterceptorChain = []; 
  11.   // 同步請(qǐng)求攔截器 
  12.   var synchronousRequestInterceptors = true
  13.   // 遍歷請(qǐng)求攔截器 
  14.   this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { 
  15.     // 判斷 runWhen 如果是函數(shù),則執(zhí)行函數(shù),結(jié)果若為 false,則不執(zhí)行當(dāng)前攔截器 
  16.     if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) { 
  17.       return
  18.     } 
  19.     // 判斷當(dāng)前攔截器是否同步 
  20.     synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; 
  21.     // 插入 requestInterceptorChain 數(shù)組首位 
  22.     // 效果:[interceptor.fulfilled, interceptor.rejected, ...] 
  23.     requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); 
  24.   }); 
  25.  
  26.   // 響應(yīng)攔截器執(zhí)行鏈 
  27.   var responseInterceptorChain = []; 
  28.   // 遍歷所有的響應(yīng)攔截器 
  29.   this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { 
  30.     // 插入 responseInterceptorChain 尾部 
  31.     // 效果:[ ..., interceptor.fulfilled, interceptor.rejected] 
  32.     responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); 
  33.   }); 
  34.  
  35.   var promise; 
  36.  
  37.   // 如果非同步 
  38.   // 一般大家在使用 axios.interceptors.request.use 都沒有傳遞第三個(gè)配置參數(shù) 
  39.   // 所以一般情況下會(huì)走這個(gè)邏輯 
  40.   if (!synchronousRequestInterceptors) { 
  41.     var chain = [dispatchRequest, undefined]; 
  42.     // 將請(qǐng)求攔截器執(zhí)行鏈放到 chain 數(shù)組頭部 
  43.     Array.prototype.unshift.apply(chain, requestInterceptorChain); 
  44.     // 將響應(yīng)攔截器執(zhí)行鏈放到 chain 數(shù)組末尾 
  45.     chain = chain.concat(responseInterceptorChain); 
  46.     // 給 promise 賦值 Promise 對(duì)象,并注入 request config 
  47.     promise = Promise.resolve(config); 
  48.     // 循環(huán) chain 數(shù)組,組合成 Promise 執(zhí)行鏈 
  49.     while (chain.length) { 
  50.       // 正好 resolve 和 reject 對(duì)應(yīng)方法,兩兩一組 
  51.       promise = promise.then(chain.shift(), chain.shift()); 
  52.     } 
  53.     // 返回 Promise 執(zhí)行鏈 
  54.     return promise; 
  55.   } 
  56.  
  57.   // 同步方式 
  58.   var newConfig = config; 
  59.   // 循環(huán)并執(zhí)行所有請(qǐng)求攔截器 
  60.   while (requestInterceptorChain.length) { 
  61.     var onFulfilled = requestInterceptorChain.shift(); 
  62.     var onRejected = requestInterceptorChain.shift(); 
  63.     try { 
  64.       // 執(zhí)行定義請(qǐng)求前的“請(qǐng)求攔截器” then 處理方法 
  65.       newConfig = onFulfilled(newConfig); 
  66.     } catch (error) { 
  67.       // 執(zhí)行定義請(qǐng)求前的“請(qǐng)求攔截器” catch 處理方法 
  68.       onRejected(error); 
  69.       break; 
  70.     } 
  71.   } 
  72.  
  73.   try { 
  74.     // 執(zhí)行網(wǎng)絡(luò)請(qǐng)求 
  75.     promise = dispatchRequest(newConfig); 
  76.   } catch (error) { 
  77.     return Promise.reject(error); 
  78.   } 
  79.  
  80.   // 循環(huán)并執(zhí)行所有響應(yīng)攔截器 
  81.   while (responseInterceptorChain.length) { 
  82.     promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift()); 
  83.   } 
  84.   // 返回 Promise 對(duì)象 
  85.   return promise; 
  86. }; 

可以看到由于請(qǐng)求攔截器和響應(yīng)攔截器使用了 unshift 和 push,那么 use 攔截器的先后順序就有變動(dòng)。

通過如上代碼的分析,可以得知若有多個(gè)攔截器的執(zhí)行順序規(guī)則是:

  • 請(qǐng)求攔截器:先 use,后執(zhí)行
  • 響應(yīng)攔截器:先 use,先執(zhí)行

關(guān)于攔截器執(zhí)行這部分,涉及到一個(gè) PR改動(dòng): Requests unexpectedly delayed due to an axios internal promise[11],推薦大家閱讀一下,有助于熟悉微任務(wù)和宏任務(wù)。

改動(dòng)的原因:如果請(qǐng)求攔截器中存在一些長(zhǎng)時(shí)間的任務(wù),會(huì)使得使用 axios 的網(wǎng)絡(luò)請(qǐng)相較于不使用 axios 的網(wǎng)絡(luò)請(qǐng)求會(huì)延后,為此,通過為攔截管理器增加 synchronous 和 runWhen 字段,來實(shí)現(xiàn)同步執(zhí)行請(qǐng)求方法。

八、取消網(wǎng)絡(luò)請(qǐng)求

在網(wǎng)絡(luò)請(qǐng)求中,會(huì)遇到許多非預(yù)期的請(qǐng)求取消,當(dāng)然也有主動(dòng)取消請(qǐng)求的時(shí)候,例如,用戶獲取 id=1 的新聞數(shù)據(jù),需要耗時(shí) 30s,用戶等不及了,就返回查看 id=2 的新聞詳情,此時(shí)我們可以在代碼中主動(dòng)取消 id=1 的網(wǎng)絡(luò)請(qǐng)求,節(jié)省網(wǎng)絡(luò)資源。

8.1 如何取消 Axios 請(qǐng)求

通過 CancleToken.source() 工廠方法創(chuàng)建取消請(qǐng)求的實(shí)例 source

在發(fā)起請(qǐng)求的 request Config 中設(shè)置 cancelToken 值為 source.token

在需要主動(dòng)取消請(qǐng)求的地方調(diào)用:source.cancle()

  1. const CancelToken = axios.CancelToken; 
  2. const source = CancelToken.source(); 
  3.  
  4. axios.get('/user/12345', { 
  5.   cancelToken: source.token 
  6. }).catch(function (thrown) { 
  7.   if (axios.isCancel(thrown)) { 
  8.     console.log('Request canceled', thrown.message); 
  9.   } else { 
  10.     // handle error 
  11.   } 
  12. }); 
  13.  
  14. axios.post('/user/12345', { 
  15.   name'new name' 
  16. }, { 
  17.   cancelToken: source.token 
  18. }) 
  19.  
  20. // 主動(dòng)取消請(qǐng)求 (提示信息是可選的參數(shù)) 
  21. source.cancel('Operation canceled by the user.'); 

同一個(gè) source 實(shí)例調(diào)用取消 cancle() 方法時(shí),會(huì)取消所有含有當(dāng)前實(shí)例 source.token 的請(qǐng)求。

8.2 取消請(qǐng)求功能的原理

想必大家也很好奇是怎么實(shí)現(xiàn)取消網(wǎng)絡(luò)請(qǐng)求功能的,實(shí)際上有了上述的基礎(chǔ),把 Axios 的請(qǐng)求想象成為一條事件執(zhí)行鏈,執(zhí)行鏈中任意一處發(fā)生了異常,都會(huì)中斷整個(gè)請(qǐng)求。

整個(gè)請(qǐng)求執(zhí)行鏈中的設(shè)計(jì)了,首先來看:axios.CancelToken.source():

  1. /** 
  2.  * Returns an object that contains a new `CancelToken` and a function that, when called, 
  3.  * cancels the `CancelToken`. 
  4.  */ 
  5. CancelToken.source = function source() { 
  6.   var cancel; 
  7.   var token = new CancelToken(function executor(c) { 
  8.     cancel = c; 
  9.   }); 
  10.   return { 
  11.     token: token, 
  12.     cancel: cancel 
  13.   }; 
  14. }; 

該工廠方法返回了一個(gè)對(duì)象,該對(duì)象包含了一個(gè) token(取消令牌,CancleToken 對(duì)象的實(shí)例),以及一個(gè)取消與 token 映射綁定的取消請(qǐng)求方法 cancle()。

其中 new CancelToken() 會(huì)創(chuàng)建 CancleToken 的單例,通過傳入函數(shù)方式,拿到了取消請(qǐng)求的回調(diào)函數(shù),該函數(shù)內(nèi)會(huì)構(gòu)造 token 取消的原因,并通過執(zhí)行 resolvePromise(),主動(dòng) reslove。

同樣是一個(gè)微任務(wù),當(dāng)主動(dòng)調(diào)用 cancle() 方法后,會(huì)調(diào)用 resolvePromise(reason),此時(shí)就會(huì)給當(dāng)前 cancleToken 實(shí)例的 reason 字段賦值“請(qǐng)求取消的原因”:

  1. function CancelToken(executor) { 
  2.   if (typeof executor !== 'function') { 
  3.     throw new TypeError('executor must be a function.'); 
  4.   } 
  5.  
  6.   // 初始化一個(gè) promise 屬性,resolvePromise 變量指向 resolve 
  7.   var resolvePromise; 
  8.   this.promise = new Promise(function promiseExecutor(resolve) { 
  9.     resolvePromise = resolve; 
  10.   }); 
  11.  
  12.   // 賦值 token 為當(dāng)前對(duì)象的實(shí)例 
  13.   var token = this; 
  14.  
  15.   // 省略... 
  16.  
  17.   // 執(zhí)行外部傳入的初始化方法,將取消請(qǐng)求的方法,賦值給返回對(duì)象的 cancel 屬性 
  18.   executor(function cancel(message) { 
  19.     if (token.reason) { 
  20.       // Cancellation has already been requested 
  21.       return
  22.     } 
  23.  
  24.     token.reason = new Cancel(message); 
  25.     resolvePromise(token.reason); 
  26.   }); 

在 ./lib/core/dispatchRequest.js 文件中:

  1. function throwIfCancellationRequested(config) { 
  2.   // 當(dāng) request config 中有實(shí)例化 cancelToken 時(shí) 
  3.   // 執(zhí)行 throwIfRequested() 方法 
  4.   // throwIfRequested() 方法在 cancleToken 實(shí)例的 reason 字段有值時(shí) 
  5.   // 拋出異常 
  6.   if (config.cancelToken) { 
  7.     config.cancelToken.throwIfRequested(); 
  8.   } 
  9.   // 判斷 config.signal.aborted 值為真的時(shí)候拋出異常 
  10.   // 該值時(shí)通過 new AbortController().signal,不過目前暫時(shí)未用到 
  11.   // 官方文檔上暫也暫未更新相關(guān)內(nèi)容 
  12.   if (config.signal && config.signal.aborted) { 
  13.     throw new Cancel('canceled'); 
  14.   } 
  15.  
  16. module.exports = function dispatchRequest(config) { 
  17.   // 準(zhǔn)備發(fā)起請(qǐng)求前檢查 
  18.   throwIfCancellationRequested(config); 
  19.    
  20.   // 省略... 
  21.    
  22.   var adapter = config.adapter || defaults.adapter; 
  23.   return adapter(config).then(function onAdapterResolution(response) { 
  24.     // 請(qǐng)求成功后檢查 
  25.     throwIfCancellationRequested(config); 
  26.     // 省略... 
  27.     return response; 
  28.   }, function onAdapterRejection(reason) { 
  29.     if (!isCancel(reason)) { 
  30.       // 請(qǐng)求發(fā)生錯(cuò)誤時(shí)候檢查 
  31.       throwIfCancellationRequested(config); 
  32.       // 省略... 
  33.     } 
  34.     // 省略... 
  35.  
  36.     return Promise.reject(reason); 
  37.   }); 

在文章前邊分析攔截器的時(shí)候講到了 dispatchRequest() 在請(qǐng)求攔截器之后執(zhí)行。

在請(qǐng)求前,請(qǐng)求成功、失敗后三個(gè)時(shí)機(jī)點(diǎn),都會(huì)通過 throwIfCancellationRequested() 函數(shù)檢查是否取消了請(qǐng)求,throwIfCancellationRequested() 函數(shù)判斷了 cancleToken.reason 是否有值,如果有則拋出異常并中斷請(qǐng)求 Promise 執(zhí)行鏈。

九、CSRF 防御

Axios 支持防御 CSRF(Cross-site request forgery,跨站請(qǐng)求偽造)攻擊,而防御 CSRF 攻擊的最簡(jiǎn)單方式就是加 Token。

CSRF 的攻擊可以簡(jiǎn)述為:服務(wù)器錯(cuò)把攻擊者的請(qǐng)求當(dāng)成了正常用戶的請(qǐng)求。

加一個(gè) Token 為什么就能解決吶?首先 Token 是服務(wù)端隨用戶每次請(qǐng)求動(dòng)態(tài)生成下發(fā)的,用戶在提交表單、查詢數(shù)據(jù)等行為的時(shí)候,需要在網(wǎng)絡(luò)請(qǐng)求體加上這個(gè)臨時(shí)性的 Token 值,攻擊者無法在三方網(wǎng)站中獲取當(dāng)前 Token,因此服務(wù)端就可以通過驗(yàn)證 Token 來區(qū)分是否是正常用戶的請(qǐng)求。

Axios 在請(qǐng)求配置中提供了兩個(gè)字段:

  1. // cookie 中攜帶的 Token 名稱,通過該名稱可以從 cookie 中拿到 Token 值 
  2. xsrfCookieName: 'XSRF-TOKEN'
  3. // 請(qǐng)求 Header 中攜帶的 Token 名稱,通過該成名可從 Header 中拿到 Token 值 
  4. xsrfHeaderName: 'X-XSRF-TOKEN'

用于附加驗(yàn)證防御 CSRF 攻擊的 Token。

十、值得一說的自定義工具庫

在 Axios 內(nèi),沒有引入其他例如 lodash 的工具函數(shù)依賴,都在自己內(nèi)部按需實(shí)現(xiàn)了工具函數(shù),提供給整個(gè)項(xiàng)目使用。

個(gè)人非常喜歡這種做法,尤其是在一個(gè) ES5 的工具庫下,這樣做不僅代碼易讀,與此同時(shí)還顯得非常得純粹、干凈、清晰!

如果團(tuán)隊(duì)內(nèi)有這種訴求,建議可以寫一個(gè) ESM 模塊的工具庫,這樣做以后,在打包 Tree Shaking 時(shí),打包的結(jié)果應(yīng)該能更加干凈。

總結(jié)

總體來說,Axios 涉及到的設(shè)計(jì)模式就有:?jiǎn)卫J健⒐S模式、職責(zé)鏈模式、適配器模式,因此絕對(duì)是值得學(xué)習(xí)的一個(gè)工具庫,梳理之后不僅利于我們靈活使用其 API,更有助于根據(jù)業(yè)務(wù)去自定義擴(kuò)展封裝網(wǎng)絡(luò)請(qǐng)求,將網(wǎng)絡(luò)請(qǐng)求統(tǒng)一收口。 

與此同時(shí),Axios 絕對(duì)是一個(gè)可以作為軟件工程編碼的學(xué)習(xí)范本,其中的文件夾結(jié)構(gòu),功能設(shè)計(jì),功能解耦,按需封裝工具類,以及靈活運(yùn)用設(shè)計(jì)模式都是值得揣度回味。

責(zé)任編輯:武曉燕 來源: DYBOY
相關(guān)推薦

2021-11-22 16:12:34

Axios Axios-Retry前端

2013-12-24 10:05:04

memcached

2018-07-30 16:31:00

javascriptaxioshttp

2018-12-29 14:14:32

2019-05-07 15:49:27

AI人工智能藝術(shù)

2010-07-13 10:40:30

唐駿

2021-08-19 15:36:09

數(shù)據(jù)備份存儲(chǔ)備份策略

2014-07-03 15:40:09

Apache Spar

2024-03-29 12:50:00

項(xiàng)目分層模型

2021-02-15 14:48:31

Hive語法sql

2022-03-15 08:36:46

遞歸查詢SQL

2024-02-22 08:31:26

數(shù)據(jù)恢復(fù)工具MySQL回滾SQL

2021-04-14 06:53:52

C# 修飾符 Public

2021-04-16 15:02:11

CAP理論分布式

2012-06-20 10:47:25

Team Leader

2012-06-20 15:01:25

iOS開發(fā)

2023-02-27 10:45:16

2021-09-26 06:43:07

封裝網(wǎng)絡(luò)請(qǐng)求

2018-11-16 16:35:19

Java源碼編程語言

2017-03-16 11:39:33

Openstack源碼姿勢(shì)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)