Based on TS encapsulation of axios and automatic generation of api

TSPair axios-based packaging and apiautomatic generation

Get into the habit of writing together! This is the first day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

The content of this article is a library made for myself to practice encapsulation and a little bit of my own ideas. If you like it, or think it is useful to you and can help you, then I am very happy.

This article warehouse: jp-axios

1. Based on axiosthe package

1.1 Let’s talk about why we need to encapsulate

  1. The network module is extracted to provide a unified management point

    • In Figure 1, we 组件ABChave used it in , axiosand we need ABCto introduce axiosand use it in , respectively. The disadvantage of this is that

      • Introduce the same dependency repeatedly

      • There axiosis , and maintenance is difficult when axiosan upgrade occurs and maintenance is stopped or there is a BUGmajor problem.

      • axiosThe request address, method, parameters, etc. used for each call may be the same, but there are multiple call points, which is difficult to update

        Figure 1:

        image.png

    • In Figure 2, we can provide the request method in the encapsulation layer, unified management of all interfaces here, and then provide it to different components to call. The advantage is that

      • The encapsulation layer used is its own project code, there is no third-party dependency, only the encapsulation layer depends onceaxios
      • The request method of each component is unified, and it is only necessary to maintain the encapsulation layer to maintain all components that use the encapsulation method.

      Figure II:

      image.png

  2. 避免 axios或者使用的第三方网络请求依赖, 停止维护,或者发生重大 BUG, 自身团队又无法解决的时候,需要更换时, 升级更新维护难度飙升的情况, 就像现在浏览器都在发展 fetch, 我们要知道,所有的东西,在官方声明后,之前的产物都会退出历史舞台,就比如 co, 推出 async/await 之后, 就退出了历史舞台了

1.2 封装 `axios

1.2.1 基础封装

第一步: 增强 axios 的配置参数

// ./types/index.ts
/**
 * @description 扩展 `AxiosRequestConfig`, 使用时可以传递专属拦截器
 */
export interface JPRequestConfig<T = JPResponse, K = JPResponse> extends AxiosRequestConfig {
  /**
   * @description 拦截器
   */
  interceptors?: JPInterceptors<T, K>
}

/**
 * @description 封装拦截器接口
 * 响应拦截其中,我们提供两个泛型参数, 
 * 因为有时候我们入参是被 `axios` 包装的响应对象,
 * 但是需要返回它的 `data` 属性,才是我们接口的实际返回值
 */
export interface JPInterceptors<T = JPResponse, K = JPResponse> {
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestInterceptorCatch?: (err: any) => any
  responseInterceptor?: (res: T) => K
  responseInterceptorCatch?: (err: any) => any
}

/**
 * @description 将响应类型给外部使用,统一提供,避免导出引入`AxiosResponse`
 */
export interface JPResponse<T = any> extends AxiosResponse<T, any> {}

/**
 * @description 判断拦截器的入参和出参
 */
export type Interceptor<T, K extends boolean = false> = K extends true ? JPResponse<T> : T
复制代码

第二步: 使用 class 进行类封装, 更方便创建多个实例,适用范围更广,封装性更强一些。

// ./jp-axios.ts
import axios from 'axios'
import type { AxiosInstance } from 'axios'
import type { JPRequestConfig, JPResponse, JPInterceptors, Interceptor } from './types'

class JPAxios {
    instance: AxiosInstance
    // 实例拦截器, 第一组实力拦截器,类型必然是`axios`包装的类型,所以用`JPResponse<T>`包裹
    constructor(config?: JPRequestConfig<JPResponse<T>, T>) {
        // 1.创建实例,并保存
    	this.instance = axios.create(config)
    	this.interceptors = config?.interceptors

    	// 定义的实例拦截器
    	config?.interceptors && this.use(config?.interceptors as any)
  	}
    
    // 封装统一请求的方法
    request(config: JPRequestConfig): Promise<T> {
    	// ...
  	}
    
    // 封装拦截器注册方法
    // 泛型T: 响应拦截器出参类型
    // 泛型K: 表示入参是否是`axios`的响应包装类型
    use<T = JPResponse, K extends boolean = false>(interceptors: JPInterceptors<Interceptor<T, K>, T>) {
        /**
         * Tips: 拦截器执行结构
         *    - Q:请求  S: 响应  F: 服务器
         *    - 如果对顺序要求,可以通过设置对应的拦截顺序进行修改,机制如下
         *    - Q1/S1 拦截器1
         *    - Q2/S2 拦截器2
         *    - Q3/S3 拦截器3
         *
         *         F         F  服务器响应
         *    Q1   ↑    S1   ↓
         *    Q2   ↑    S2   ↓
         *    Q3   ↑    S3   ↓
         * 浏览器发送
         */
        // 实例请求拦截器
        this.instance.interceptors.request.use(
          interceptors?.requestInterceptor,
          interceptors?.requestInterceptorCatch
        )
        // 实例响应拦截器
        this.instance.interceptors.response.use(
          interceptors?.responseInterceptor as any,
          interceptors?.responseInterceptorCatch
        )
    }
}
复制代码

第三步: 封装统一请求方法, 作为其他方法模板

// ./jp-axios.ts
class JPAxios {
    // ...
    // 封装统一请求的方法,作为模板
    request<T = JPResponse>(config: JPRequestConfig<T, T>): Promise<T> {
      return new Promise((resolve, reject) => {
        // 定义请求拦截器
        if (config.interceptors?.requestInterceptor)
          config = config.interceptors?.requestInterceptor(config)

        // 进行请求
        this.instance
          .request<any, T>(config)
          .then((res) => {
            // 定义接口响应拦截器
            if (config.interceptors?.responseInterceptor)
              res = config.interceptors?.responseInterceptor(res)

            resolve(res)
          })
          .catch((err) => {
            reject(err)
          })
      })
    }
    
    get<T = JPResponse>(config: JPRequestConfig<T, T>): Promise<T> {
        return this.request({ ...config, method: 'GET' })
    }
    // ...
}
复制代码

在基础封装完毕之后, 我们为所有的实例提供了多种方式的拦截器, 分别是实例拦截器, 接口拦截器, 让接口的可控性更强了,我们可以像这样去使用它

// 1.创建实例
const instance = new JPAxios({
  baseURL: 'http://localhost:3000',
  timeout: 5000,
})

// 2.正常使用
instance
  .get('/users')
  .then((res) => {
    console.log(res)
  })
  .catch((err) => {
    console.log(err)
  })

// 3.如果需要拦截器可以使用实例拦截器
const instance1 = new JPAxios({
  baseURL: 'http://localhost:3000',
  timeout: 5000,
  interceptors: {
    requestInterceptor: (config) => {
      // 可以在请求前做一些处理
      return config
    },
    requestInterceptorCatch: (err) => {
      // 如果发送请求失败,可以在这里做一些处理
      console.log(err)
    },
    responseInterceptor: (response) => {
      // 可以在请求后做一些处理
      return response
    },
    responseInterceptorCatch: (err) => {
      // 如果请求失败,可以在这里做一些处理
      console.log(err)
    },
  },
})
// 4.你也可以使用实例的方法来创建拦截器
instance1.use({
  requestInterceptor(params) {
    // you can do something
  },
  requestInterceptorCatch(params) {
    // you can do something
  },
  responseInterceptor(params) {
    // you can do something
  },
  responseInterceptorCatch(params) {
    // you can do something
  },
})

// 5.如果你需要针对单独接口做特殊处理,也可以定义接口拦截器
instance1.get({
  url: '/users',
  interceptors: {
      requestInterceptor(params) {
    	// you can do something
      },
      requestInterceptorCatch(params) {
        // you can do something
      },
      responseInterceptor(params) {
        // you can do something
      },
      responseInterceptorCatch(params) {
        // you can do something
      }
  }
})
复制代码

1.2.2 增加取消重复请求的功能

第一步: 为 JPRequestConfig 添加开关参数

// ./types/index.ts
export interface JPRequestConfig<T = JPResponse, K = JPResponse> extends AxiosRequestConfig {
  /**
   * @description 拦截器
   */
  interceptors?: JPInterceptors<T, K>
  /**
   * @description 是否删除重复请求
   */
  removeRepeat?: boolean
}
复制代码

第二步: 在构造函数内提供对应的实现

// ./jp-axios.ts
class JPAxios {
    instance: AxiosInstance
    // 当前实例开启取消重复请求功能
    removeRepeat: boolean
    // 存储对应请求的取消方法
  	private abortControllers = new Map<string, AbortController | Canceler>()
    constructor(config?: JPRequestConfig<JPResponse<T>, T>) {
        // 1.创建实例,并保存
    	this.instance = axios.create(config)
    	this.interceptors = config?.interceptors
        
        // 是否开启
        this.removeRepeat = !!config?.removeRepeat
        if (this.removeRepeat) {
            const requestInterceptor = (config: JPRequestConfig) => {
                // 如果有重复的,在这里就去除掉
                this.removePending(config)
                // 添加下次的判定
                this.addPending(config)
                return config
            }

            const responseInterceptor = (response: JPResponse) => {
                // 请求成功或失败都去除对应请求
                this.removePending(response.config)
                return response
            }

            const responseInterceptorCatch = (error: AxiosError) => {
                error.config && this.removePending(error.config)
                return Promise.reject(error)
            }
            // 建立最上层拦截器, 对相同请求,进行处理
            this.use({ requestInterceptor, responseInterceptor, responseInterceptorCatch })
        }

    	// 定义的实例拦截器
    	config?.interceptors && this.use(config?.interceptors as any)
  	}
    // ...
    /** ******* 根据配置提供是否删除重复请求  **********/
    /**
     * 生成每个请求唯一的键
     */
    private getPendingKey(config: JPRequestConfig): string {
        let { data } = config
        const { url, method, params } = config
        if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象
        return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
    }

    /**
     * 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
     */
    private addPending(config: JPRequestConfig) {
        const pendingKey = this.getPendingKey(config)
        config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
            if (!this.abortControllers.has(pendingKey))
                this.abortControllers.set(pendingKey, cancel)
        })
    }

    /**
     * 删除重复的请求
     */
    private removePending(config: JPRequestConfig) {
        const pendingKey = this.getPendingKey(config)
        // 如果同样的请求,之前存在,则取消
        if (this.abortControllers.has(pendingKey)) {
            const cancelToken = this.abortControllers.get(pendingKey)
            cancelToken!(pendingKey)
            this.abortControllers.delete(pendingKey)
        }
    }
}
复制代码

1.2.3 使用

通过拦截器, 取消重复请求,我们其实就可以完成大量日常业务需求, 还有一项平常的需求是 loading ,我也提供了对应的参数, 在 JPRequestConfig

// ./types/index.ts
/**
 * @description 扩展 `AxiosRequestConfig`, 使用时可以传递专属拦截器
 */
export interface JPRequestConfig<T = JPResponse, K = JPResponse> extends AxiosRequestConfig {
  /**
   * @description 拦截器
   */
  interceptors?: JPInterceptors<T, K>

  /**
   * @description 请求状态开始,是否展示loading
   */
  loading?: boolean

  /**
   * @description 是否删除重复请求
   */
  removeRepeat?: boolean
}
复制代码

所以我们可以这样去使用

// 1.全局的`loading`什么时候开启, 大多数情况下,`loading`都在我们的组件内部,但是有时候我们需要控制全屏的 `loading` 效果, 又不是全部接口都需要,所以可以这样用
const instance = new JPAxios({
    interceptors: {
        requestInterceptor: (config) => {
          	if (config.loading) GlobalLoading.start()
            return config
        },
        responseInterceptor: (response) => {
            // 可以在请求后做一些处理
            if (GlobalLoading.isStart) GlobalLoading.done()
            return response
        },
        responseInterceptorCatch: (err) => {
            // 如果请求失败,可以在这里做一些处理
            if (GlobalLoading.isStart) GlobalLoading.done()
            console.log(err)
        },
    },
})

instance.request({ url: '/user/info', loading: true }) // 这样可以再拦截器中获取到,并且针对特定接口开启全局`loading`效果了

// 2.取消重复请求
// 2.1 实例开启, 当前实例所有接口,都会检测是否重复
const instance1 = new JPAxios({ removeRepeat: true })

// 2.2 单独接口使用
instance.request({ url: '/user/info', loading: true, removeRepeat: true })
复制代码

关于取消请求:

可以看下MDN的描述: XMLHttpRequest ,

XMLHttpRequest 对象是我们发起一个网络请求的根本,在它底下有怎么一个方法 .abort(),就是中断一个已被发出的请求。

image.png

axiosIt has been encapsulated for us. We signalcan cancelTokencancel it through or configuration. I don’t think it needs to be encapsulated separately. It can be used like this. After all, there are very few needs to cancel the request, and it is also a specific interface that needs to be canceled.

const instance = new JPAxios()

const controller = new AbortController()
instance.request({ 
    url: '/user/info', 
    signal: controller.signal
})
// 需要取消,则调用`abort`方法
controller.abort()
复制代码

Swagger2. Generate TSmodules directly through document generationAPI

The encapsulation axiosreminds me of a question, since it is encapsulated axios, the input parameter field and the response field are provided, why not directly connect all interfaces and encapsulate them together? So, I did this thing

Through the swaggerdocument or OpenApithe information provided by the backend, all the encapsulated content can be directly generated, you can see the following picture

generate.webp

image.png

jp-api.gif

We can generate these directly, isn't it, we can save a lot of effort?

So I made this function, and you are welcome to experience it. There may be some problems, but this is also a step of my own open source. If there are bugs, I hope you can put them in the comments or in the GitHubwarehouse issues.

For specific usage, in README

Warehouse address: jp-axios

If you like it, please give a star, thank you

Guess you like

Origin juejin.im/post/7083434985356525598