Preface

Recently, I want to write a request library that can adapt to multiple platforms. After studying xhr and fetch, I found that the parameters, responses, and callback functions of the two are very different. Thinking that if the request library wants to adapt to multiple platforms, a unified parameter transmission and response format is required, then a lot of judgments will be made inside the request library, which not only takes time and effort, but also shields the underlying request core differences.

When reading the source code of axios and umi-request, I thought that the request library basically contains several general functions such as interceptors, middleware, and shortcut requests, which have nothing to do with the specific request process. Then pass parameters to let users contact the underlying request kernel. The problem is that the request library has built-in multiple low-level request cores, and the parameters supported by the cores are different. The upper-level library may do some processing to smooth out the differentiation of some parameters, but for the unique functions of the bottom-level kernel, either give up or just Some specific kernel-specific parameters can be added to the parameter list. For example, in axios, its request configuration parameter list lists some browser only parameters. For axios that only needs to run in the node environment, the parameters are somewhat redundant, and if axios needs to support other requests The kernel (such as small programs, fast applications, Huawei Hongmeng, etc.), then the parameter redundancy will become larger and larger, and the scalability is also poor.

Think about it another way. Since the realization of a unified request library that adapts to multiple platforms has these problems, can it be possible to provide a way for different request cores from the bottom up to easily assign interceptors and intermediates to them? Several general functions such as software, shortcut request, etc., and retain the differentiation of different request kernels?

Design and implementation

If our request library is not related to the request kernel, we can only adopt the mode of separating the kernel from the request library. When using it, you need to pass in the request kernel, initialize an instance, and then use it. Or based on the request library, pass into the kernel, and preset request parameters for secondary packaging.

Basic structure

First implement a basic architecture

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

As you can see, there is a twist here, the adapter function is called through the instance method.

In this way, imagination space is provided for modifying the request and response.

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

    // ...some code
}

Middleware

You can use koa's onion model to intercept and modify requests.

Examples of middleware calls:

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

Realize the basic model of middleware?

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

For global middleware, you only need to add a static method of use and exec.

PreQuest inherits from the Middleware class, you can register the middleware on the instance.

So how to call the middleware before the request?

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
}

In the middleware model, the return value of the previous middleware cannot be passed to the next middleware, so it is passed and assigned in the middleware through an object.

Interceptor

Interceptors are another way to modify parameters and responses.

First look at how the interceptor is used in axios.

import axios from 'axios'

const instance = axios.create()

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

According to usage, we can realize a basic structure

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

The code is very simple, the implementation of the interceptor is a bit difficult. There are two main knowledge points here: Array.reduce and the use of the second parameter of Promise.then.

When registering the interceptor, successHandler and errorHandler are paired. Errors thrown in the successHandler must be handled in the corresponding errorHandler, so the errors received by the errorHandler are thrown in the previous interceptor.

How to use the interceptor?

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

Interceptor middleware

The interceptor can also be a middleware, which can be implemented by registering the middleware. The request interceptor is await next() before 0609b51b8a81e8, and the response interceptor is after it.

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

There are two types of interceptors: request interceptors and response interceptors.

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

use

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

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

// 注册到中间中
instance.use(interceptor.register)

Type request

Here I call instance.get('/api') a type request. If the type request is integrated in the library, it will inevitably pollute the parameters of the adapter function passed in from outside. Because it is necessary to get and path /api and mix them into the parameters, there is usually a need to modify the path in the middleware.

The implementation is very simple, just traverse the HTTP request type and hang it under this

class PreQuest {
    constructor(private adapter) {
        this.mount()
    }
    
    // 挂载所有类型的别名请求
    mount() {
       methods.forEach(method => {
           this[method] = (path, opt) => {
             // 混入 path 和 method 参数
             return this.request({ path, method, ...opt })
           }
       })
    }
    
    // ...some code

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

Simple request

In axios, you can directly use the following form to call

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

I call this request method a simple request.

How do we implement this kind of request method here?

Instead of using class, it is better to use the traditional function class notation. You only need to determine whether the function is called new, and then execute different logic inside the function.

The demo is as follows

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

// new 调用
const instance = new PreQuest(adapter)
instance.get('/api').then(res => console.log(res))

// 简单调用
PreQuest('/api').then(res => console.log(res))

If the class is written, function calls cannot be made. We can make a fuss on the class instance.

First initialize an instance, take a look at the usage

const prequest = new PreQuest(adapter)

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

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

What is instantiated by new is an object, and the object cannot be executed as a function, so the object cannot be created in the form of new.

axios.create axios to generate an instance, you can get inspiration from it. If the .create method returns a function, all the methods on the new object are hung on the function. In this way, our needs can be achieved.

Simple design:

Method 1: Copy the method on the prototype

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

Note: In some versions of es, the for in loop can not traverse the method of class generation instance prototype.

Method 2: You can also use Proxy to proxy an empty function to hijack access.

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

The disadvantage of the above two methods is that the create method will no longer be PreQuest , that is

const prequest = PreQuest.create(adapter)

prequest instanceof PreQuest  // false

Individuals have not yet thought of the use of judging whether prequest is the PreQuest instance, and have not thought of a good solution. If there is a solution, please let me know in the comments.

The .create create an'instance' using 0609b51b8a83e2 may not be intuitive, and we can also hijack the new operation through Proxy.

The demo is as follows:

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

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

In the process of writing the code, I chose to hijack the adapter function ), which produced many unexpected effects. . . You can think about it for a few minutes, and then look at the document and source code

Actual combat

Take the WeChat applet as an example. wx.request that comes with the applet is not easy to use. Using the code we encapsulated above, we can easily create a small program request library.

Encapsulate Mini Program Native Request

Promise the native applet request and design the parameter opt object

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

transfer

const instance = PreQuest.create(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' })

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

instance.post('/api', { baseURL: 'http://loclahost:3000' })

Obtain a native request instance

First look at how to interrupt the request in the applet

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

request.abort()

Using this layer we encapsulate will not get the native request instance.

So what to do? We can start by passing the parameters

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

The usage is as follows:

const instance = PreQuest.create(adapter)

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

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

It should be noted that: because wx.request is executed after n middleware and interceptors, there are a large number of asynchronous tasks in it, so the nativeRequest obtained through the above can only be executed in asynchronous.

Compatible with multi-platform applets

I checked several small program platforms and quick apps, and found that the request method is the one set of small programs. In fact, we can take wx.request and pass it in when creating an instance.

Conclusion

In the above content, we have basically implemented a request library that has nothing to do with the request core, and designed two ways to intercept requests and responses. We can choose freely according to our needs and preferences.

This way of loading and unloading the kernel is very easy to extend. When faced with a platform that axios does not support, there is no need to look for open source and easy-to-use request libraries. I believe that many people basically look for axios-miniprogram solutions when they are developing small programs. Through our PreQuest project, you can experience axios-like capabilities.

PreQuest project, in addition to the content mentioned above, also provides global configuration, global middleware, alias request and other functions. The project also has a request library PreQuest @prequest/miniprogram , @prequest/fetch ... It also provides a non-invasive way for some projects that use native xhr, fetch and other APIs. The ability @prequest/wrapper

reference

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

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


LuckyHH
75 声望14 粉丝

在校大学生