由封裝一個(gè)請(qǐng)求庫(kù)所想到的...

前言

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

閱讀 axios 和 umi-request 源碼時(shí)想到彻亲,請(qǐng)求庫(kù)其實(shí)基本都包含了攔截器、中間件和快捷請(qǐng)求等幾個(gè)通用的吮廉,與具體請(qǐng)求過(guò)程無(wú)關(guān)的功能苞尝。然后通過(guò)傳參,讓用戶接觸底層請(qǐng)求內(nèi)核宦芦。問(wèn)題在于宙址,請(qǐng)求庫(kù)內(nèi)置多個(gè)底層請(qǐng)求內(nèi)核,內(nèi)核支持的參數(shù)是不一樣的调卑,上層庫(kù)可能做一些處理抡砂,抹平一些參數(shù)的差異化,但對(duì)于底層內(nèi)核的特有的功能恬涧,要么放棄舀患,要么只能在參數(shù)列表中加入一些具體內(nèi)核的特有的參數(shù)。比如在 axios 中气破,它的請(qǐng)求配置參數(shù)列表中,羅列了一些 browser only的參數(shù)旷痕,那對(duì)于只需要在 node 環(huán)境中運(yùn)行的 axios 來(lái)說(shuō)欺抗,參數(shù)多少有些冗余绞呈,并且如果 axios 要支持其他請(qǐng)求內(nèi)核(比如小程序佃声、快應(yīng)用圾亏、華為鴻蒙等)志鹃,那么參數(shù)冗余也將越來(lái)越大,擴(kuò)展性也差捧杉。

換個(gè)思路來(lái)想淳玩,既然實(shí)現(xiàn)一個(gè)適配多平臺(tái)的統(tǒng)一的請(qǐng)求庫(kù)有這些問(wèn)題蜕着,那么是否可以從底向上的承匣,針對(duì)不同的請(qǐng)求內(nèi)核韧骗,提供一種方式可以很方便的為其賦予攔截器袍暴、中間件政模、快捷請(qǐng)求等幾個(gè)通用功能耗式,并且保留不同請(qǐng)求內(nèi)核的差異化刊咳?

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

我們的請(qǐng)求庫(kù)要想與請(qǐng)求內(nèi)核無(wú)關(guān)娱挨,那么只能采用內(nèi)核與請(qǐng)求庫(kù)相分離的模式让蕾。使用時(shí),需要將請(qǐng)求內(nèi)核傳入笋婿,初始化一個(gè)實(shí)例足丢,再進(jìn)行使用斩跌∫唬或者基于請(qǐng)求庫(kù)袖订,傳入內(nèi)核洛姑,預(yù)置請(qǐng)求參數(shù)來(lái)進(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' })

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

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

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

    // ...some code
}

中間件

可以采用 koa 的洋蔥模型酝碳,對(duì)請(qǐng)求進(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)
})

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

const compose =  require('koa-compose')

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

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

PreQuest 繼承自 Middleware 類蛮放,即可在實(shí)例上注冊(cè)中間件包颁。

那么怎么在請(qǐng)求前調(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
}

中間件模型中娩嚼,前一個(gè)中間件的返回值是傳不到下一個(gè)中間件中佃迄,所以是通過(guò)一個(gè)對(duì)象在中間件中傳遞和賦值呵俏。

攔截器

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

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

import axios from 'axios'

const instance = axios.create()

instance.interceptor.request.use(
    (opt) => modifyOpt(opt),
    (e) => handleError(e)
)

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

class Interceptor {
    cbs = []
    
    // 注冊(cè)攔截器
    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)
    }
}

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

注冊(cè)攔截器時(shí),successHandlererrorHandler 是成對(duì)的稽揭, successHandler 中拋出的錯(cuò)誤溪掀,要在對(duì)應(yīng)的 errorHandler 中處理揪胃,所以 errorHandler 接收到的錯(cuò)誤喊递,是上一個(gè)攔截器中拋出的。

攔截器怎么使用呢?

class PreQuest {
    // ... some code
    interceptor = {
        request: new Interceptor()
        response: new Interceptor()
    }
    
    // ...some code
    
    async request(opt){
        
        // 執(zhí)行攔截器铐伴,修改請(qǐng)求參數(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
    }
    
}

攔截器中間件

攔截器也可以是一個(gè)中間件即供,可以通過(guò)注冊(cè)中間件來(lái)實(shí)現(xiàn)逗嫡。請(qǐng)求攔截器在 await next() 前執(zhí)行驱证,響應(yīng)攔截器在其后抹锄。

const instance = new Middleware()

instance.use(async (ctx, next) => {
    // Promise 鏈?zhǔn)秸{(diào)用伙单,更改請(qǐng)求參數(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)...
})

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

class InterceptorMiddleware {
    request = new Interceptor()
    response = new Interceptor()
    
    // 注冊(cè)中間件
    register: async (ctx, next) {
        ctx.request = await this.request.exec(ctx.request)
        await next()
        ctx.response = await thie.response.exec(ctx.response)
    }
}

使用

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

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

// 注冊(cè)到中間中
instance.use(interceptor.register)

類型請(qǐng)求

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

實(shí)現(xiàn)很簡(jiǎn)單留荔,只需要遍歷 HTTP 請(qǐng)求類型聚蝶,并將其掛在 this 下即可

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

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

簡(jiǎn)單請(qǐng)求

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

axios('http://localhost:3000/api').then(res => console.log(res))

我將這種請(qǐng)求方式稱之為簡(jiǎn)單請(qǐng)求验靡。

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

不使用 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))

// 簡(jiǎn)單調(diào)用
PreQuest('/api').then(res => console.log(res))

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

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

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

簡(jiǎ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
    }
}

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

方式二: 還可以使用 Proxy 代理一個(gè)空函數(shù)撵术,來(lái)劫持訪問(wèn)背率。

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)
          },
        })
    }
}

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

const prequest = PreQuest.create(adapter)

prequest instanceof PreQuest  // false

個(gè)人目前還沒(méi)有想到寝姿,判斷 prequest 是不是 PreQuest 實(shí)例有什么用交排,并且也沒(méi)有想到好的解決辦法。有解決方案的請(qǐng)?jiān)谠u(píng)論里告訴我饵筑。

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

Demo如下:

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

const PreQuest = new Proxy(InnerPreQuest, {
    construct(_, args) {
        return () => InnerPreQuest.create(...args)
    }
})

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

實(shí)戰(zhàn)

以微信小程序?yàn)槔剐弧P〕绦蛑凶詭У?wx.request 并不好用徙邻。使用上面我們封裝的代碼帅容,可以很容易的打造出一個(gè)小程序請(qǐng)求庫(kù)麦乞。

封裝小程序原生請(qǐng)求

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

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,
    })
  })
}

調(diào)用

const instance = PreQuest.create(adapter)

// 中間件模式
instance.use(async (ctx, next) => {
    // 修改請(qǐng)求參數(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' })

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

首先看一下在小程序中怎樣中斷請(qǐng)求

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

request.abort()

使用我們封裝的這一層膨处,將拿不到原生請(qǐng)求實(shí)例测摔。

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

function adapter(opt) {
    const { getWxInstance } = opt
    
    return new Promise(() => {
        
        getWxInstance(
            wx.request(
               // some code
            )
        )
        
    })
}

用法如下:

const instance = PreQuest.create(adapter)

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

setTimeout(() => {
    nativeRequest.abort()
})

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

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

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

結(jié)語(yǔ)

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

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

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

參考

axios: https://github.com/axios/axios

umi-request:https://github.com/umijs/umi-request

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末屈扎,一起剝皮案震驚了整個(gè)濱河市埃唯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鹰晨,老刑警劉巖墨叛,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異模蜡,居然都是意外死亡漠趁,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門忍疾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)闯传,“玉大人,你說(shuō)我怎么就攤上這事卤妒∩蹋” “怎么了字币?”我有些...
    開封第一講書人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)共缕。 經(jīng)常有香客問(wèn)我洗出,道長(zhǎng),這世上最難降的妖魔是什么图谷? 我笑而不...
    開封第一講書人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任翩活,我火速辦了婚禮,結(jié)果婚禮上便贵,老公的妹妹穿的比我還像新娘菠镇。我一直安慰自己,他們只是感情好承璃,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開白布利耍。 她就那樣靜靜地躺著,像睡著了一般绸硕。 火紅的嫁衣襯著肌膚如雪堂竟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,050評(píng)論 1 291
  • 那天玻佩,我揣著相機(jī)與錄音,去河邊找鬼席楚。 笑死咬崔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的烦秩。 我是一名探鬼主播垮斯,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼只祠!你這毒婦竟也來(lái)了兜蠕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤抛寝,失蹤者是張志新(化名)和其女友劉穎熊杨,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盗舰,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晶府,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钻趋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片川陆。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蛮位,靈堂內(nèi)的尸體忽然破棺而出较沪,到底是詐尸還是另有隱情鳞绕,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布尸曼,位于F島的核電站们何,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏骡苞。R本人自食惡果不足惜垂蜗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望解幽。 院中可真熱鬧贴见,春花似錦、人聲如沸躲株。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)霜定。三九已至档悠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間望浩,已是汗流浹背辖所。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留磨德,地道東北人缘回。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像典挑,于是被迫代替她去往敵國(guó)和親酥宴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容