likes
comments
collection
share

vue3+ts中台项目小结之axios请求封装

作者站长头像
站长
· 阅读数 19

公司平台新搭建了一个中台系统吧,自己在忙别的项目了,几乎没有什么参与度,作为被边缘化的成员,自己还是要努把力,多多学习,争取下进步,加油derder~!

项目是vite搭建的,重点来看下axios文件夹中封装的内容(axios安装过程这里就略过了)。闲话不多说,首先来看下目录结构的:

axios                             
├─ config                         
│  └─ index.ts                    
├─ plugins                        
│  ├─ axios-controller.plugin.ts  
│  ├─ axios-debounce.plugin.ts    
│  ├─ axios-exception.plugin.ts   
│  ├─ axios-extend.plugin.ts      
│  ├─ axios-path-to-regexp.ts     
│  ├─ axios-retry.plugin.ts       
│  └─ axios-token.plugin.ts       
├─ index.ts                       
└─ interface.ts                   

就从axios文件夹中根目录的index.ts文件:

import { axiosExceptionPlugin } from './plugins/axios-exception.plugin'
import axios from 'axios'
import config from './config'
import { getTokenLocalApi } from '@/service/local-api/token/token.local'
import axiosProCreator from './plugins/axios-extend.plugin'
import AxiosDebouncePlugin from './plugins/axios-debounce.plugin'
import { axiosTokenPlugin } from './plugins/axios-token.plugin'
import { axiosRetryPlugin } from './plugins/axios-retry.plugin'
import { axiosPathToRegexp } from './plugins/axios-path-to-regexp'
import { axiosControllerPlugin } from './plugins/axios-controller.plugin'

const fetch = axios.create(config.defaultConfig)
const axiosPro = axiosProCreator(fetch)

/**
 * 异常捕获插件
 */
axiosPro.use(axiosExceptionPlugin, config.pluginConfig.exceptionConfig)

/**
 * 重试插件
 */
axiosPro.use(axiosRetryPlugin, config.pluginConfig.retryPluginConfig)

/**
 * 令牌插件
 */
axiosPro.use(axiosTokenPlugin, {
  tokenResource: async () => (await getTokenLocalApi())?.token,
  tokenName: config.pluginConfig.tokenPluginConfig.name
})

/**
 * path替换插件
 */
axiosPro.use(axiosPathToRegexp)

/**
 * 请求防抖插件
 */
axiosPro.use(new AxiosDebouncePlugin())

/**
 * 控制器插件
 */
axiosPro.use(axiosControllerPlugin)

export default axiosPro

分析:

  • 首先来看axios.create(自定义的配置,例如baseUrl等)来创建一个新的axios命名为fetch(配置文件后面会列出)

接着来看下import config from './config'中的配置内容:

/**
 * @ Description: axios配置
 */
import {
  AXIOS_REQUEST_TIMEOUT,
  AXIOS_RETRIES,
  AXIOS_TOKEN_NAME,
  AXIOS_DELAY_TIME,
  AXIOS_BASE_URL,
  exceptionCode,
  exceptionWhitelist
} from '@/config/axios.config'

export default {
  /**
   * 基础默认配置
   */
  defaultConfig: {
    baseURL: AXIOS_BASE_URL,
    timeout: AXIOS_REQUEST_TIMEOUT
  },

  /**
   * 插件配置
   */
  pluginConfig: {
    /**
     * 重试插件
     */
    retryPluginConfig: {
      retries: AXIOS_RETRIES, // 重试次数
      delayTime: AXIOS_DELAY_TIME // 每次重试之间的实践间隔
    },
    /**
     * 令牌插件
     */
    tokenPluginConfig: {
      name: AXIOS_TOKEN_NAME
    },
    /**
     * 异常捕获配置
     */
    exceptionConfig: {
      /**
       * 异常code
       */
      exceptionCode: exceptionCode,
      /**
       * 白名单
       */
      whitelist: exceptionWhitelist
    }
  }
}

配置文件中,还有配置文件

import { AXIOS_REQUEST_TIMEOUT, AXIOS_RETRIES, AXIOS_TOKEN_NAME, AXIOS_DELAY_TIME, AXIOS_BASE_URL, exceptionCode, exceptionWhitelist } from '@/config/axios.config'

import type { AxiosResponse } from 'axios'

/**
 * 请求超时时长(单位:毫秒)
 */
export const AXIOS_BASE_URL = '/api'

/**
 * 请求超时时长(单位:毫秒)
 */
export const AXIOS_REQUEST_TIMEOUT = 20000
/**
 * 令牌名称
 */
export const AXIOS_TOKEN_NAME = 'Authorization'

/**
 * 令牌名称
 */
export const AXIOS_REFRESH_TOKEN_NAME = 'refresh_token'

/**
 * 令牌刷新时间差 单位:分组
 */
export const AXIOS_REFRESH_DIFFERENCE_MINUTES = 1

/**
 * 令牌头前缀
 */
export const AXIOS_TOKEN_CONTENT_PREFIX = 'Bearer '

/**
 * 失败重试次数
 */
export const AXIOS_RETRIES = 3

/**
 * 失败重试时间间隔
 */
export const AXIOS_DELAY_TIME = 1500

/**
 * 异常code
 */
export const exceptionCode = []

/**
 * 异常白名单
 */
export const exceptionWhitelist = (response: AxiosResponse) => response.data?.status

里面设置了一些常用的涉及请求的常量,这里不一一介绍了的。

index.ts文件中可以看到const axiosPro = axiosProCreator(fetch),那么我们就来看下的这个axiosProCreator是啥

//axiosProCreator
/**
 * @ Description: axios扩展插件
 */

import type { AxiosInstance, AxiosStatic } from 'axios'
import type { AxiosPlugin } from '../interface'

declare module 'axios' {
  interface Axios {
    use(axiosPlugin: AxiosPlugin, options?: any): void
  }
}

export default function axiosProCreator(axios: AxiosStatic | AxiosInstance) {
  axios.use = function (axiosPlugin: AxiosPlugin, options?: any) {
    axiosPlugin.install(axios, options)
  }
  return axios
}

分析

  • ts中的 declare module 意为声明模块。www.bilibili.com/video/BV179…
  • axiosProCreator函数中,为传递进来的axios参数添加了use方法,内部调用了install方法,然后返回参数axios

interface.ts接口类型文件:

import type { AxiosInstance, AxiosStatic } from 'axios'

export interface AxiosPlugin {
  install: (axios: AxiosStatic | AxiosInstance, options: any) => void
}

export interface AxiosPluginInstance {
  install(axios: AxiosStatic | AxiosInstance, options: any): void
}

分析:

  • 定义了接口类型,接口AxiosPluginAxiosPluginInstance都要有一个install方法

接下来看下第一个插件的axiosExceptionPlugin--异常插件:

/**
 * @ Description: 异常插件
 */

import type { AxiosPlugin } from '../interface'
import type { AxiosInstance, AxiosResponse, AxiosStatic } from 'axios'
import isArray from 'lodash/fp/isArray'
import isFunction from 'lodash/fp/isFunction'
interface Options {
  /**
   * 异常code
   */
  exceptionCode: string[]
  /**
   * 白名单
   */
  whitelist: string[] | ((response: AxiosResponse<any, any>) => Boolean)
}

export const axiosExceptionPlugin: AxiosPlugin = {
  install: (axios: AxiosStatic | AxiosInstance, options: Options) => {
    axios.interceptors.response.use(
      (response) => {
        // 2xx 范围内的状态码都会触发该函数。
        const code = response.data?.code
        const whitelist = options.whitelist
        if (
          /**
           * 数组,code在白名单中
           */
          (isArray(whitelist) && whitelist.includes(code)) ||
          /**
           * 函数返回值为真
           */
          (isFunction(whitelist) && whitelist(response))
        ) {
          return response
        }
        return Promise.reject(response.data)
      },
      function (error) {
        // 超出 2xx 范围的状态码都会触发该函数。
        // 对响应错误做点什么
        return Promise.reject(error)
      }
    )
  }
}

分析:

axiosExceptionPlugin插件中有一个install方法,通过axios.interceptors.response做了一层响应拦截,响应码状态码在2xx范围内,或者在白名单中的话,就返回数据response

然后是重试插件-axiosRetryPlugin:

//
/**
 * @ name: axiosRetryPlugin
 * @ Description: 错误重试插件
 */

import { AxiosError, type AxiosInstance, type AxiosStatic } from 'axios'
import type { AxiosPlugin } from '../interface'

interface Options {
  retries: number // 重试次数
  delayTime: number // 每次重试之间的实践间隔为 retry count * delayTime
}

declare module 'axios' {
  interface AxiosRequestConfig {
    retryIndex?: number
  }
}
/**
 * axios重试插件
 * @param axios
 * @returns
 */
export const axiosRetryPlugin: AxiosPlugin = {
  install: (axios: AxiosStatic | AxiosInstance, options: Options) => {
    axios.interceptors.response.use(undefined, function (error) {
      if (error instanceof AxiosError) {
        const { config } = error
        if (config?.retryIndex === undefined) config.retryIndex = 1
        if (config.retryIndex <= options.retries) {
          return new Promise((resolve) => {
            setTimeout(() => {
              if (config.retryIndex) config.retryIndex++
              return resolve(axios(config))
            }, options.delayTime * (config.retryIndex || 1))
          })
        }
        return Promise.reject(error)
      }

      return Promise.reject(error)
    })
    return axios
  }
}

接着继续看下令牌插件-axiosTokenPlugin:

/**
 * @ name:axiosTokenPlugin 
 * @ Description: 令牌插件
 */

import { AXIOS_TOKEN_CONTENT_PREFIX } from '@/config/axios.config'
import type { AxiosInstance, AxiosStatic } from 'axios'
import type { AxiosPlugin } from '../interface'

/**
 * axios token挂载插件
 * @param axios
 * @returns
 */
interface Options {
  tokenName: string // 令牌名称
  tokenResource: () => any // token资源
}

export const axiosTokenPlugin: AxiosPlugin = {
  install: (axios: AxiosStatic | AxiosInstance, options: Options) => {
    axios.interceptors.request.use(async (config) => {
      const token = await options.tokenResource()
      if (config && config.headers && token)
        config.headers[options.tokenName] = `${AXIOS_TOKEN_CONTENT_PREFIX}${token}`
      return config
    })
  }
}

path替换插件-axiosPathToRegexp

/**
 * @ Name: axiosPathToRegexp
 * @ Description: 路径替换插件
 */

import type { AxiosPlugin } from '../interface'
import type { AxiosInstance, AxiosStatic } from 'axios'
import { compile } from 'path-to-regexp'
declare module 'axios' {
  interface AxiosRequestConfig {
    pathParams?: Record<string, any>
  }
}
export const axiosPathToRegexp: AxiosPlugin = {
  install: (axios: AxiosStatic | AxiosInstance) => {
    axios.interceptors.request.use((config) => {
      if (config.pathParams && config.url) {
        config.url = compile(config.url)(config.pathParams)
      }
      return config
    })
  }
}

请求防抖插件-AxiosDebouncePlugin :

/**
 * @ Name: AxiosDebouncePlugin
 * @ Description: 防抖插件
 */

import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosStatic } from 'axios'
import type { AxiosPluginInstance } from '../interface'

declare module 'axios' {
  interface Axios {
    $get<T = any, R = AxiosResponse<T>, D = any>(
      url: string,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    $post<T = any, R = AxiosResponse<T>, D = any>(
      url: string,
      data?: D,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    $delete<T = any, R = AxiosResponse<T>, D = any>(
      url: string,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    $put<T = any, R = AxiosResponse<T>, D = any>(
      url: string,
      data?: D,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
    $patch<T = any, R = AxiosResponse<T>, D = any>(
      url: string,
      data?: D,
      config?: AxiosRequestConfig<D>
    ): Promise<R>
  }
}

type Method = 'get' | 'post' | 'delete' | 'put' | 'patch'

export interface Options {
  methods: Method[]
}

const DEFAULT_OPTIONS: Options = {
  methods: ['get', 'post', 'delete', 'put', 'patch']
}

class AxiosDebouncePlugin implements AxiosPluginInstance {
  /**
   * 执行区
   */
  private execution: Record<string, any> = {}

  /**
   * 默认配置
   */
  private defaultOptions: Options
  constructor(private options?: Options) {
    this.defaultOptions = Object.assign({}, DEFAULT_OPTIONS, options)
  }

  /**
   * 执行区增加新成员
   * @param key
   * @param calFun
   */
  private pushExecution(key: string, calFun: Promise<any>) {
    this.execution[key] = calFun
  }

  /**
   * 移除执行区
   * @param key
   */
  private removeExecution(key: string) {
    delete this.execution[key]
  }

  /**
   * 是否正在执行
   * @param key
   * @returns
   */
  isPending(key: string) {
    return Reflect.has(this.execution, key)
  }

  /**
   * key值生成器
   * @param url
   * @param data
   * @returns
   */
  keyCreator(url: string, data: Record<string, any>) {
    return `${url}: ${JSON.stringify(data)}`
  }

  /**
   * 执行拦截器
   * @param target
   * @param argArray
   * @returns
   */
  exeInterceptor(
    target:
      | (<T = any, R = AxiosResponse<T, any>, D = any>(
          url: string,
          config?: AxiosRequestConfig<D> | undefined
        ) => Promise<R>)
      | (<T = any, R = AxiosResponse<T, any>, D = any>(
          url: string,
          data?: D,
          config?: AxiosRequestConfig<D>
        ) => Promise<R>),
    argArray: any[]
  ) {
    const key = this.keyCreator(argArray[0], argArray[1])
    if (this.isPending(key)) {
      /**
       * 正在执行中
       */
      return this.execution[key]
    } else {
      /**
       * 执行区中无此项实例正在执行
       */
      const call = target(argArray[0], argArray[1], argArray[2]).finally(() =>
        this.removeExecution(key)
      )
      this.pushExecution(key, call)
      return call
    }
  }

  /**
   * 插件安装
   * @param axios
   */
  install(axios: AxiosStatic | AxiosInstance) {
    this.defaultOptions.methods.forEach((method) => {
      if (method === 'get' || method === 'delete') {
        axios[`$${method}`] = new Proxy(axios[method], {
          apply: (target, thisArg, argArray) => {
            return this.exeInterceptor(target, argArray)
          }
        })
      } else {
        axios[`$${method}`] = new Proxy(axios[method], {
          apply: (target, thisArg, argArray) => {
            return this.exeInterceptor(target, argArray)
          }
        })
      }
    })
  }
}

export default AxiosDebouncePlugin

控制器插件-axiosControllerPlugin

/**
 * @ Name: axiosControllerPlugin
 * @ Description: 控制器插件
 */

import type { AxiosInstance, AxiosStatic } from 'axios'
import type { AxiosPlugin } from '../interface'

declare module 'axios' {
  interface AxiosRequestConfig {
    controller?: AbortController
  }
}

/**
 * axios token挂载插件
 * @param axios
 * @returns
 */

export const axiosControllerPlugin: AxiosPlugin = {
  install: (axios: AxiosStatic | AxiosInstance) => {
    axios.interceptors.request.use(async (config) => {
      config.signal = config.data?.$controller || config.params?.$controller
      delete config.data?.$controller
      delete config.params?.$controller
      return config
    })
  }
}

主要就是弄明白弄清楚这种插拔式的拆分思路吧,理解和沉淀都需要时间,看完不是重点,弄会理解才是。