likes
comments
collection
share

谁都能学会的axios二次封装,集成【取消重复请求、超时重发】等强大功能

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

一、为什么要对axios封装?多此一举?

本文将基于 typescript 对 axios 进行优雅封装,同时会将每部分内容细化,尽最大努力带大家弄明白每一处知识和细节。

其实要对 axios 进行封装并不是一个简单的过程,也有许多人认为这是化简为繁,但其实封装本身就是麻烦自己,方便所有人。对 axios 进行二次封装有助于使项目的网络请求方式更加规范化,可复用性和定制化程度更高,减少重复劳动。

“如何对 axios 进行封装”、“前端短时间内发送多个http请求,如何确保获取最后发送请求的响应?”这些是我们在面试中常聊到的话题,相信跟着本文来一点点敲出属于自己的 AxiosMax,在回答这些问题的时候也能更随心应手。

本文基于 vue-vben-admin 项目对 axios 的封装方式,全文代码仓库在文末。

二、项目结构

谁都能学会的axios二次封装,集成【取消重复请求、超时重发】等强大功能

  • AbortAxios.ts: 取消请求实体类
  • Axios.ts: 请求实体类
  • axiosRetry.ts: 重复请求方法
  • checkErrorStatus:错误状态码处理
  • config.ts:静态配置
  • index.ts:实例创建、拦截器实现
  • type.ts:类型定义

三、类型定义

如果项目并没有使用 ts 的需求,或者还不了解 ts 的,也可以直接采用 js 进行封装,把相关类型删除就行。

type.ts

import type { AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse, AxiosInstance, AxiosError } from 'axios'

/**
 *  axios实例配置选项,继承AxiosRequestConfig
 */
export interface AxiosOptions extends AxiosRequestConfig {
  // 是否直接返回data数据
  directlyGetData?: boolean
  // 定义拦截器
  interceptors?: RequstInterceptors
  // 是否取消重复请求
  abortRepetitiveRequest?: boolean
  // 重连配置
  retryConfig?: {
    // 重连次数
    count: number
    // 每次请求间隔时间
    waitTime: number
  }
}

/**
 *  定义拦截器抽象类,后续在index.ts文件中继承实现
 */
export abstract class RequstInterceptors {
  // 请求拦截器
  abstract requestInterceptors?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
  // 请求错误拦截器
  abstract requestInterceptorsCatch: (err: Error) => Error
  // 响应拦截器
  abstract responseInterceptor?: (res: AxiosResponse) => AxiosResponse
  // 响应错误拦截器
  abstract responseInterceptorsCatch?: (axiosInstance: AxiosInstance, error: AxiosError) => void;
}

/**
 *  定义返回类型 
 */
export interface Respones<T = any> {
  code: number
  result: T
}

四. 具体封装步骤

1. 创建请求实体类

Axios.ts文件中,创建一个实体类 AxiosMax

后续可通过 const useRequest = new AxiosMax(...)直接使用。

class AxiosMax {
  // axios实例, 通过axios.create()方法创建
  private axiosInstance: AxiosInstance
  // 传入的配置
  private options: AxiosOptions
  // 拦截器
  private interceptors: RequstInterceptors | undefined
  constructor(options: AxiosOptions) {
    this.axiosInstance = axios.create(options)
    this.options = options
    this.interceptors = options.interceptors
    // 对拦截器进行初始化注册
    this.setInterceptors()
  }
  ...
}

2. 在 AxiosMax 实体类中创建统一请求方法

后续通过 ueRequest.get({ url: '/a' })直接调用

class AxiosMax {
    
    ...
    
  /**
   * 统一请求方法
   */
  request<T = any>(config: AxiosRequestConfig): Promise<T> {
    return new Promise((resolve, reject) => {
      this.axiosInstance.request<any, AxiosResponse<Respones>>(config).then((res) => {
        return resolve(res as unknown as Promise<T>)
      }).catch((err) => {
        return reject(err)
      })
    })
  }

  get<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'GET' })
  }

  post<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' })
  }

  put<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'PUT' })
  }

  delete<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }
  
 	... 
 
}

3. 注册拦截器方法

这部分的内容相对于较多,但是关键知识较多,希望大家能够耐心看完。

本小节主要是讲述创建注册拦截器的方法。

class AxiosMax {
    
    ...
    
  /**
   * 注册拦截器方法
   */
  setInterceptors() {
    // 如果配置中并没有传入拦截器,则直接返回
    if (!this.interceptors) return
	
    // 解构出各个拦截器
    const {
      requestInterceptors,
      requestInterceptorsCatch,
      responseInterceptor,
      responseInterceptorsCatch
    } = this.interceptors

    // 挂载请求拦截器
    this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
      if (requestInterceptors) {
        // 如果存在请求拦截器,则将 config 先交给 requestInterceptors 做对应的配置。
        config = requestInterceptors(config)
      }
      return config
    }, requestInterceptorsCatch ?? undefined)

    // 挂载响应拦截器
    this.axiosInstance.interceptors.response.use((res: AxiosResponse) => {
        
      if (responseInterceptor) {
        // 如果存在响应拦截器,则将返回值先交给 responseInterceptor 做处理
        res = responseInterceptor(res)
      }

      // 根据 options.directlyGetData 配置选项判断是否直接取得data值 
      if (this.options.directlyGetData) {
        res = res.data
      }
      return res
    }, (err: AxiosError) => {
      if (responseInterceptorsCatch) {
        // 如果存在响应错误拦截器,则将返回值交给 responseInterceptorsCatch 做处理
        return responseInterceptorsCatch(this.axiosInstance, err)
      }
      return err
    })
  }

	...
    
}

内容到这里,我们已经实现了 axios 二次封装的雏形,直接拿去使用也不是不行,但总感觉还是缺了点什么,下面就将带大家继续完善我们的 AxiosMax,坚持就是胜利!

4. 错误状态码统一判断

经历了上面复杂的内容,我们来点简单的放松一下!

checkErrorStatus.ts 中:

/**
 * 对错误状态码进行检测
 */
export function checkErrorStatus(status: number | undefined, message: string | undefined, callback: (errorMessage: string) => any) {
  let errorMessage = message ?? ''
  switch (status) {
    case 400:
      errorMessage = '客户端错误,请求格式或参数有误!'
      break
    case 401:
      errorMessage = '身份认证不通过'
      break
    case 403:
      errorMessage = '用户得到授权,但是访问是被禁止的!'
      break
    case 404:
      errorMessage = '未找到目标资源!'
      break
    case 500:
      errorMessage = '服务器错误!'
      break
    case 503:
      errorMessage = '服务器错误!'
      break
  }
  if (errorMessage.length > 0) {
    callback(`checkErrorStatus:${errorMessage}`)
  }
}

后续将在响应错误拦截器当中调用 checkErrorStatus 函数,来对错误状态进行统一判断。

5. 取消重复请求

“前端短时间内发送多个http请求,如何确保获取最后发送请求的响应?”这是面试官常问的一道题,接下来,我就将带大家实现一遍相似功能,其核心是通过 AbortController API来取消请求。

AbortAxios.ts中:

import { AxiosRequestConfig } from "axios"

// 用于存储控制器
const pendingMap = new Map<string, AbortController>()

// 创建各请求唯一标识, 返回值类似:'/api:get',后续作为pendingMap的key
const getUrl = (config: AxiosRequestConfig) => {
  return [config.url, config.method].join(':')
}

class AbortAxios {
  // 添加控制器
  addPending(config: AxiosRequestConfig) {
    this.removePending(config)
    const url = getUrl(config)
    // 创建控制器实例
    const abortController = new AbortController()
    // 定义对应signal标识
    config.signal = abortController.signal
    if (!pendingMap.has(url)) {
      pendingMap.set(url, abortController)
    }
  }
  // 清除重复请求
  removePending(config: AxiosRequestConfig) {
    const url = getUrl(config)
    if (pendingMap.has(url)) {
      // 获取对应请求的控制器实例
      const abortController = pendingMap.get(url)
      // 取消请求
      abortController?.abort()
      // 清除出pendingMap
      pendingMap.delete(url)
    }
  }
}

export default AbortAxios

AxiosMax类的 setInterceptors方法中补上下面标识的内容:

谁都能学会的axios二次封装,集成【取消重复请求、超时重发】等强大功能

至此,我们的注册拦截器函数才完成,我们也实现了取消重复请求的功能。

6. 超时报错重发

本小节将带大家实现请求超时报错重发的功能。

axiosRetry.ts 中:

import type { AxiosError, AxiosInstance } from "axios";

export function retry(instance: AxiosInstance, err: AxiosError) {
  const config: any = err.config
  // 获取配置项内容(请求间隔时间,请求次数)
  const { waitTime, count } = config.retryConfig ?? {}
  // 当前重复请求的次数
  config.currentCount = config.currentCount ?? 0
  console.log(`第${config.currentCount}次重连`)

  // 如果当前的重复请求次数已经大于规定次数,则返回Promise
  if (config.currentCount >= count) {
    return Promise.reject(err)
  }
  config.currentCount++
  
  // 等待间隔时间结束后再执行请求
  return wait(waitTime).then(() => instance(config))
}

function wait(waitTime: number) {
  return new Promise(resolve => setTimeout(resolve, waitTime))
}

我们将在下一小节中展示 retry 函数的具体用法。

7. 实现拦截器

index.ts中:

// 继承了我们在最开始实现的抽象类RequstInterceptors,主要关心responseInterceptorsCatch内容
const _RequstInterceptors: RequstInterceptors = {
  requestInterceptors(config) {
    return config
  },
  requestInterceptorsCatch(err) {
    return err
  },
  responseInterceptor(config) {
    return config
  },
  responseInterceptorsCatch(axiosInstance, err: AxiosError) {
    let message = err.code === 'ECONNABORTED' ? '请求超时' : undefined
    // 判断本次请求是否已经被取消
    if (axios.isCancel(err)) {
      return Promise.reject(err);
    }
    // 检查响应状态码
    checkErrorStatus((err as AxiosError).response?.status, message,  (message) => console.log(message))
    
    // 响应错误实现重连功能
    return retry(axiosInstance, err as AxiosError)
  },
}

8. 创建实例

这是我们的最后一步,就是创建 AxiosMax 的实例,然后就可以尽情使用由我们自己封装的axios了。

const useRequest = new AxiosMax({
  directlyGetData: true,
  baseURL: staticAxiosConfig.baseUrl,
  timeout: 3000,
  interceptors: _RequstInterceptors,
  abortRepetitiveRequest: true,
  retryConfig: {
    count: 5,
    waitTime: 500
  }
})

export default useRequest

9. 测试

通过简单的测试,封装的 axios 已经实现了我们既定的功能。

谁都能学会的axios二次封装,集成【取消重复请求、超时重发】等强大功能

代码仓库地址:github.com/windlil/axi…

感谢大家的阅读!