基于 TS 对 axios 的封装和 api 自动生成

基于 TSaxios 的封装和 api 自动生成

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 1 天,点击查看活动详情

本文内容为自己练习封装和一点点自己的想法, 而做的一个库. 如果你喜欢, 或者觉得对你有用, 能帮助到你,那我就很开心了.

本文仓库: jp-axios

一.基于 axios 的封装

1.1 先说说为什么要封装

  1. 抽离网络模块统一提供管理点

    • 在图一中, 我们分别在 组件ABC 中使用到了, axios, 我们需要分别要在 ABC 中引入 axios 进行使用,这样的缺点在于

      • 重复引入相同依赖

      • 在每个组件内对 axios 进行了强度依赖, 当 axios 发生升级停止维护或者有重大 BUG 的时候,维护难度大

      • 每次调用 axios 所使用的的请求地址,方式,参数等,可能存在相同, 但存在与多个调用点, 更新难度大

        图一:

        image.png

    • 在图二中,我们可以再封装层中, 提供请求方式, 将所有的接口统一在这里管理,然后提供给不同组件调用,这样的好处是

      • 使用的封装层是自身项目代码,没有第三方依赖度, 只有封装层依赖了一次 axios
      • 每个组件的请求方式统一,且仅仅只需要维护封装层,就可以达到维护了所有使用到封装方法的组件

      图二:

      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

axios 已经为我们封装好了, 我们可以通过 signal 或者 cancelToken 配置,进行取消, 我感觉不需要单独封装了, 可以像这样使用, 毕竟需要取消请求的需求很少, 需要取消的也是特定的接口

const instance = new JPAxios()

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

二. 通过 Swagger 文档生直接成 TS 模块 API

通过封装 axios 让我想到一个问题, 既然封装了 axios ,提供了入参字段, 响应字段, 为什么不直接连 所有接口 一并封装好呢? 于是乎,我做了这件事情

通过后端提供的 swagger 文档或者是 OpenApi 的信息,直接生成封装好的所有内容, 可以看下下面这张图

generate.webp

image.png

jp-api.gif

我们可以直接生成这些, 是不是,就可以节约很多功夫呢?

所以我做了这个功能, 欢迎大家来体验一下, 可能会存在一些问题,但是这是也是自己开源的一步,有bug希望大家在评论或者 GitHub 仓库的 issues 中提出.

具体使用方法, 在 README

仓库地址: jp-axios

如果喜欢, 请给一个 star ,谢谢

猜你喜欢

转载自juejin.im/post/7083434985356525598