前言
最近想写一个可以适配多平台的请求库,在研究 xhr 和 fetch 发现二者的参数、响应、回调函数等差别很大。想到如果请求库想要适配多平台,需要统一的传参和响应格式,那么势必会在请求库内部做大量的判断,这样不但费时费力,还会屏蔽掉底层请求内核差异。
阅读 axios 和 umi-request 源码时想到,请求库其实基本都包含了拦截器、中间件和快捷请求等几个通用的,与具体请求过程无关的功能。然后通过传参,让用户接触底层请求内核。问题在于,请求库内置多个底层请求内核,内核支持的参数是不一样的,上层库可能做一些处理,抹平一些参数的差异化,但对于底层内核的特有的功能,要么放弃,要么只能在参数列表中加入一些具体内核的特有的参数。比如在 axios 中,它的请求配置参数列表中,罗列了一些 browser only的参数,那对于只需要在 node 环境中运行的 axios 来说,参数多少有些冗余,并且如果 axios 要支持其他请求内核(比如小程序、快应用、华为鸿蒙等),那么参数冗余也将越来越大,扩展性也差。
换个思路来想,既然实现一个适配多平台的统一的请求库有这些问题,那么是否可以从底向上的,针对不同的请求内核,提供一种方式可以很方便的为其赋予拦截器、中间件、快捷请求等几个通用功能,并且保留不同请求内核的差异化?
设计实现
我们的请求库要想与请求内核无关,那么只能采用内核与请求库相分离的模式。使用时,需要将请求内核传入,初始化一个实例,再进行使用。或者基于请求库,传入内核,预置请求参数来进行二次封装。
基本架构
首先实现一个基本的架构
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())
// 创建实例
const prequest = new PreQuest(adapter)
// 这里实际调用的是 adapter 函数
prequest.request({ url: 'http://localhost:3000/api' })
复制代码可以看到,这里饶了个弯,通过实例方法调用了 adapter 函数。
这样的话,为修改请求和响应提供了想象空间。
class PreQuest {
    // ...some code
    
    async request(opt){
        const options = modifyReqOpt(opt)
        const res = await this.adapter(options)
        return modifyRes(res)
    }
    // ...some code
}
复制代码中间件
可以采用 koa 的洋葱模型,对请求进行拦截和修改。
中间件调用示例:
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)
})
复制代码实现中间件基本模型?
const compose =  require('koa-compose')
class Middleware {
    // 中间件列表
    cbs = []
    
    // 注册中间件
    use(cb) {
       this.cbs.push(cb)
       return this
    }
    
    // 执行中间件
    exec(ctx, next){
        // 中间件执行细节不是重点,所以直接使用 koa-compose 库
        return compose(this.cbs)(ctx, next)
    }
}
复制代码全局中间件,只需要添加一个 use 和 exec 的静态方法即可。
PreQuest 继承自 Middleware 类,即可在实例上注册中间件。
那么怎么在请求前调用中间件?
class PreQuest extends Middleware {
    // ...some code
     
    async request(opt) {
    
        const ctx = {
            request: opt,
            response: {}
        }
        
        // 执行中间件
        async this.exec(ctx, async (ctx) => {
            ctx.response = await this.adapter(ctx.request)
        })
        
        return ctx.response
    }
        
    // ...some code
}
复制代码中间件模型中,前一个中间件的返回值是传不到下一个中间件中,所以是通过一个对象在中间件中传递和赋值。
拦截器
拦截器是修改参数和响应的另一种方式。
首先看一下 axios 中拦截器是怎么用的。
import axios from 'axios'
const instance = axios.create()
instance.interceptor.request.use(
    (opt) => modifyOpt(opt),
    (e) => handleError(e)
)
复制代码根据用法,我们可以实现一个基本结构
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)
    }
}
复制代码代码很简单,有点难度的就是拦截器的执行了。这里主要有两个知识点: Array.reduce 和 Promise.then 第二个参数的使用。
注册拦截器时,successHandler 与 errorHandler 是成对的, successHandler 中抛出的错误,要在对应的 errorHandler 中处理,所以 errorHandler 接收到的错误,是上一个拦截器中抛出的。
拦截器怎么使用呢?
class PreQuest {
    // ... some code
    interceptor = {
        request: new Interceptor()
        response: new Interceptor()
    }
    
    // ...some code
    
    async request(opt){
        
        // 执行拦截器,修改请求参数
        const options = await this.interceptor.request.exec(opt)
        
        const res = await this.adapter(options)
        
        // 执行拦截器,修改响应数据
        const response = await this.interceptor.response.exec(res)
        
        return response
    }
    
}
复制代码拦截器中间件
拦截器也可以是一个中间件,可以通过注册中间件来实现。请求拦截器在 await next() 前执行,响应拦截器在其后。
const instance = new Middleware()
instance.use(async (ctx, next) => {
    // Promise 链式调用,更改请求参数
    await Promise.resolve().then(reqInterceptor1).then(reqInterceptor2)...
    // 执行下一个中间件、或执行到 this.adapter 函数
    await next()
    // Promise 链式调用,更改响应数据
    await Promise.resolve().then(resInterceptor1).then(resInterceptor2)...
})
复制代码拦截器有请求拦截器和响应拦截器两类。
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)
    }
}
复制代码使用
const instance = new Middleware()
const interceptor = new InterceptorMiddleware()
// 注册拦截器
interceptor.request.use(
    (opt) => modifyOpt(opt),
    (e) => handleError(e)
)
// 注册到中间中
instance.use(interceptor.register)
复制代码实战
以微信小程序为例。小程序中自带的 wx.request 并不好用。使用上面我们封装的代码,可以很容易的打造出一个小程序请求库。
封装小程序原生请求
将原生小程序请求 Promise 化,设计传参 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,
    })
  })
}
复制代码调用
const instance = new PreQuest(adapter)
// 中间件模式
instance.use(async (ctx, next) => {
    // 修改请求参数
    ctx.request.path = '/prefix' + ctx.request.path
    
    await next()
    
    // 修改响应
    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' })
复制代码其它
获取 wx.request 请求实例、兼容多个小程序平台等请参阅 @prequest/miniprogram
结语
上面的内容中,我们基本实现了一个与请求内核无关的请求库,并且设计了两种拦截请求和响应的方式,我们可以根据自己的需求和喜好自由选择。
这种内核装卸的方式非常容易扩展。当面对一个 axios 不支持的平台时,也不用费劲的去找开源好用的请求库了。我相信很多人在开发小程序的时候,基本都有去找 axios-miniprogram 的解决方案。通过我们的 PreQuest 项目,可以体验到类似 axios 的能力。
PreQuest 项目中,除了上面提到的内容,还提供了全局配置、全局中间件、别名请求等功能。项目中也有基于 PreQuest 封装的请求库,@prequest/miniprogram,@prequest/fetch…也针对一些使用原生 xhr、fetch 等 API 的项目,提供了一种不侵入的方式来赋予 PreQuest的能力 @prequest/wrapper





















![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)
