<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          axios 源碼解析:十分鐘帶你實(shí)現(xiàn)一個(gè) mini-axios

          共 30035字,需瀏覽 61分鐘

           ·

          2024-07-10 08:49

              

          大廠技術(shù)  高級(jí)前端  Node進(jìn)階

          點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)

          回復(fù)1,加入高級(jí)Node交流群

          原文:https://juejin.cn/post/7388316163578363916

          整個(gè)實(shí)現(xiàn)流程分為 5 個(gè)大部分:

          1. 準(zhǔn)備測(cè)試環(huán)境
          2. axios 核心請(qǐng)求構(gòu)建
          3. 多宿主環(huán)境(瀏覽器 || node)適配思想
          4. 攔截器的實(shí)現(xiàn)原理
          5. 如何取消請(qǐng)求

          1、準(zhǔn)備基礎(chǔ)的測(cè)試環(huán)境

          1.1 基于 Koa 準(zhǔn)備一個(gè)最簡(jiǎn)單的服務(wù)程序:

          import Koa from 'koa';
          const app = new Koa();

          // 一個(gè)簡(jiǎn)單的路由處理函數(shù)
          app.use(async ctx => {
            ctx.body = 'Hello, World!';
          });

          // 啟動(dòng)服務(wù)器
          app.listen(3000, () => {
            console.log('Server is running on http://localhost:3000');
          });

          因?yàn)槲覀冃枰跒g覽器中測(cè)試請(qǐng)求,所以服務(wù)端還需要支持瀏覽器跨域,所以我們添加一個(gè)支持跨域的中間件:


          app.use(async (ctx, next) => {
              ctx.set('Access-Control-Allow-Origin''*'); // 允許所有來(lái)源
              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端測(cè)試環(huán)境:

          我們初始化基礎(chǔ)的測(cè)試 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ǔ)的瀏覽器測(cè)試環(huán)境')

          基礎(chǔ)的 node 測(cè)試環(huán)境比較簡(jiǎn)單,就是一個(gè)普通的 js 文件,只要我們上述的 js 文件不包含瀏覽器端的宿主 api 那么也可以直接在 node 端進(jìn)行測(cè)試。整個(gè)結(jié)構(gòu)搭建完成之后應(yīng)該就是下面的文件格式:

          2、axios 核心請(qǐng)求構(gòu)建

          2.1 開(kāi)發(fā) axios 的入口模塊:

          我們?cè)跍y(cè)試文件夾下面新建一個(gè) axios.js 的文件,入口內(nèi)容開(kāi)發(fā)比較簡(jiǎn)單,我們就不再過(guò)多贅述了,主要就是開(kāi)發(fā)一個(gè) Axios 類(lèi),初始化 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('開(kāi)始發(fā)送請(qǐng)求', 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í)例上掛手動(dòng)掛載一個(gè) create 方法
            instance.create = function create(instanceConfig{
              // 將用戶傳入的配置和默認(rèn)配置進(jìn)行合并
              return createInstance(mergeConfig(defaultConfig, instanceConfig))
            };
            return instance
          }

          // 基于默認(rèn)配置,利用工廠函數(shù)創(chuàng)建出一個(gè)默認(rèn)的 axios 實(shí)例

          const axios = createInstance({
            // 默認(rèn)的網(wǎng)絡(luò)延遲時(shí)間
            timeout0,
            // adapter: 默認(rèn)的適配器配置
            adapter: ["xhr""http""fetch"],
            // 基礎(chǔ)路徑
            beseURL"",
            headers: {}
          });

          // 給 axios 添加一系列其他配置

          axios.Axios = Axios;
          axios.default = axios;

          export default axios



          axios 入口核心代碼其實(shí)比較簡(jiǎn)單,最核心的就是利用工廠函數(shù)創(chuàng)建出一個(gè)最基礎(chǔ)的 request 請(qǐng)求方法

          如果我們不需要進(jìn)行額外的自定義配置,那么 axios 本身就已經(jīng)可以開(kāi)箱即用了。如果我們調(diào)用 create,本質(zhì)上就是合并用戶自定義的 axios 配置然后重新產(chǎn)生一個(gè) requiest 方法。

          開(kāi)發(fā)完畢之后,我們就可以在之前已經(jīng)準(zhǔn)備好的測(cè)試文件中導(dǎo)入 axios 實(shí)例來(lái)進(jìn)行測(cè)試了:


          import axios from './axios.js'

          axios('http://localhost:3000/')


          瀏覽器中最基礎(chǔ)的測(cè)試已經(jīng)可以了。

          我們看一下node環(huán)境:

          至此基礎(chǔ)的開(kāi)發(fā)和測(cè)試就完畢了。

          2.2 利用參數(shù)歸一化的技巧處理 _requiest 的參數(shù)問(wèn)題:

          參數(shù)歸一化是 js 這種弱類(lèi)型語(yǔ)言中一種常見(jiàn)的統(tǒng)一函數(shù)入?yún)⒌姆椒ǎ锰幘褪?減少主干函數(shù)中對(duì)于參數(shù)校驗(yàn)的判斷邏輯,統(tǒng)一函數(shù)參數(shù)的類(lèi)型,讓主干函數(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)一為一個(gè)配置對(duì)象
              const config = this.requestHelper(url, options)
              console.log('config', config)
            }

          我們來(lái)測(cè)試一下輸出:

          我們可以看到參數(shù)就已經(jīng)統(tǒng)一成為一個(gè)對(duì)象了。在統(tǒng)一完畢 _requiest 這個(gè)函數(shù)的參數(shù)之后,因?yàn)楝F(xiàn)在 Axios 這個(gè)類(lèi)中存在一個(gè) defaultConfig 的默認(rèn)配置,而 _requiest 本身又可以接收一個(gè)配置對(duì)象,所以我們可以將將這兩個(gè)配置進(jìn)行簡(jiǎn)單的合并:


            /**
             *
             * @param {string} url
             * @param {Object} options
             */

            _requiest(url, options) {
              // 首先進(jìn)行參數(shù)歸一化,將所有的參數(shù)全部統(tǒng)一為一個(gè)配置對(duì)象
              const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
              console.log('最終的配置', config)
            }

          3、多環(huán)境請(qǐng)求發(fā)送的問(wèn)題處理:

          前端工程師接觸的更多的環(huán)境一般是瀏覽器環(huán)境,瀏覽器環(huán)境中兩個(gè)發(fā)送請(qǐng)求的方案:

          1. xhr
          2. fetch

          但是如果是比較舊的node環(huán)境的話這兩種請(qǐng)求方案都不支持,node環(huán)境中原生支持的請(qǐng)求庫(kù)是 http 以及 https 模塊。

          axios作為一個(gè)通用的http請(qǐng)求庫(kù)就必須要解決這個(gè)問(wèn)題。也就是它必須能夠適應(yīng)不同環(huán)境的請(qǐng)求方案。針對(duì)這個(gè)問(wèn)題,axios 提出了 適配器 的概念,axios中所有的請(qǐng)求的發(fā)送都是基于這個(gè)適配器來(lái)進(jìn)行發(fā)送的,源碼中專門(mén)有一個(gè)模塊來(lái)處理請(qǐng)求適配的問(wèn)題:

          適配器的思想其實(shí)極其簡(jiǎn)單,就是根據(jù)判斷哪一套請(qǐng)求 api 存在,那么就使用那一套請(qǐng)求 api。這個(gè)和 vue 內(nèi)部 nextTick 異步模塊的處理方案是一致的。大家如果感興趣可以去查閱一下。

          我們來(lái)簡(jiǎn)單實(shí)現(xiàn)一下適配器的核心邏輯:

          我們新建一個(gè) Adapte.js 的文件:


          export default {
              /**
               * 獲取請(qǐng)求適配器的方法
               * @param {Function | Function[]} adapters 
               */

              getAdapter(adapters) {

              }
          }

          我們同樣可以進(jìn)行參數(shù)歸一化:


          // getAdapte 參數(shù)歸一化

          /**
           * 獲取請(qǐng)求適配器的方法
           * @param {Function | Function[]} adapters
           */


          const getAdapteHandlers = (adapters) => {
              return  Array.isArray(adapters) ? adapters : [adapters]
          }

          export default {
            /**
             * 獲取請(qǐng)求適配器的方法
             * @param {Function | Function[] | string[]} adapters
             */

            getAdapter(adapters) {
              // 參數(shù)歸一化
              adapters = getAdapteHandlers(adapters)
            },
          }


          我們?cè)傩陆ㄒ粋€(gè) dispatchRequest 模塊,并且在這個(gè)模塊中統(tǒng)一發(fā)送 axios 請(qǐng)求:



          /**
           * 
           * axios 統(tǒng)一進(jìn)行 http 請(qǐng)求發(fā)送的模塊
           * 
           * @param {Object} config 
           */

          export default function dispatchRequest(config{
            console.log('開(kāi)始請(qǐng)求發(fā)送', config)
          }

          我們將這個(gè)模塊導(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)一為一個(gè)配置對(duì)象
              const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
              // 調(diào)用 dispatchRequest 方法進(jìn)行請(qǐng)求的發(fā)送和處理,并且將合并之后的配置傳入
              dispatchRequest(config)
            }

          看一下控制臺(tái)我們就可以發(fā)現(xiàn)請(qǐng)求方法被成功調(diào)用了:

          接著我們就需要在 dispatchRequest 方法中處理多環(huán)境請(qǐng)求適配的問(wèn)題了

          首先我們要明白一個(gè)點(diǎn),和vue3中的渲染器同樣的設(shè)計(jì)理念,axios中的請(qǐng)求器也是允許用戶自定義的,用戶只需要在配置中指定 adapter 配置,傳入一個(gè) () => Promise 類(lèi)型的函數(shù)就可以了 ,因此 mini axios 也需要支持這個(gè)設(shè)計(jì):

          用戶只需要將這個(gè)配置替換掉,比如我們可以傳入如下的配置:

          那么原本默認(rèn)的適配器就會(huì)被替換:

          然后我們將 adapter 配置傳入到 adapate.getAdapter 函數(shù)中:


          export default function dispatchRequest(config{
            // 利用適配器獲取當(dāng)前環(huán)境的請(qǐng)求 api
            adapate.getAdapter(config.adapter)
          }

          此時(shí)我們就可以在 adapate.getAdapter 函數(shù)中獲取到最終的適配器的配置了。

          因?yàn)閰?shù)歸一的緣故,已經(jīng)被統(tǒng)一為一個(gè)數(shù)組結(jié)構(gòu)了:

          我們目前先不測(cè)試自定義適配器的情況,所以我們先將基礎(chǔ)配置復(fù)原:

          那么我們拿到的適配器配置就是基礎(chǔ)的配置:

          我們先針對(duì)基礎(chǔ)配置進(jìn)行處理,我們先梳理一下適配器中需要處理的一些基礎(chǔ)問(wèn)題:

          1. 首先需要提供出 xhr, http, fetch 這三種請(qǐng)求方案對(duì)應(yīng)的請(qǐng)求方法。
          2. 查找當(dāng)前環(huán)境支持的第一個(gè)請(qǐng)求類(lèi)型,使用基于該請(qǐng)求類(lèi)型封裝的請(qǐng)求方法來(lái)作為請(qǐng)求方案。

          我們?cè)谶@里就不把所有的請(qǐng)求方案都寫(xiě)出來(lái)了,我們先以 xhr 為例子來(lái)封裝一個(gè)請(qǐng)求模塊:我們新建一個(gè) xhr 模塊,先添加上如下代碼:


          // 首先判斷當(dāng)前環(huán)境是否存在 xhr 模塊

          const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined'

          這個(gè)判定就是為了判斷當(dāng)前宿主環(huán)境是否支持 XMLHttpRequest 這個(gè)構(gòu)造函數(shù)

          緊接著如果支持的話,那么我們就可以返回一個(gè)Promise函數(shù),在函數(shù)中封裝 xhr 的邏輯:


          export default isXHRAdapterSupported && function (config{
             return Promise((resplved, rejected) => {
             
             })
          }

          至于里面細(xì)枝末節(jié)的內(nèi)容我就不過(guò)多贅述了,大家感興趣可以去查閱代碼,總體上比較簡(jiǎn)單。

          封裝好了 xhr 模塊之后,我們就在 adapate 適配器中導(dǎo)入該模塊,并且進(jìn)行簡(jiǎn)單配置:


          import xhr from './xhr.js'

          // 假如目前的瀏覽器比較古老,只支持 xhr

          const adapteConfig = {
              xhr,
              fetchundefined,
              httpundefined
          }


          緊接著,我們需要來(lái)編寫(xiě)查找當(dāng)前環(huán)境支持的第一個(gè)請(qǐng)求方法的邏輯:


            /**
             * 獲取請(qǐng)求適配器的方法
             * @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í)總體很簡(jiǎn)單:


            /**
             * 獲取請(qǐng)求適配器的方法
             * @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í)并沒(méi)有太多特別的地方,和大部分開(kāi)源庫(kù)中實(shí)現(xiàn)異步任務(wù)調(diào)度是類(lèi)似的方案,總體上是以下的思路:

          1. 實(shí)現(xiàn)攔截器的注冊(cè)邏輯:

          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 類(lèi)中導(dǎo)入該模塊,初始化攔截器存儲(chǔ)容器


          class Axios {
            /**
             *
             * @param {Object} defaultConfig
             */


            constructor(defaultConfig) {
              this.defaultConfig = defaultConfig;
              // 初始化攔截器容器
              this.interceptors = {
                requestnew InterceptorManager(),
                responsenew InterceptorManager()
              };
            }
            }
            
            function createInstance(defaultConfig{
            // 初始化 axios 實(shí)例
            const context = new Axios(defaultConfig);
            const instance = Axios.prototype.requiest.bind(context);
            // 實(shí)例上掛手動(dòng)掛載一個(gè) create 方法
            instance.create = function create(instanceConfig{
              // 將用戶傳入的配置和默認(rèn)配置進(jìn)行合并
              return createInstance(mergeConfig(defaultConfig, instanceConfig));
            };
            // 獲取到當(dāng)前 axios 對(duì)象上的攔截器
            instance.interceptors = context.interceptors;
            return instance;
          }

          核心就是 在 Axios 實(shí)例上掛載了攔截器對(duì)象

          然后在使用的過(guò)程中注冊(cè)請(qǐng)求和響應(yīng)攔截器


          const instance = axios.create({
            uri'http://localhost:3000/',
            // adapter: () => new Promise((resolve, reject) => {
            //   resolve('自定義內(nèi)容')
            // })

          })

          // console.log('instance', instance.interceptors)
          // 添加請(qǐng)求攔截器
          instance.interceptors.request.use(
            function (config{
              // 在發(fā)送請(qǐng)求之前做些什么,例如添加 token 到請(qǐng)求頭
              config.headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`
              return config;
            },
            function (error{
              // 對(duì)請(qǐng)求錯(cuò)誤做些什么
              return Promise.reject(error)
            }
          )

          // 添加響應(yīng)攔截器
          instance.interceptors.response.use(
            function (response{
              // 對(duì)響應(yīng)數(shù)據(jù)做點(diǎn)什么
              return response
            },
            function (error{
              // 對(duì)響應(yīng)錯(cuò)誤做點(diǎn)什么,例如處理 401 錯(cuò)誤
              if (error.response && error.response.status === 401) {
                // 清除本地存儲(chǔ)的 token,并跳轉(zhuǎn)到登錄頁(yè)面
                localStorage.removeItem('token')
                window.location.href = '/login'
              }
              return Promise.reject(error)
            }
          )
            

          緊接著我們開(kāi)始在 _request 中處理請(qǐng)求和響應(yīng)攔截器的內(nèi)容:我們先將請(qǐng)求和響應(yīng)攔截器打印出來(lái):


            /**
             *
             * @param {string} url
             * @param {Object} options
             */

            _requiest(url, options) {
              // 首先進(jìn)行參數(shù)歸一化,將所有的參數(shù)全部統(tǒng)一為一個(gè)配置對(duì)象
              const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
              // 開(kāi)始處理請(qǐng)求和響應(yīng)攔截器的內(nèi)容
              console.log('獲取到的請(qǐng)求和響應(yīng)攔截器'this.interceptors)
              // 調(diào)用 dispatchRequest 方法進(jìn)行請(qǐng)求的發(fā)送和處理,并且將合并之后的配置傳入
              dispatchRequest(config)
            }

          我們可以看到請(qǐng)求和響應(yīng)攔截器注冊(cè)進(jìn)來(lái)了。

          1. 開(kāi)始進(jìn)行攔截器和請(qǐng)求方法的任務(wù)編排:

          所謂任務(wù)編排其實(shí)很簡(jiǎn)單,就是在底層維護(hù)一個(gè)任務(wù)隊(duì)列來(lái)處理一系列任務(wù),隊(duì)列類(lèi)似于下面這樣:


          [
            請(qǐng)求攔截器1成功方法,
            請(qǐng)求攔截器1失敗方法,
            請(qǐng)求方法,
            undefined,
            響應(yīng)攔截器1成功方法,
            響應(yīng)攔截器1失敗方法
          ]

          然后從頭到尾循環(huán)這個(gè)隊(duì)列,每一次循環(huán)都取出當(dāng)前隊(duì)列的頭兩位,并且使用 Promise.then 將其注冊(cè)為當(dāng)前任務(wù)階段成功和失敗的處理函數(shù)。

          此時(shí)瀏覽器主線程任務(wù)執(zhí)行完畢之后會(huì)依次執(zhí)行 Promise.then 注冊(cè)的微任務(wù)。

          理解了思路,代碼實(shí)現(xiàn)就非常簡(jiǎn)單了:


            /**
             *
             * @param {string} url
             * @param {Object} options
             */

            _requiest(url, options) {
              // 首先進(jìn)行參數(shù)歸一化,將所有的參數(shù)全部統(tǒng)一為一個(gè)配置對(duì)象
              const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
              // 開(kāi)始處理請(qǐng)求和響應(yīng)攔截器的內(nèi)容
              console.log('獲取到的請(qǐng)求和響應(yīng)攔截器'this.interceptors)
              // 調(diào)用 dispatchRequest 方法進(jìn)行請(qǐng)求的發(fā)送和處理,并且將合并之后的配置傳入
              const chain = [dispatchRequest, undefined]
              // 開(kāi)始進(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)
              })
              // 開(kāi)始注冊(cè)異步任務(wù), 注意這里很重要,將 config 配置對(duì)象按照 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ā)送請(qǐng)求:



          import adapate from './Adapte.js'

          /**
           * 
           * axios 統(tǒng)一進(jìn)行 http 請(qǐng)求發(fā)送的模塊
           * 
           * @param {Object} config 
           */

          export default function dispatchRequest(config{
            // 利用適配器獲取當(dāng)前環(huán)境的請(qǐng)求 api
            const adapter = adapate.getAdapter(config.adapter)
            return adapter(config)
          }

          調(diào)整 xhr 適配器,簡(jiǎn)單返回一個(gè)結(jié)果:


          // 首先判斷當(dāng)前環(huán)境是否存在 xhr 模塊

          const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined'

          export default isXHRAdapterSupported && function (config{
              return new Promise((resolved, rejected) => {
                 resolved('請(qǐng)求結(jié)果')
              })
          }


          我們?cè)跍y(cè)試代碼中加入一些測(cè)試log:


          instance.interceptors.request.use(
            function (config{
              // 在發(fā)送請(qǐng)求之前做些什么,例如添加 token 到請(qǐng)求頭
              config.headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`
              console.log('請(qǐng)求攔截器', config)
              return config;
            },
            function (error{
              // 對(duì)請(qǐng)求錯(cuò)誤做些什么
              return Promise.reject(error)
            }
          )

          // 添加響應(yīng)攔截器
          instance.interceptors.response.use(
            function (response{
              console.log('響應(yīng)攔截器', response)
              // 對(duì)響應(yīng)數(shù)據(jù)做點(diǎn)什么
              return response
            },
            function (error{
              // 對(duì)響應(yīng)錯(cuò)誤做點(diǎn)什么,例如處理 401 錯(cuò)誤
              if (error.response && error.response.status === 401) {
                // 清除本地存儲(chǔ)的 token,并跳轉(zhuǎn)到登錄頁(yè)面
                localStorage.removeItem('token')
                window.location.href = '/login'
              }
              return Promise.reject(error)
            }
          )

          可以看到整個(gè)流程按照預(yù)期的執(zhí)行了

          至此我們就搞清楚了 axios 攔截器的設(shè)計(jì)哲學(xué)了。

          5、axios 取消請(qǐng)求的實(shí)現(xiàn)方案

          axios 取消請(qǐng)求的實(shí)現(xiàn)我們?cè)谶@里就不一行一行的寫(xiě)代碼了,我們直接去源碼中一探究竟:

          1. 首先我們簡(jiǎn)單梳理一下 axios 的源碼結(jié)構(gòu):

          axios 的核心代碼就分模塊存在放 lib 目錄下面:

          lib目錄下面幾個(gè)核心文件夾的作用分別是:


          adapters:適配器相關(guān)邏輯
          cancel: 請(qǐng)求取消相關(guān)功能
          core: axios 核心代碼
          Axios.js: Axios 核心類(lèi)
          dispatchRequest: 請(qǐng)求發(fā)送的核心模塊,和我們手寫(xiě)的核心邏輯類(lèi)似
          helpers: 工具方法
          1. 回顧 axios 請(qǐng)求取消的使用方式(以 ### 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 } }); // 取消請(qǐng)求 
              
              source.cancel('取消請(qǐng)求的原因');

          核心其實(shí)就是三步:

          1. 創(chuàng)建 CancelToken.source 對(duì)象
          2. 將該對(duì)象中的 token 配置到 cancelToken 上
          3. 在指定的時(shí)候調(diào)用 source.cancel 方法

          我們先來(lái)看一下 CancelToken 這個(gè)的核心邏輯:



          class CancelToken 
             
             construcoer(sxsc) {
              // 緩存改變 promise 狀態(tài)的方法
              let resolvePromise;
              this.promise = new Promise(function promiseExecutor(resolve{
                resolvePromise = resolve;
              });
              // 注冊(cè)異步微任務(wù)
              this.promise.then(cancel => {
                // 沒(méi)有訂閱任務(wù),那么直接退出
                if (!token._listeners) return;
                let i = token._listeners.length;
                // 依次執(zhí)行訂閱任務(wù)
                while (i-- > 0) {
                  token._listeners[i](cancel);
                }
                token._listeners = null;
              })
              
              // 注冊(cè)任務(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
              };
          }

          從以上的核心代碼我們可以看出

          1. cancelToken 本質(zhì)上就是一個(gè) CancelToken 的實(shí)例,因此我們可以調(diào)用 CancelToken 原型上的 subscribe 方法來(lái)注冊(cè)一個(gè)異步任務(wù)。
          2. CancelToke 內(nèi)部通過(guò) Promise 微任務(wù)的方式來(lái)管理了一個(gè)任務(wù)隊(duì)列,任何通過(guò) subscribe 注冊(cè)的任務(wù)都會(huì)在該 Promise 對(duì)象的狀態(tài)完成之后得到執(zhí)行。
          3. cancel 方法做的最核心的一件事情就是 resolvePromise(token.reason) 將上面聊到的 Promise 對(duì)象的狀態(tài)變成已完成,從而將 subscribe 注冊(cè)的任務(wù)全部推入微任務(wù)隊(duì)列去進(jìn)行執(zhí)行。
          4. CancelToken 底層實(shí)際上就是實(shí)現(xiàn)了一個(gè)典型的發(fā)布訂閱模式,外部的模塊可以通過(guò) subscribe 方法來(lái)注冊(cè)一系列時(shí)間,然后通過(guò)調(diào)用 cancel 執(zhí)行這些事件。

          梳理了 CancelToken 模塊的實(shí)現(xiàn)邏輯,我們?cè)賮?lái)看請(qǐng)求取消的實(shí)現(xiàn)方案就很好理解了, 我們?cè)趧?chuàng)建 axios 請(qǐng)求任務(wù)的時(shí)候傳入了 cancelToken: source.token 實(shí)際上就是把 CancelToken 對(duì)象傳入到了配置中,然后 axios 內(nèi)部會(huì)在某一個(gè)模塊中通過(guò) cancelToken.subscribe 的方法注冊(cè)一個(gè)請(qǐng)求取消的事件,最后我們?cè)谛枰牡胤秸{(diào)用cannal方法觸發(fā)事件的調(diào)用從而取消請(qǐng)求。

          實(shí)際上我們查看 http 模塊就可以看到在 http 模塊中 axios 通過(guò) cancelToken.subscrib 注冊(cè)了請(qǐng)求取消事件:

          另外如果我們查看 dispatchReques 源碼,我們可以看到,如果我們進(jìn)行了請(qǐng)求取消相關(guān)配置,在請(qǐng)求發(fā)布的每一個(gè)階段,axios都會(huì)調(diào)用 throwIfCancellationRequested 方法來(lái)檢查請(qǐng)求的取消狀態(tài),如果發(fā)現(xiàn)某一個(gè)階段請(qǐng)求和已經(jīng)取消了,那么這個(gè)階段以及以后的額任務(wù)都不會(huì)繼續(xù)執(zhí)行了:

          另外我們?cè)跀U(kuò)展一個(gè)點(diǎn),就是在瀏覽環(huán)境,我們可以使用 controller.signal; 方式來(lái)配置請(qǐng)求取消,這樣的話,那么 axios 就真的可以利用這個(gè) api 來(lái)真正的取消指定的 http 請(qǐng)求傳輸并且回收相關(guān)資源了。

          至此我們就從提上了解完畢了 axios 中最核心的設(shè)計(jì)原理。

          擴(kuò)展:ts 中模板模式實(shí)現(xiàn)通用請(qǐng)求處理的方案:

          在企業(yè)中我們目前一般使用 ts 來(lái)編寫(xiě)底層的庫(kù),而 ts 為我們提供了接口和類(lèi)型更加強(qiáng)大的抽象類(lèi),基于這些高級(jí)語(yǔ)法,我們可以使用模板模式更好的實(shí)現(xiàn)跨平臺(tái)的通用請(qǐng)求邏輯。總體實(shí)現(xiàn)方案如下:

          比如:

          我們現(xiàn)在需要封裝一個(gè)通用的業(yè)務(wù)請(qǐng)求庫(kù),這個(gè)庫(kù)底層的依賴的基礎(chǔ)請(qǐng)求庫(kù)可能是 axios,也可能是傳統(tǒng)的 fetch,也可能是針對(duì) Vue 框架的 VueRequest,也可能是針對(duì)react的 useSWR 等等,我們可能需要根據(jù)不同的業(yè)務(wù)場(chǎng)景進(jìn)行靈活的切換。

          但是在切換的過(guò)程中,我們可能希望做到在使用層面無(wú)感,使用層面的 api 統(tǒng)一。

          比如在基于 axios 來(lái)進(jìn)行請(qǐng)求的時(shí)候我們這樣用:


          import { request } from 'my-request'

          request('xxx', {
            ...xxx
          })

          在基于 vueRequest 時(shí)候我們也希望可以直接這樣使用:


          import { request } from 'my-request'

          request('xxx', {
            ...xxx
          })

          也就是這個(gè)庫(kù)底層需要對(duì)用戶完全屏蔽各種底層請(qǐng)求庫(kù)存的差異,不管是基于什么樣的底層請(qǐng)求庫(kù),api還是照樣的調(diào)用,代碼還是照樣的寫(xiě)。為了實(shí)現(xiàn)這一點(diǎn),我們就可以這樣去進(jìn)行設(shè)計(jì):

          1. 暴露一個(gè)標(biāo)準(zhǔn)的 request 方法,不論在怎樣的情況下,發(fā)送請(qǐng)求都是基于這一個(gè)統(tǒng)一的請(qǐng)求方法,并且存在統(tǒng)一側(cè)參數(shù)類(lèi)型。
          2. 暴露一個(gè)標(biāo)準(zhǔn)的 interface RequestHandler 接口,任何需要注冊(cè)到請(qǐng)求庫(kù)中的模塊都需要按照標(biāo)準(zhǔn)統(tǒng)一的接口進(jìn)行設(shè)計(jì)。
          3. 按照 RequestHandler 接口的約束來(lái)封裝對(duì)應(yīng)的請(qǐng)求模塊。
          4. request上暴露一個(gè)對(duì)外的 use 方法,用戶調(diào)用這個(gè)方法可以將指定的請(qǐng)求模塊進(jìn)行注冊(cè)。
          5. 用戶需要將默認(rèn)的請(qǐng)求庫(kù) AxiosFetch替換為指定的請(qǐng)求庫(kù)使用指定的 請(qǐng)求庫(kù) VueRequestFetch的時(shí)候,只需要導(dǎo)入基于統(tǒng)一的接口封裝的 VueRequestFetch 模塊,然后調(diào)用 use 函數(shù)將該模塊進(jìn)行注冊(cè)就可以了,其他的業(yè)務(wù)代碼完全不需要替換。

          request 庫(kù)最核心的代碼類(lèi)似于下面這樣:


          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 處理請(qǐng)求
            useHandler.get()
          }

          request.use = (handler: RequestHandler) {
            // 注冊(cè)
             useHandler = handler
          }

          在使用的時(shí)候極其簡(jiǎn)單


          import { request } from '@xxx/core'
          import VueRequestFetch from '@xxx/VueRequestFetch'

          // 注冊(cè)自定義請(qǐng)求庫(kù)

          const { use } = request

          use(VueRequestFetch)

          request('xxx', {})

          request.get('xxx', {})

          • Node 社群

          •      


            我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。


          • 最后不要忘了點(diǎn)個(gè)贊再走噢

          瀏覽 218
          2點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          2點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产视频黄片 | 成人区精品一区二区婷婷255 | 日韩AV小电影 | 操逼18禁网站 | 欧美一级婬片免费视频黄 |