<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>

          封裝一個(gè)網(wǎng)絡(luò)請求庫要考慮的內(nèi)容

          共 17819字,需瀏覽 36分鐘

           ·

          2021-06-08 12:55

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

          回復(fù)1,加入高級 Node 進(jìn)階交流群

          前言

          最近想寫一個(gè)可以適配多平臺(tái)的請求庫,在研究 xhr 和 fetch 發(fā)現(xiàn)二者的參數(shù)、響應(yīng)、回調(diào)函數(shù)等差別很大。想到如果請求庫想要適配多平臺(tái),需要統(tǒng)一的傳參和響應(yīng)格式,那么勢必會(huì)在請求庫內(nèi)部做大量的判斷,這樣不但費(fèi)時(shí)費(fèi)力,還會(huì)屏蔽掉底層請求內(nèi)核差異。

          閱讀 axios 和 umi-request 源碼時(shí)想到,請求庫其實(shí)基本都包含了攔截器、中間件和快捷請求等幾個(gè)通用的,與具體請求過程無關(guān)的功能。然后通過傳參,讓用戶接觸底層請求內(nèi)核。問題在于,請求庫內(nèi)置多個(gè)底層請求內(nèi)核,內(nèi)核支持的參數(shù)是不一樣的,上層庫可能做一些處理,抹平一些參數(shù)的差異化,但對于底層內(nèi)核的特有的功能,要么放棄,要么只能在參數(shù)列表中加入一些具體內(nèi)核的特有的參數(shù)。比如在 axios 中,它的請求配置參數(shù)列表中,羅列了一些 browser only的參數(shù),那對于只需要在 node 環(huán)境中運(yùn)行的 axios 來說,參數(shù)多少有些冗余,并且如果 axios 要支持其他請求內(nèi)核(比如小程序、快應(yīng)用、華為鴻蒙等),那么參數(shù)冗余也將越來越大,擴(kuò)展性也差。

          換個(gè)思路來想,既然實(shí)現(xiàn)一個(gè)適配多平臺(tái)的統(tǒng)一的請求庫有這些問題,那么是否可以從底向上的,針對不同的請求內(nèi)核,提供一種方式可以很方便的為其賦予攔截器、中間件、快捷請求等幾個(gè)通用功能,并且保留不同請求內(nèi)核的差異化?

          設(shè)計(jì)實(shí)現(xiàn)

          我們的請求庫要想與請求內(nèi)核無關(guān),那么只能采用內(nèi)核與請求庫相分離的模式。使用時(shí),需要將請求內(nèi)核傳入,初始化一個(gè)實(shí)例,再進(jìn)行使用?;蛘呋谡埱髱?,傳入內(nèi)核,預(yù)置請求參數(shù)來進(jìn)行二次封裝。

          基本架構(gòu)

          首先實(shí)現(xiàn)一個(gè)基本的架構(gòu)

          class PreQuest {
              constructor(private adapter)
              
              request(opt) {
                  return this.adapter(opt)
              }
          }

          const adapter = (opt) => nativeRequestApi(opt)
          // eg: const adapter = (opt) => fetch(opt).then(res => res.json())

          // 創(chuàng)建實(shí)例
          const prequest = new PreQuest(adapter)

          // 這里實(shí)際調(diào)用的是 adapter 函數(shù)
          prequest.request({ url: 'http://localhost:3000/api' })
          復(fù)制代碼

          可以看到,這里饒了個(gè)彎,通過實(shí)例方法調(diào)用了 adapter 函數(shù)。

          這樣的話,為修改請求和響應(yīng)提供了想象空間。

          class PreQuest {
              // ...some code
              
              async request(opt){
                  const options = modifyReqOpt(opt)
                  const res = await this.adapter(options)
                  return modifyRes(res)
              }

              // ...some code
          }
          復(fù)制代碼

          中間件

          可以采用 koa 的洋蔥模型,對請求進(jìn)行攔截和修改。

          中間件調(diào)用示例:

          const prequest = new PreQuest(adapter)

          prequest.use(async (ctx, next) => {
              ctx.request.path = '/perfix' + ctx.request.path
              await next()
              ctx.response.body = JSON.parse(ctx.response.body)
          })
          復(fù)制代碼

          實(shí)現(xiàn)中間件基本模型?

          const compose =  require('koa-compose')

          class Middleware {
              // 中間件列表
              cbs = []
              
              // 注冊中間件
              use(cb) {
                 this.cbs.push(cb)
                 return this
              }
              
              // 執(zhí)行中間件
              exec(ctx, next){
                  // 中間件執(zhí)行細(xì)節(jié)不是重點(diǎn),所以直接使用 koa-compose 庫
                  return compose(this.cbs)(ctx, next)
              }
          }
          復(fù)制代碼

          全局中間件,只需要添加一個(gè) use 和 exec 的靜態(tài)方法即可。

          PreQuest 繼承自 Middleware 類,即可在實(shí)例上注冊中間件。

          那么怎么在請求前調(diào)用中間件?

          class PreQuest extends Middleware {
              // ...some code
               
              async request(opt) {
              
                  const ctx = {
                      request: opt,
                      response: {}
                  }
                  
                  // 執(zhí)行中間件
                  async this.exec(ctx, async (ctx) => {
                      ctx.response = await this.adapter(ctx.request)
                  })
                  
                  return ctx.response
              }
                  
              // ...some code
          }

          復(fù)制代碼

          中間件模型中,前一個(gè)中間件的返回值是傳不到下一個(gè)中間件中,所以是通過一個(gè)對象在中間件中傳遞和賦值。

          攔截器

          攔截器是修改參數(shù)和響應(yīng)的另一種方式。

          首先看一下 axios 中攔截器是怎么用的。

          import axios from 'axios'

          const instance = axios.create()

          instance.interceptor.request.use(
              (opt) => modifyOpt(opt),
              (e) => handleError(e)
          )
          復(fù)制代碼

          根據(jù)用法,我們可以實(shí)現(xiàn)一個(gè)基本結(jié)構(gòu)

          class Interceptor {
              cbs = []
              
              // 注冊攔截器
              use(successHandler, errorHandler) {
                  this.cbs.push({ successHandler, errorHandler })
              }
              
              exec(opt) {
                return this.cbs.reduce(
                  (t, c, idx) => t.then(c.successHandler, this.handles[idx - 1]?.errorHandler),
                  Promise.resolve(opt)
                )
                .catch(this.handles[this.handles.length - 1].errorHandler)
              }
          }
          復(fù)制代碼

          代碼很簡單,有點(diǎn)難度的就是攔截器的執(zhí)行了。這里主要有兩個(gè)知識(shí)點(diǎn): Array.reduce 和 Promise.then 第二個(gè)參數(shù)的使用。

          注冊攔截器時(shí),successHandler 與 errorHandler 是成對的, successHandler 中拋出的錯(cuò)誤,要在對應(yīng)的 errorHandler 中處理,所以 errorHandler 接收到的錯(cuò)誤,是上一個(gè)攔截器中拋出的。

          攔截器怎么使用呢?

          class PreQuest {
              // ... some code
              interceptor = {
                  request: new Interceptor()
                  response: new Interceptor()
              }
              
              // ...some code
              
              async request(opt){
                  
                  // 執(zhí)行攔截器,修改請求參數(shù)
                  const options = await this.interceptor.request.exec(opt)
                  
                  const res = await this.adapter(options)
                  
                  // 執(zhí)行攔截器,修改響應(yīng)數(shù)據(jù)
                  const response = await this.interceptor.response.exec(res)
                  
                  return response
              }
              
          }
          復(fù)制代碼

          攔截器中間件

          攔截器也可以是一個(gè)中間件,可以通過注冊中間件來實(shí)現(xiàn)。請求攔截器在 await next() 前執(zhí)行,響應(yīng)攔截器在其后。

          const instance = new Middleware()

          instance.use(async (ctx, next) => {
              // Promise 鏈?zhǔn)秸{(diào)用,更改請求參數(shù)
              await Promise.resolve().then(reqInterceptor1).then(reqInterceptor2)...
              // 執(zhí)行下一個(gè)中間件、或執(zhí)行到 this.adapter 函數(shù)
              await next()
              // Promise 鏈?zhǔn)秸{(diào)用,更改響應(yīng)數(shù)據(jù)
              await Promise.resolve().then(resInterceptor1).then(resInterceptor2)...
          })
          復(fù)制代碼

          攔截器有請求攔截器和響應(yīng)攔截器兩類。

          class InterceptorMiddleware {
              request = new Interceptor()
              response = new Interceptor()
              
              // 注冊中間件
              register: async (ctx, next) {
                  ctx.request = await this.request.exec(ctx.request)
                  await next()
                  ctx.response = await thie.response.exec(ctx.response)
              }
          }
          復(fù)制代碼

          使用

          const instance = new Middleware()
          const interceptor = new InterceptorMiddleware()

          // 注冊攔截器
          interceptor.request.use(
              (opt) => modifyOpt(opt),
              (e) => handleError(e)
          )

          // 注冊到中間中
          instance.use(interceptor.register)
          復(fù)制代碼

          類型請求

          這里我把類似 instance.get('/api') 這樣的請求叫做類型請求。庫中集成類型請求的話,難免會(huì)對外部傳入的adapter 函數(shù)的參數(shù)進(jìn)行污染。因?yàn)樾枰獮檎埱蠓绞?nbsp;get 和路徑 /api 分配鍵名,并且將其混入到參數(shù)中,通常在中間件中會(huì)有修改路徑的需求。

          實(shí)現(xiàn)很簡單,只需要遍歷 HTTP 請求類型,并將其掛在 this 下即可

          class PreQuest {
              constructor(private adapter) {
                  this.mount()
              }
              
              // 掛載所有類型的別名請求
              mount() {
                 methods.forEach(method => {
                     this[method] = (path, opt) => {
                       // 混入 path 和 method 參數(shù)
                       return this.request({ path, method, ...opt })
                     }
                 })
              }
              
              // ...some code

              request(opt) {
                  // ...some code
              }
          }

          復(fù)制代碼

          簡單請求

          axios 中,可以直接使用下面這種形式進(jìn)行調(diào)用

          axios('http://localhost:3000/api').then(res => console.log(res))
          復(fù)制代碼

          我將這種請求方式稱之為簡單請求。

          我們這里怎么實(shí)現(xiàn)這種寫法的請求方式呢?

          不使用 class ,使用傳統(tǒng)函數(shù)類寫法的話比較好實(shí)現(xiàn),只需要判斷函數(shù)是否是 new 調(diào)用,然后在函數(shù)內(nèi)部執(zhí)行不同的邏輯即可。

          demo 如下

          function PreQuest({
              if(!(this instanceof PreQuest)) {
                  console.log('不是new 調(diào)用')
                  return // ...some code
              }
             
             console.log('new調(diào)用'
             
             //... some code
          }

          // new 調(diào)用
          const instance = new PreQuest(adapter)
          instance.get('/api').then(res => console.log(res))

          // 簡單調(diào)用
          PreQuest('/api').then(res => console.log(res))
          復(fù)制代碼

          class 寫法的話,不能進(jìn)行函數(shù)調(diào)用。我們可以在 class 實(shí)例上做文章。

          首先初始化一個(gè)實(shí)例,看一下用法

          const prequest = new PreQuest(adapter)

          prequest.get('http://localhost:3000/api')

          prequest('http://localhost:3000/api')
          復(fù)制代碼

          通過 new 實(shí)例化出來的是一個(gè)對象,對象是不能夠當(dāng)做函數(shù)來執(zhí)行,所以不能用 new 的形式來創(chuàng)建對象。

          再看一下 axios 中生成實(shí)例的方法 axios.create, 可以從中得到靈感,如果 .create 方法返回的是一個(gè)函數(shù),函數(shù)上掛上了所有 new 出來對象上的方法,這樣的話,就可以實(shí)現(xiàn)我們的需求。

          簡單設(shè)計(jì)一下:

          方式一: 拷貝原型上的方法

          class PreQuest {

              static create(adapter) {
                  const instance = new PreQuest(adapter)
                  
                  function inner(opt{
                     return instance.request(opt)
                  }
                  
                  for(let key in instance) {
                      inner[key] = instance[key]
                  }
                  
                  return inner
              }
          }
          復(fù)制代碼

          注意: 在某些版本的 es 中,for in 循環(huán)遍歷不出 class 生成實(shí)例原型上的方法。

          方式二: 還可以使用 Proxy 代理一個(gè)空函數(shù),來劫持訪問。

          class PreQuest {
              
              // ...some code

              static create(adapter) {
                  const instance = new PreQuest(adapter)
                 
                  return new Proxy(function (){}, {
                    get(_, name) {
                      return Reflect.get(instance, name)
                    },
                    apply(_, __, args) {
                      return Reflect.apply(instance.request, instance, args)
                    },
                  })
              }
          }
          復(fù)制代碼

          上面兩種方法的缺點(diǎn)在于,通過 create 方法返回的將不再是 PreQuest 的實(shí)例,即

          const prequest = PreQuest.create(adapter)

          prequest instanceof PreQuest  // false
          復(fù)制代碼

          個(gè)人目前還沒有想到,判斷 prequest 是不是 PreQuest 實(shí)例有什么用,并且也沒有想到好的解決辦法。有解決方案的請?jiān)谠u論里告訴我。

          使用 .create 創(chuàng)建 '實(shí)例' 的方式可能不符合直覺,我們還可以通過 Proxy 劫持 new 操作。

          Demo如下:

          class InnerPreQuest {
            create() {
               // ...some code
            }
          }

          const PreQuest = new Proxy(InnerPreQuest, {
              construct(_, args) {
                  return () => InnerPreQuest.create(...args)
              }
          })
          復(fù)制代碼

          在編寫代碼的過程中,我選擇劫持了 adapter 函數(shù),這產(chǎn)生了很多意想不到的效果。。。你可以思考幾分鐘,然后看一下這個(gè)文檔和源碼

          實(shí)戰(zhàn)

          以微信小程序?yàn)槔?。小程序中自帶?nbsp;wx.request 并不好用。使用上面我們封裝的代碼,可以很容易的打造出一個(gè)小程序請求庫。

          封裝小程序原生請求

          將原生小程序請求 Promise 化,設(shè)計(jì)傳參 opt 對象

          function adapter(opt{
            const { path, method, baseURL, ...options } = opt
            const url = baseURL + path
            return new Promise((resolve, reject) => {
              wx.request({
                ...options,
                url,
                method,
                success: resolve,
                fail: reject,
              })
            })
          }

          復(fù)制代碼

          調(diào)用

          const instance = PreQuest.create(adapter)

          // 中間件模式
          instance.use(async (ctx, next) => {
              // 修改請求參數(shù)
              ctx.request.path = '/prefix' + ctx.request.path
              
              await next()
              
              // 修改響應(yīng)
              ctx.response.body = JSON.parse(ctx.response.body)
          })

          // 攔截器模式
          instance.interecptor.request.use(
              (opt) => {
                  opt.path = '/prefix' + opt.path
                  return opt
              }
          )

          instance.request({ path: '/api', baseURL: 'http://localhost:3000' })

          instance.get('http://localhost:3000/api')

          instance.post('/api', { baseURL: 'http://loclahost:3000' })
          復(fù)制代碼

          獲取原生請求實(shí)例

          首先看一下在小程序中怎樣中斷請求

          const request = wx.request({
              // ...some code
          })

          request.abort()
          復(fù)制代碼

          使用我們封裝的這一層,將拿不到原生請求實(shí)例。

          那么怎么辦呢?我們可以從傳參中入手

          function adapter(opt{
              const { getWxInstance } = opt
              
              return new Promise(() => {
                  
                  getWxInstance(
                      wx.request(
                         // some code
                      )
                  )
                  
              })
          }
          復(fù)制代碼

          用法如下:

          const instance = PreQuest.create(adapter)

          let nativeRequest
          instance.post('http://localhost:3000/api', {
              getWxInstance(instance) {
                nativeRequest = instance
              }
          })

          setTimeout(() => {
              nativeRequest.abort()
          })
          復(fù)制代碼

          需要注意的是:因?yàn)?nbsp;wx.request 的執(zhí)行是在 n 個(gè)中間件、攔截器之后執(zhí)行的,里面存在大量異步任務(wù),所以通過上面拿到的 nativeRequest 只能在異步中執(zhí)行。

          兼容多平臺(tái)小程序

          查看了幾個(gè)小程序平臺(tái)和快應(yīng)用,發(fā)現(xiàn)請求方式都是小程序的那一套,那其實(shí)我們完全可以將 wx.request 拿出來,創(chuàng)建實(shí)例的時(shí)候再傳進(jìn)去。

          結(jié)語

          上面的內(nèi)容中,我們基本實(shí)現(xiàn)了一個(gè)與請求內(nèi)核無關(guān)的請求庫,并且設(shè)計(jì)了兩種攔截請求和響應(yīng)的方式,我們可以根據(jù)自己的需求和喜好自由選擇。

          這種內(nèi)核裝卸的方式非常容易擴(kuò)展。當(dāng)面對一個(gè) axios 不支持的平臺(tái)時(shí),也不用費(fèi)勁的去找開源好用的請求庫了。我相信很多人在開發(fā)小程序的時(shí)候,基本都有去找 axios-miniprogram 的解決方案。通過我們的 PreQuest 項(xiàng)目,可以體驗(yàn)到類似 axios 的能力。

          PreQuest 項(xiàng)目中,除了上面提到的內(nèi)容,還提供了全局配置、全局中間件、別名請求等功能。項(xiàng)目中也有基于 PreQuest 封裝的請求庫,@prequest/miniprogram,@prequest/fetch...也針對一些使用原生 xhr、fetch 等 API 的項(xiàng)目,提供了一種不侵入的方式來賦予 PreQuest的能力 @prequest/wrapper

          參考

          axios: github.com/axios/axios

          umi-request:github.com/umijs/umi-r…

          關(guān)于本文

          作者:xdoer

          https://juejin.cn/post/6960254713631604766


          最后

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

             “分享、點(diǎn)贊、在看” 支持一波  

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

          手機(jī)掃一掃分享

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

          手機(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>
                  国产精品久久福利 | 亚洲视频91 | 日韩黄色一级免费片 | 天天爱,天天操 | 精品熟妇视频一区二区三区 |