Axios+TS实现重复请求拦截+全局加载动画

942 阅读3分钟

当多个请求的method(get,post,put等),urlparamsdata 完全相同时,我们可以认为他们是相同请求。而当后端还没有返回数据,此时又发起了相同的请求,就可以对该请求进行拦截。

封装思路:

  1. 根据当前请求生成KEY。可以使用qs 库或者手写一个queryString,来生成一个requestKey标注这些请求。(对应下面代码中的generateReqKey方法)

  2. 如果该请求的requestKey在pendingRequestmap中不存在,则新创建一个controller对象(new AbortController),并将controller的signal属性挂载到请求的config属性中。否则就获取之前的controller,并挂载到当前请求的config属性上。(对应addPendingRequest方法)

    挂载singal的目的是为了之后能够取消请求。注意,相同的requestKey(相同的请求)要挂载相同的singal方便后面统一取消重复请求。

    const controller = new AbortController();
    // controller具有一个方法 `abort()`和一个属性 `signal`
    
  3. 检查是否存在重复请求,若存在则取消已发请求。(对应removePendingRequest方法)

    controller.abort()
    

封装的代码

在这里,我们使用qs序列化对象。

npm i qs
npm i @types/qs
// 拦截重复请求
import type { AxiosRequestConfig } from 'axios'
import qs from 'qs'
// 从 v0.22.0 开始,Axios 支持以 fetch API 方式—— AbortController 取消请求:
// CancelToken 已经被淘汰了

export class RequestCanceler {
  // 存储每个请求的标志和取消函数
  pendingRequest: Map<string, AbortController>
  constructor() {
    this.pendingRequest = new Map<string, AbortController>()
  }

  generateReqKey(config: AxiosRequestConfig): string {
    const { method, url } = config
    return [url || '', method || '', qs.stringify(config.params), qs.stringify(config.data)].join('&')
  }

  addPendingRequest(config: AxiosRequestConfig) {
    const requestKey: string = this.generateReqKey(config)
    if (!this.pendingRequest.has(requestKey)) {
      const controller = new AbortController()
      // 给config挂载signal
      config.signal = controller.signal
      this.pendingRequest.set(requestKey, controller)
    } else {
      // 如果requestKey已经存在,则获取之前设置的controller,并挂载signal
      config.signal = (this.pendingRequest.get(requestKey) as AbortController).signal
    }
  }

  removePendingRequest(config: AxiosRequestConfig) {
    const requestKey = this.generateReqKey(config)
    if (this.pendingRequest.has(requestKey)) {
      // 取消请求
      (this.pendingRequest.get(requestKey) as AbortController).abort()
      // 从pendingRequest中删掉
      this.pendingRequest.delete(requestKey)
    }
  }
}

如何使用

封装好了,就可以开始使用啦:

import axios from 'axios'

import { RequestCanceler } from './helper/requestCanceler'

const canceler = new RequestCanceler()
// 设置全局超时 2min
axios.defaults.timeout = 120000
// 创建axios实例
const service = axios.create({
    headers: {
        'Content-Type': 'application/json',
    },
})

// 添加请求拦截器
service.interceptors.request.use(
    function (config) {
        // 检查是否存在重复请求,若存在则取消已发的请求
        canceler.removePendingRequest(config)
        // 把当前的请求信息添加到pendingRequest
        canceler.addPendingRequest(config)
        return config
    },
    function (error) {
        // 对请求错误做些什么
        return Promise.reject(error)
    }
)

// 添加响应拦截器
service.interceptors.response.use(
    function (response: AxiosResponse) {
        canceler.removePendingRequest(response.config)
        return response.data
    },
    function (error: AxiosError) {
        canceler.removePendingRequest(error.config || {})
        const { response } = error
        if (axios.isCancel(error)) {
            return {
                success: false,
                message: 'repeat request',
            }
        }
        return Promise.reject(error)
    }
)

以上,就对axios进行了最基础的重复请求拦截的封装。

加载动画

因为我们已经对每个请求序列化生成了key:所以我们可以在每个请求前,开始加载动画,请求完后,关闭加载动画。这里我使用的是antdesign 5.0。

import { message } from 'antd'
// 开始加载动画
message.loading({
   content: '加载中...',
   key: requestKey,
   duration:0,设为 0 时不自动关闭
})

// 关闭加载动画
message.destroy(requestKey)
// 拦截重复请求
import type { AxiosRequestConfig } from 'axios'
import { message } from 'antd'
import qs from 'qs'

export class RequestCanceler {
  // 存储每个请求的标志和取消函数
  pendingRequest: Map<string, AbortController>
  constructor() {
    this.pendingRequest = new Map<string, AbortController>()
  }

  generateReqKey(config: AxiosRequestConfig): string {
    const { method, url } = config
    return [url || '', method || '', qs.stringify(config.params), qs.stringify(config.data)].join('&')
  }

  addPendingRequest(config: AxiosRequestConfig) {
    const requestKey: string = this.generateReqKey(config)
    message.loading({
      content: '加载中...',
      key: requestKey,
    })
    if (!this.pendingRequest.has(requestKey)) {
      const controller = new AbortController()
      // 给config挂载signal
      config.signal = controller.signal
      this.pendingRequest.set(requestKey, controller)
    } else {
      // 如果requestKey已经存在,则获取之前设置的controller,并挂载signal
      config.signal = (this.pendingRequest.get(requestKey) as AbortController).signal
    }
  }

  removePendingRequest(config: AxiosRequestConfig) {
    const requestKey = this.generateReqKey(config)
    message.destroy(requestKey)
    if (this.pendingRequest.has(requestKey)) {
      // 取消请求
      ;(this.pendingRequest.get(requestKey) as AbortController).abort()
      // 从pendingRequest中删掉
      this.pendingRequest.delete(requestKey)
    }
  }
}