高扩展性的企业级axios二次封装

483 阅读4分钟

准备工作和内容大概

  1. 安装axios和axios-retry
  2. 封装成一个通用类Vaxios
  3. 基于插件化的思想,扩展axios功能
  4. 导出特殊用法的request、http、axios方法

安装依赖

# pnpm 
pnpm add axios axios-retry
# yarn
yarn add axios axios-retry
# npm
npm i axios axios-retry

目录结构

image.png

  1. Vaxios基于axios封装的类
  2. cancel实现取消重复请求
  3. jsonp实现jsonp请求
  4. plugins基于扩展内容封装成一个个插件
  5. types定义的一些常用类型
  6. index主导出入口

扩展AxiosRequestConfig配置项

// axios-retry包里面已经定义了
declare module 'axios' {
    interface AxiosRequestConfig {
        'axios-retry'?: IAxiosRetryConfigExtended;
    }
}

// 本地项目中
declare module 'axios' {
  interface AxiosRequestConfig {
    jsonp?: JsonpConfig// 支持jsonp的配置
    requestKey?: number | string | symbol//请求唯一key,用于获取被捕获的错误信息
    ignoreCancelToken?: boolean// 支持取消清除重复请求
  }
}

一些类型定义

// types.ts
import { Vaxios } from './Vaxios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { JsonpConfig } from './jsonp'
export type RequestInterceptor = {
  onFulfilled: Parameters<UseRequestInterceptors>['0']
  onRejected: Parameters<UseRequestInterceptors>['1']
}
export type ResponseInterceptor = {
  onFulfilled: Parameters<UseResponseInterceptors>['0']
  onRejected: Parameters<UseResponseInterceptors>['1']
}
export type UseRequestInterceptors<V = AxiosRequestConfig> = (
  onFulfilled?: ((value: V) => V | void) | null,
  onRejected?: ((error: any) => void) | null,
) => RequestInterceptor
export type UseResponseInterceptors<V = AxiosResponse> = (
  onFulfilled?: ((value: V) => Partial<V> | void) | null,
  onRejected?: ((error: any) => void) | null,
) => ResponseInterceptor
export type Plugin = (
  vAxios: Vaxios,
  axios: AxiosInstance,
  config: AxiosRequestConfig,
) => void
export { Vaxios }
declare module 'axios' {
  interface AxiosRequestConfig {
    jsonp?: JsonpConfig
    requestKey?: number | string | symbol
    ignoreCancelToken?: boolean
  }
}

基于axios封装Vaxios类

import axios from 'axios'
import { merge } from 'lodash-unified'
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import type {
  Plugin,
  RequestInterceptor,
  ResponseInterceptor,
  UseRequestInterceptors,
  UseResponseInterceptors,
} from './types'
export class Vaxios {
  // 存放axios实例
  private axios: AxiosInstance
  // 存放配置信息
  private config: AxiosRequestConfig
  // 存放请求拦截器钩子
  private requestInterceptors = new Array<RequestInterceptor>()
  // 存放响应拦截器钩子
  private responseInterceptors = new Array<ResponseInterceptor>()
  // 存放插件
  private pluginArr = new Array<Plugin>()
  constructor(config: AxiosRequestConfig) {
    this.axios = axios.create(config)
    this.config = config
    this.setupInterceptors()
  }
  // 注册插件,支持链式调用
  public use(plugin: Plugin) {
    plugin(this, this.axios, this.config)
    this.pluginArr.push(plugin)
    return this
  }
  // 直接使用插件数组注册
  public plugins(pluginArr: Array<Plugin>) {
    pluginArr.forEach((plugin) => this.use(plugin))
  }
  // 返回axios实例,便于使用axios实例的方法和属性
  public getAxios() {
    return this.axios
  }
  // 注册拦截器钩子
  private setupInterceptors() {
    this.axios.interceptors.request.use(
      (config) => {
        const configArr = this.requestInterceptors
          .map(({ onFulfilled }) => onFulfilled?.(config))
          .filter((item) => !!item && typeof item === 'object')
        return merge(config, ...configArr)
      },
      (error) => {
        this.requestInterceptors.forEach(({ onRejected }) =>
          onRejected?.(error),
        )
        return Promise.reject(error)
      },
    )
    this.axios.interceptors.response.use(
      (response) => {
        const responseArr = this.responseInterceptors
          .map(({ onFulfilled }) => onFulfilled?.(response))
          .filter((item) => !!item && typeof item === 'object')
        return merge(response, ...responseArr)
      },
      (error) => {
        this.responseInterceptors.forEach(({ onRejected }) =>
          onRejected?.(error),
        )
        return Promise.reject(error)
      },
    )
  }
  // 请求拦截器注册方法
  useRequestInterceptors: UseRequestInterceptors = (
    onFulfilled,
    onRejected,
  ) => {
    const interceptors = { onFulfilled, onRejected }
    this.requestInterceptors.push(interceptors)
    return interceptors
  }
  // 只注册错误请求钩子
  useRequestCatchInterceptors(onRejected: RequestInterceptor['onRejected']) {
    return this.useRequestInterceptors(null, onRejected)
  }
  // 响应拦截器注册方法
  useResponseInterceptors: UseResponseInterceptors = (
    onFulfilled,
    onRejected,
  ) => {
    const interceptors = { onFulfilled, onRejected }
    this.responseInterceptors.push(interceptors)
    return interceptors
  }
  // 只注册错误响应钩子
  useResponseCatchInterceptors(onRejected: ResponseInterceptor['onRejected']) {
    return this.useResponseInterceptors(null, onRejected)
  }
  // 注销请求拦截器钩子
  offRequestInterceptor(interceptor: RequestInterceptor) {
    const idx = this.requestInterceptors.indexOf(interceptor)
    if (idx >= 0) {
      this.requestInterceptors.splice(idx, 1)
      return true
    }
    return false
  }
  // 注销响应拦截器钩子
  offResponseInterceptor(interceptor: ResponseInterceptor) {
    const idx = this.responseInterceptors.indexOf(interceptor)
    if (idx >= 0) {
      this.responseInterceptors.splice(idx, 1)
      return true
    }
    return false
  }
}
export default Vaxios

实现取消重复请求功能

封装AxiosCanceler类

import type { AxiosRequestConfig } from 'axios'

// 用于存储每个请求的标识和取消函数
const pendingMap = new Set<string>()

const getPendingUrl = (config: AxiosRequestConfig): string => {
  return [config.method, config.url].join('&')
}

export class AxiosCanceler {
  /**
   * 添加请求
   * @param config 请求配置
   */
  public addPending(config: AxiosRequestConfig): void {
    const url = getPendingUrl(config)
    const controller = new AbortController()
    if (pendingMap.has(url)) {
      config.signal = controller.signal
      controller.abort(url)
    } else {
      pendingMap.add(url)
    }
  }

  /**
   * 清除所有等待中的请求
   */
  public removeAllPending(): void {
    this.reset()
  }

  /**
   * 移除请求
   * @param config 请求配置
   */
  public removePending(config: AxiosRequestConfig): void {
    const url = getPendingUrl(config)
    if (pendingMap.has(url)) {
      // 给一些延迟时间防止一瞬间大量请求
      requestAnimationFrame(() => {
        pendingMap.delete(url)
      })
    }
  }

  /**
   * 重置
   */
  public reset(): void {
    pendingMap.clear()
  }
}

实现cancelPlugin 插件

import axios from 'axios'
import { AxiosCanceler } from './cancel'
import type {
  AxiosError,
  AxiosRequestConfig,
} from 'axios'
import type { Vaxios } from './types'
export const cancelPlugin = (vAxios: Vaxios) => {
  const axiosCanceler = new AxiosCanceler()
  vAxios.useRequestInterceptors((config) => {
    const ignoreCancelToken = config?.ignoreCancelToken
    if (!ignoreCancelToken) {
      axiosCanceler.addPending(config)
    }
  })
  vAxios.useResponseInterceptors(
    (response) => {
      const config = response.config
      const ignoreCancelToken = config?.ignoreCancelToken
      if (response && !ignoreCancelToken) {
        axiosCanceler.removePending(config)
      }
    },
    (error) => {
      // 遇见canceled错误不释放资源
      if (axios.isCancel(error)) {
        return
      }
      const config = (error as AxiosError)?.config
      if (config && !config.ignoreCancelToken) {
        // 出错释放资源
        axiosCanceler.removePending(config as AxiosRequestConfig)
      }
    },
  )
}

实现jsonp功能

  1. jsonp的实现是参考axios-jsonp这个包
  2. jsonp主要是用axios的adapter去实现的

实现jsonpAdapter

import axios from 'axios'
import type {
  AxiosPromise,
  AxiosRequestConfig,
  AxiosResponse,
  InternalAxiosRequestConfig,
} from 'axios'
let cid = 1
function buildParams(params: Record<string, any>) {
  const result: Array<string> = []

  for (const i in params) {
    result.push(
      `${window.encodeURIComponent(i)}=${window.encodeURIComponent(params[i])}`,
    )
  }

  return result.join('&')
}
export type JsonpConfig = {
  callbackName?: string
}
export default function jsonpAdapter(
  config: AxiosRequestConfig,
  jsonpConfig: JsonpConfig,
) {
  return new Promise((resolve, reject) => {
    let script: HTMLScriptElement | null = document.createElement('script')
    let src = config.url ?? ''
    // 拿到aborted信息,实现重复请求的取消
    const isAbort = !!config.signal?.aborted
    const onError = () => {
      remove()
      reject(
        new axios.AxiosError(
          'Jsonp Error',
          'JSONP_ERROR',
          config as InternalAxiosRequestConfig,
        ),
      )
    }
    // 被取消了本次的jsonp的请求
    if (isAbort) {
      onError()
      return
    }
    if (config.params) {
      const params = buildParams(config.params)

      if (params) {
        src += (src.includes('?') ? '&' : '?') + params
      }
    }

    script.async = true

    function remove() {
      if (script) {
        //@ts-ignore
        script.onload = script.onreadystatechange = script.onerror = null

        if (script.parentNode) {
          script.parentNode.removeChild(script)
        }

        script = null
      }
    }

    const jsonp = `axiosJsonpCallback${cid++}`
    //@ts-ignore
    const old = window[jsonp]
    //@ts-ignore
    window[jsonp] = function (responseData) {
      //@ts-ignore
      window[jsonp] = old

      if (isAbort) {
        return
      }

      const response: AxiosResponse = {
        data: responseData,
        headers: {},
        statusText: 'ok',
        config: config as InternalAxiosRequestConfig,
        status: 200,
      }
      resolve(response)
    }

    const additionalParams = {
      _: Date.now(),
    }
    //@ts-ignore
    additionalParams[jsonpConfig.callbackName || 'callback'] = jsonp

    src += (src.includes('?') ? '&' : '?') + buildParams(additionalParams)

    //@ts-ignore
    script.onload = script.onreadystatechange = function () {
      //@ts-ignore
      if (!script.readyState || /loaded|complete/.test(script.readyState)) {
        remove()
      }
    }
    script.onerror = function () {
      onError()
    }

    script.src = src
    document.head.appendChild(script)
  }) as AxiosPromise
}

实现jsonpPlugin

import axiosJsonpAdapter from './jsonp'
import axios from 'axios'
import type {
  AxiosInstance,
  AxiosPromise,
  AxiosRequestConfig,
} from 'axios'
/**
 * 配置jsonp插件
 * @param axiosInstance axios实例
 */
export const jsonpPlugin = (_: Vaxios, axiosInstance: AxiosInstance) => {
  const adapter = axiosInstance.defaults.adapter
  const jsonpAdapter = (config: AxiosRequestConfig): AxiosPromise => {
    if (config.jsonp) {
      return axiosJsonpAdapter(config, config.jsonp)
    }
    config.adapter = adapter
    return axios(config)
  }
  axiosInstance.defaults.adapter = jsonpAdapter
}

实现出现错误重新请求功能

  1. 基于axios-retry这个包的功能
  2. 基于vaxios的高扩展性实现retryPlugin插件
import axiosRetry from 'axios-retry'
import type {
  AxiosInstance,
  AxiosRequestConfig,
} from 'axios'
import type { Vaxios } from './types'
export const retryPlugin = (
  _: Vaxios,
  axiosInstance: AxiosInstance,
  config: AxiosRequestConfig,
) => {
  const retry = config?.['axios-retry']
  axiosRetry(axiosInstance, {
    ...(retry || {}),
    retries: retry?.retries || 3,
    retryDelay: retry?.retryDelay || axiosRetry.exponentialDelay,
    retryCondition(error) {
      // 重复请求取消产生的错误不进行重式
      if (axios.isCancel(error)) {
        return false
      }
      return retry?.retryCondition?.(error) ?? true
    },
  })
}

组合我们的插件并导出

import axiosRetry from 'axios-retry'
import axios from 'axios'
import axiosJsonpAdapter from './jsonp'
import { AxiosCanceler } from './cancel'
import type {
  AxiosError,
  AxiosInstance,
  AxiosPromise,
  AxiosRequestConfig,
} from 'axios'
import type { Vaxios } from './types'
/**
 *配置重传插件
 * @param axios axios实例
 * @param config 配置项
 */
export const retryPlugin = (
  _: Vaxios,
  axiosInstance: AxiosInstance,
  config: AxiosRequestConfig,
) => {
  const retry = config?.['axios-retry']
  axiosRetry(axiosInstance, {
    ...(retry || {}),
    retries: retry?.retries || 3,
    retryDelay: retry?.retryDelay || axiosRetry.exponentialDelay,
    retryCondition(error) {
      // 重复请求取消产生的错误不进行重式
      if (axios.isCancel(error)) {
        return false
      }
      return retry?.retryCondition?.(error) ?? true
    },
  })
}

/**
 * 配置jsonp插件
 * @param axiosInstance axios实例
 */
export const jsonpPlugin = (_: Vaxios, axiosInstance: AxiosInstance) => {
  const adapter = axiosInstance.defaults.adapter
  const jsonpAdapter = (config: AxiosRequestConfig): AxiosPromise => {
    if (config.jsonp) {
      return axiosJsonpAdapter(config, config.jsonp)
    }
    config.adapter = adapter
    return axios(config)
  }
  axiosInstance.defaults.adapter = jsonpAdapter
}
/**
 *清除重复请求插件
 * @param _ axios实例
 * @param __ 配置项
 * @param vAxios vAxios实例
 */
export const cancelPlugin = (vAxios: Vaxios) => {
  const axiosCanceler = new AxiosCanceler()
  vAxios.useRequestInterceptors((config) => {
    const ignoreCancelToken = config?.ignoreCancelToken
    if (!ignoreCancelToken) {
      axiosCanceler.addPending(config)
    }
  })
  vAxios.useResponseInterceptors(
    (response) => {
      const config = response.config
      const ignoreCancelToken = config?.ignoreCancelToken
      if (response && !ignoreCancelToken) {
        axiosCanceler.removePending(config)
      }
    },
    (error) => {
      // 遇见canceled错误不释放资源
      if (axios.isCancel(error)) {
        return
      }
      const config = (error as AxiosError)?.config
      if (config && !config.ignoreCancelToken) {
        // 出错释放资源
        axiosCanceler.removePending(config as AxiosRequestConfig)
      }
    },
  )
}

export default [retryPlugin, jsonpPlugin, cancelPlugin]


index.ts入口文件导出

import axios from 'axios'
import Vaxios from './Vaxios'
import plugins from './plugins'
import type { AxiosError, AxiosRequestConfig } from 'axios'
export * from './types'
export * from './Vaxios'
export * from './plugins'
const request = async <D = any, E = AxiosError>(
  conf: AxiosRequestConfig,
  vaxios: Vaxios,
): Promise<[E, null] | [null, D]> => {
  let response = null
  let error = null
  // 通过唯一请求key获取被捕获的错误信息
  const requestKey = Symbol('requestKey')
  // 通过拦截器获取错误信息
  const interceptor = vaxios.useResponseCatchInterceptors(
    (axiosError: AxiosError) => {
      const { config } = axiosError ?? {}
      if (config?.requestKey === requestKey) {
        error = axiosError
      }
    },
  )
  try {
    response = await vaxios.getAxios().request({ ...conf, requestKey })
  } catch (err) {
    error = err
  }
  vaxios.offResponseInterceptor(interceptor)
  return [error as E, response?.data]
}
export const createRequest = (config: AxiosRequestConfig) => {
  const vaxios = new Vaxios(config)
  vaxios.plugins(plugins)
  const currentRequest = <D = any, E = AxiosError>(
    conf: AxiosRequestConfig,
  ) => {
    return request<D, E>(conf, vaxios)
  }
  currentRequest['vaxios'] = vaxios
  currentRequest['axios'] = vaxios.getAxios()
  return currentRequest
}
createRequest['axios'] = axios

export default createRequest

使用方式

项目中创建axios.ts文件

import { createRequest as _ } from '@xxxx/axios'
import type { Plugin } from '@xxxx/axios'
// 全局配置
_.axios.defaults.timeout = 6000

// 创建request实例
const $ = _({
  headers: { 'Content-Type': 'application/json;charset=UTF-8' },
})

// 合并请求配置request
$.vaxios.useRequestInterceptors(
  (conf) => {
    console.log(conf, 'conf')

    return {
      headers: {
        // "x-hello":'hello'
      },
    }
  },
  // 不会捕获错误信息
  (err) => {
    console.log(err, '请求错误')
  },
)

// 合并响应数据response
$.vaxios.useResponseInterceptors(
  (response) => {
    console.log(response, 'response')

    return {
      data: {
        // "hello2":"xxx"
      },
    }
  },
  // 不会捕获错误信息
  (err) => {
    console.log(err, '响应错误')
  },
)

// 使用自定义插件实现formData axios实例和request配置,vaxios实例
const checkErrorStatusPlugin: Plugin = (vaxios) => {
  vaxios.useResponseCatchInterceptors((error) => {
    let errMessage = ''

    // 根据业务情况
    const status = error?.response?.status
    // 根据业务情况来
    const msg: string = error?.response?.data?.error?.message ?? ''
    switch (status) {
      case 400:
        errMessage = `${msg}`
        break
      case 401:
        errMessage = msg || '用户没有权限(令牌、用户名、密码错误)!'
        break
      case 403:
        errMessage = '用户得到授权,但是访问是被禁止的。!'
        break
      // 404请求不存在
      case 404:
        errMessage = '网络请求错误,未找到该资源!'
        break
      case 405:
        errMessage = '网络请求错误,请求方法未允许!'
        break
      case 408:
        errMessage = '网络请求超时!'
        break
      case 500:
        errMessage = '服务器错误,请联系管理员!'
        break
      case 501:
        errMessage = '网络未实现!'
        break
      case 502:
        errMessage = '网络错误!'
        break
      case 503:
        errMessage = '服务不可用,服务器暂时过载或维护!'
        break
      case 504:
        errMessage = '网络超时!'
        break
      case 505:
        errMessage = 'http版本不支持该请求!'
        break
      default:
    }

    if (errMessage) {
      // 这里对errMessage进行处理比如弹出弹窗或者信息
      window.confirm(errMessage)
    }
  })
}

// 使用插件
$.vaxios.use(checkErrorStatusPlugin)

// 使用axios实例的请求拦截器
$.axios.interceptors.request.use(
  (config) => {
    return config
  },
  // 捕获请求错误
  (error) => {
    console.log(error, '请求错误拦截')
  },
)

// 使用axios实例的响应拦截器
$.axios.interceptors.response.use(
  (response) => {
    return response
  },
  // 捕获响应错误
  (error) => {
    console.log(error, '响应错误拦截')
  },
)

// 导出
export default $
// 使用axios的request
export const request = $.axios.request
// 使用axios的post
export const http = $.axios.post

项目中使用

import request from "./axios"
type Data={
  name:string,
  password:string
}
const [error, data] = await request<Data>({
  url: 'https://example/xxx',
  method:'GET',
  "axios-retry":{
    retries:3
  },
  ignoreCancelToken:false,
  // jsonp: {
  //   callbackName: 'callback',
  // },
})