<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)一個 mini-axios

          共 29765字,需瀏覽 60分鐘

           ·

          2024-08-20 09:10

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

          export default axios



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

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

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


          import axios from './axios.js'

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


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

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

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

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

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

          我們來測試一下輸出:

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


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

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

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

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

          1. xhr
          2. fetch

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

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

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

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

          我們新建一個 Adapte.js 的文件:


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

              getAdapter(adapters) {

              }
          }

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


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

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


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

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

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


          我們再新建一個 dispatchRequest 模塊,并且在這個模塊中統(tǒng)一發(fā)送 axios 請求:



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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

          我們在這里就不把所有的請求方案都寫出來了,我們先以 xhr 為例子來封裝一個請求模塊:我們新建一個 xhr 模塊,先添加上如下代碼:


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

          const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined'

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

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


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

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

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


          import xhr from './xhr.js'

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

          const adapteConfig = {
              xhr,
              fetchundefined,
              httpundefined
          }


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


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


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

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

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


          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í)例上掛手動掛載一個 create 方法
            instance.create = function create(instanceConfig{
              // 將用戶傳入的配置和默認(rèn)配置進(jìn)行合并
              return createInstance(mergeConfig(defaultConfig, instanceConfig));
            };
            // 獲取到當(dāng)前 axios 對象上的攔截器
            instance.interceptors = context.interceptors;
            return instance;
          }

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

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


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

          })

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

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

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


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

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

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

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

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


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

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

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

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


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

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



          import adapate from './Adapte.js'

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

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

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


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

          const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined'

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


          我們在測試代碼中加入一些測試log:


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

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

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

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

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

          axios 取消請求的實(shí)現(xiàn)我們在這里就不一行一行的寫代碼了,我們直接去源碼中一探究竟:

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

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

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


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

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

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

          我們先來看一下 CancelToken 這個的核心邏輯:



          class CancelToken 
             
             construcoer(sxsc) {
              // 緩存改變 promise 狀態(tài)的方法
              let resolvePromise;
              this.promise = new Promise(function promiseExecutor(resolve{
                resolvePromise = resolve;
              });
              // 注冊異步微任務(wù)
              this.promise.then(cancel => {
                // 沒有訂閱任務(wù),那么直接退出
                if (!token._listeners) return;
                let i = token._listeners.length;
                // 依次執(zhí)行訂閱任務(wù)
                while (i-- > 0) {
                  token._listeners[i](cancel);
                }
                token._listeners = null;
              })
              
              // 注冊任務(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ì)上就是一個 CancelToken 的實(shí)例,因此我們可以調(diào)用 CancelToken 原型上的 subscribe 方法來注冊一個異步任務(wù)。
          2. CancelToke 內(nèi)部通過 Promise 微任務(wù)的方式來管理了一個任務(wù)隊(duì)列,任何通過 subscribe 注冊的任務(wù)都會在該 Promise 對象的狀態(tài)完成之后得到執(zhí)行。
          3. cancel 方法做的最核心的一件事情就是 resolvePromise(token.reason) 將上面聊到的 Promise 對象的狀態(tài)變成已完成,從而將 subscribe 注冊的任務(wù)全部推入微任務(wù)隊(duì)列去進(jìn)行執(zhí)行。
          4. CancelToken 底層實(shí)際上就是實(shí)現(xiàn)了一個典型的發(fā)布訂閱模式,外部的模塊可以通過 subscribe 方法來注冊一系列時間,然后通過調(diào)用 cancel 執(zhí)行這些事件。

          梳理了 CancelToken 模塊的實(shí)現(xiàn)邏輯,我們再來看請求取消的實(shí)現(xiàn)方案就很好理解了, 我們在創(chuàng)建 axios 請求任務(wù)的時候傳入了 cancelToken: source.token 實(shí)際上就是把 CancelToken 對象傳入到了配置中,然后 axios 內(nèi)部會在某一個模塊中通過 cancelToken.subscribe 的方法注冊一個請求取消的事件,最后我們在需要的地方調(diào)用cannal方法觸發(fā)事件的調(diào)用從而取消請求。

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

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

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

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

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

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

          比如:

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

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

          比如在基于 axios 來進(jìn)行請求的時候我們這樣用:


          import { request } from 'my-request'

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

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


          import { request } from 'my-request'

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

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

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

          request 庫最核心的代碼類似于下面這樣:


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

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

          在使用的時候極其簡單


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

          // 注冊自定義請求庫

          const { use } = request

          use(VueRequestFetch)

          request('xxx', {})

          request.get('xxx', {})


          作者:文學(xué)與代碼
          https://juejin.cn/post/7388316163578363916

          瀏覽 176
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  樱桃香蕉视频 | 天天操天天操天天操天天 | 亚洲嫩穴| 麻豆传媒在线观看视频 | 久久免费看 |