<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了?我能手寫簡易版的axios

          共 9010字,需瀏覽 19分鐘

           ·

          2021-07-27 12:05

          點擊上方 程序員成長指北,關(guān)注公眾號

          回復1,加入高級Node交流群


          作者:FE_FLY

          juejin.cn/post/6973257605367988260

          axios作為我們工作中的常用的ajax請求庫,作為前端工程師的我們當然是想一探究竟,axios究竟是如何去架構(gòu)整個框架,中間的攔截器、適配器、 取消請求這些都是我們經(jīng)常使用的。

          前言

          由于axios源碼中有很多不是很重要的方法,而且很多方法為了考慮兼容性,并沒有考慮到用es6 的語法去寫。本篇主要是帶你去梳理axios的主要流程,并用es6重寫簡易版axios

          • 攔截器

          • 適配器

          • 取消請求

          攔截器

          一個axios實例上有兩個攔截器,一個是請求攔截器, 然后響應(yīng)攔截器。我們下看下官網(wǎng)的用法:

          添加攔截器

          // 添加請求攔截器
          axios.interceptors.request.use(function (config) {
          // 在發(fā)送請求之前做些什么
          return config;
          }, function (error) {
          // 對請求錯誤做些什么
          return Promise.reject(error);
          });

          移除攔截器

          const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
          axios.interceptors.request.eject(myInterceptor);

          其實源碼中就是,所有攔截器的執(zhí)行 所以說肯定有一個forEach方法。

          思路理清楚了,現(xiàn)在我們就開始去寫吧。代碼我就直接發(fā)出來,然后我在下面注解。

          export class InterceptorManager {
          constructor() {
          // 存放所有攔截器的棧
          this.handlers = []
          }

          use(fulfilled, rejected) {
          this.handlers.push({
          fulfilled,
          rejected,
          })
          //返回id 便于取消
          return this.handlers.length - 1
          }
          // 取消一個攔截器
          eject(id) {
          if (this.handlers[id]) {
          this.handlers[id] = null
          }
          }

          // 執(zhí)行棧中所有的hanlder
          forEach(fn) {
          this.handlers.forEach((item) => {
          // 這里為了過濾已經(jīng)被取消的攔截器,因為已經(jīng)取消的攔截器被置null
          if (item) {
          fn(item)
          }
          })
          }
          }

          攔截器這個類我們已經(jīng)初步實現(xiàn)了,現(xiàn)在我們?nèi)崿F(xiàn)axios 這個類,還是先看下官方文檔,先看用法,再去分析。

          axios(config)

          // 發(fā)送 POST 請求
          axios({
          method: 'post',
          url: '/user/12345',
          data: {
          firstName: 'Fred',
          lastName: 'Flintstone'
          }
          });
          axios(url[, config])
          // 發(fā)送 GET 請求(默認的方法) 
          axios('/user/12345');

          Axios 這個類最核心的方法其實還是 request 這個方法。我們先看下實現(xiàn)吧

          class Axios {
          constructor(config) {
          this.defaults = config
          this.interceptors = {
          request: new InterceptorManager(),
          response: new InterceptorManager(),
          }
          }
          // 發(fā)送一個請求
          request(config) {
          // 這里呢其實就是去處理了 axios(url[,config])
          if (typeof config == 'string') {
          config = arguments[1] || {}
          config.url = arguments[0]
          } else {
          config = config || {}
          }

          // 默認get請求,并且都轉(zhuǎn)成小寫
          if (config.method) {
          config.method = config.method.toLowerCase()
          } else {
          config.method = 'get'
          }

          // dispatchRequest 就是發(fā)送ajax請求
          const chain = [dispatchRequest, undefined]
          // 發(fā)生請求之前加入攔截的 fulfille 和reject 函數(shù)
          this.interceptors.request.forEach((item) => {
          chain.unshift(item.fulfilled, item.rejected)
          })
          // 在請求之后增加 fulfilled 和reject 函數(shù)
          this.interceptors.response.forEach((item) => {
          chain.push(item.fulfilled, item.rejected)
          })

          // 利用promise的鏈式調(diào)用,將參數(shù)一層一層傳下去
          let promise = Promise.resolve(config)

          //然后我去遍歷 chain
          while (chain.length) {
          // 這里不斷出棧 直到結(jié)束為止
          promise = promise.then(chain.shift(), chain.shift())
          }
          return promise
          }
          }

          這里其實就是體現(xiàn)了axios設(shè)計的巧妙, 維護一個棧結(jié)構(gòu) + promise 的鏈式調(diào)用 實現(xiàn)了 攔截器的功能, 可能有的小伙伴到這里還是不是很能理解,我還是給大家畫一個草圖去模擬下這個過程。

          假設(shè)我有1個請求攔截器handler和1個響應(yīng)攔截器handler

          一開始我們棧中的數(shù)據(jù)就兩個

          這個沒什么問題,由于有攔截器的存在,如果存在的話,那么我們就要往這個棧中加數(shù)據(jù),請求攔截器顧名思義要在請求之前所以是unshift。加完請求攔截器我們的棧變成了這樣

          沒什么問題,然后請求結(jié)束后,我們又想對請求之后的數(shù)據(jù)做處理,所以響應(yīng)攔截的數(shù)據(jù)自然是push了。這時候棧結(jié)構(gòu)變成了這樣:

          然后遍歷整個棧結(jié)構(gòu),每次出棧都是一對出棧, 因為promise 的then 就是 一個成功,一個失敗嘛。遍歷結(jié)束后,返回經(jīng)過所有處理的promise,然后你就可以拿到最終的值了。

          adapter

          Adapter: 英文解釋是適配器的意思。這里我就不實現(xiàn)了,我?guī)Т蠹铱匆幌略创a。adapter 做了一件事非常簡單,就是根據(jù)不同的環(huán)境 使用不同的請求。如果用戶自定義了adapter,就用config.adapter。否則就是默認是default.adpter.

           var adapter = config.adapter || defaults.adapter;

          return adapter(config).then() ...

          繼續(xù)往下看deafults.adapter做了什么事情:

          function getDefaultAdapter() {
          var adapter;
          if (typeof XMLHttpRequest !== 'undefined') {
          // For browsers use XHR adapter
          adapter = require('./adapters/xhr');
          } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
          // For node use HTTP adapter
          adapter = require('./adapters/http');
          }
          return adapter;
          }

          其實就是做個選擇:如果是瀏覽器環(huán)境:就是用xhr 否則就是node 環(huán)境。判斷process是否存在。從寫代碼的角度來說,axios源碼的這里的設(shè)計可擴展性非常好。有點像設(shè)計模式中的適配器模式, 因為瀏覽器端和node 端 發(fā)送請求其實并不一樣, 但是我們不重要,我們不去管他的內(nèi)部實現(xiàn),用promise包一層做到對外統(tǒng)一。所以 我們用axios 自定義adapter 器的時候, 一定是返回一個promise。ok請求的方法我在下面模擬寫出。

          cancleToken

          我首先問大家一個問題,取消請求原生瀏覽器是怎么做到的?有一個abort 方法??梢匀∠埱?。那么axios源碼肯定也是運用了這一點去取消請求。現(xiàn)在瀏覽器其實也支持fetch請求, fetch可以取消請求?很多同學說是不可以的,其實不是?fetch 結(jié)合 abortController 可以實現(xiàn)取消fetch請求。我們看下例子:

          const controller = new AbortController();
          const { signal } = controller;

          fetch("http://localhost:8000", { signal }).then(response => {
          console.log(`Request 1 is complete!`);
          }).catch(e => {
          console.warn(`Fetch 1 error: ${e.message}`);
          });
          // Wait 2 seconds to abort both requests
          setTimeout(() => controller.abort(), 2000);

          但是這是個實驗性功能,可惡的ie。所以我們這次還是用原生的瀏覽器xhr基于promise簡單的封裝一下。代碼如下:

          export function dispatchRequest(config) {
          return new Promise((resolve, reject) => {
          const xhr = new XMLHttpRequest()
          xhr.open(config.method, config.url)
          xhr.onreadystatechange = function () {
          if (xhr.status >= 200 && xhr.status <= 300 && xhr.readyState === 4) {
          resolve(xhr.responseText)
          } else {
          reject('失敗了')
          }
          }

          if (config.cancelToken) {
          // Handle cancellation
          config.cancelToken.promise.then(function onCanceled(cancel) {
          if (!xhr) {
          return
          }
          xhr.abort()
          reject(cancel)
          // Clean up request
          xhr = null
          })
          }
          xhr.send()
          })
          }

          Axios 源碼里面做了很多處理, 這里我只做了get處理,我主要的目的就是為了axios是如何取消請求的。先看下官方用法:

          主要是兩種用法:

          使用 cancel token 取消請求

          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 {
          // 處理錯誤
          }
          });

          axios.post('/user/12345', {
          name: 'new name'
          }, {
          cancelToken: source.token
          })

          // 取消請求(message 參數(shù)是可選的)
          source.cancel('Operation canceled by the user.');

          還可以通過傳遞一個 executor 函數(shù)到 CancelToken 的構(gòu)造函數(shù)來創(chuàng)建 cancel token:

          const CancelToken = axios.CancelToken;
          let cancel;

          axios.get('/user/12345', {
          cancelToken: new CancelToken(function executor(c) {
          // executor 函數(shù)接收一個 cancel 函數(shù)作為參數(shù)
          cancel = c;
          })
          });

          // cancel the request
          cancel();

          看了官方用法 和結(jié)合axios源碼:我給出以下實現(xiàn):

          export class cancelToken {
          constructor(exactor) {
          if (typeof executor !== 'function') {
          throw new TypeError('executor must be a function.')
          }
          // 這里其實將promise的控制權(quán) 交給 cancel 函數(shù)
          // 同時做了防止多次重復cancel 之前 Redux 還有React 源碼中也有類似的案列
          const resolvePromise;
          this.promise = new Promise(resolve => {
          resolvePromise = resolve;
          })
          this.reason = undefined;

          const cancel = (message) => {
          if(this.reason) {
          return;
          }
          this.reason = 'cancel' + message;
          resolvePromise(this.reason);
          }
          exactor(cancel)
          }

          throwIfRequested() {
          if(this.reason) {
          throw this.reason
          }
          }

          // source 其實本質(zhì)上是一個語法糖 里面做了封裝
          static source() {
          const cancel;
          const token = new cancelToken(function executor(c) {
          cancel = c;
          });
          return {
          token: token,
          cancel: cancel
          };
          }

          }

          截止到這里大體axios 大體功能已經(jīng)給出。

          接下來我就測試下我的手寫axios 有沒有什么問題?

           <script type="module" >
          import Axios from './axios.js';
          const config = { url:'http://101.132.113.6:3030/api/mock' }
          const axios = new Axios();
          axios.request(config).then(res => {
          console.log(res,'0000')
          }).catch(err => {
          console.log(err)
          })
          </script>

          打開瀏覽器看一下結(jié)果:

          成功了ok, 然后我來測試一下攔截器的功能:代碼更新成下面這樣:

          import Axios from './axios.js';
          const config = { url:'http://101.132.113.6:3030/api/mock' }
          const axios = new Axios();
          // 在axios 實例上掛載屬性
          const err = () => {}
          axios.interceptors.request.use((config)=> {
          console.log('我是請求攔截器1')
          config.id = 1;
          return config
          },err )
          axios.interceptors.request.use((config)=> {
          config.id = 2
          console.log('我是請求攔截器2')
          return config
          },err)
          axios.interceptors.response.use((data)=> {
          console.log('我是響應(yīng)攔截器1',data )
          data += 1;
          return data;
          },err)
          axios.interceptors.response.use((data)=> {
          console.log('我是響應(yīng)攔截器2',data )
          return data
          },err)
          axios.request(config).then(res => {
          // console.log(res,'0000')
          // return res;
          }).catch(err => {
          console.log(err)
          })

          ajax 請求的結(jié)果 我是resolve(1) ,所以我們看下輸出路徑:

          沒什么問題, 響應(yīng)后的數(shù)據(jù)我加了1。

          接下來我來是取消請求的兩種方式

          // 第一種方式
          let cancelFun = undefined;
          const cancelInstance = new cancelToken((c)=>{
          cancelFun = c;
          });
          config.cancelToken = cancelInstance;
          // 50 ms 就取消請求
          setTimeout(()=>{
          cancelFun('取消成功')
          },50)

          第二種方式:
          const { token, cancel } = cancelToken.source();
          config.cancelToken = token;
          setTimeout(()=>{
          cancel()
          },50)

          結(jié)果都是OK的,至此axios簡單源碼終于搞定了。

          反思

          本篇文章只是把axios源碼的大體流程走了一遍, axios源碼內(nèi)部還是做了很多兼容比如:配置優(yōu)先級:他有一個mergeConfig 方法, 還有數(shù)據(jù)轉(zhuǎn)換器。不過這些不影響我們對axios源碼的整體梳理, 源碼中其實有一個createInstance,至于為什么有?我覺得就是為了可擴展性更好, 將來有啥新功能,直接在原有axios的實例的原型鏈上去增加,代碼可維護性強, axios.all spread 都是實例new出來再去掛的,不過都很簡單,沒啥的。有興趣大家自行閱讀。

          如果覺得這篇文章還不錯
          點擊下面卡片關(guān)注我
          來個【分享、點贊、在看】三連支持一下吧

             “分享、點贊、在看” 支持一波 

          瀏覽 56
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  人妻免费在线视频 | 日韩无码三级 | 婷婷撸一撸 | 亚洲无码性爱video | 欧美日本在线播放 |