axios的二次封装之取消重复请求(节流)与配合遮罩层使用

1,635 阅读5分钟

我看过很多二次封装axios的博客,但很多都是防抖式取消重复请求(保留最后一个,取消前面所有相同的请求)

这样有两个问题:

1.如果是重复的请求,那为什么不保留第一个请求而去保留最后一次的请求呢(一般情况下,如果是比如路由跳转等其他情况,前面的请求都没用了,这时应该使用防抖)
2.保留了最后一个请求,利用cancelToken的方式取消之前的请求,实际上请求还会发送到服务器,只是前端被取消了而已

基于上面两点,我写了一个节流式的取消重复请求(保留第一次,拦截之后所有相同的请求)

  • 基础取消重复请求的版本

import originalAxios, { AxiosRequestConfig, AxiosResponse, Canceler } from 'axios'
import { Toast } from 'vant'
/* 注意:以下直接使用(实例.isCancel和实例.cancelToken)会报错找不到isCancel和cancelToken方法(创建的axios实例里的原型没有这两个方法)
只有最原始从axios库引入的axios有这两个方法,所以引入一个原始的axios命名originalAxios,调用的时候替换即可 */

//创建一个实例,不影响其他的axios请求
const axios = originalAxios.create({
  //添加代理解决跨域问题
  baseURL: '/wisdom',
  //设置超时时间
  timeout: 8000
})
//新建map用于存放正在执行的请求
const pendingRequest = new Map()

//generateReqKey:用于根据当前请求的信息,生成请求Key存放到map中作为当前请求的键;
function generateReqKey(config: AxiosRequestConfig) {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}
//addPendingRequest:用于把当前请求信息添加到pendingRequest对象中
function addPendingRequest(config: AxiosRequestConfig) {
  const requestKey = generateReqKey(config)
  /* 这下面的部分不能去除的原因是因为在清空所有请求的时候需要用到cancel令牌
所以必须在添加请求的时候就创建一个CancelToken取消令牌用于之后clearPending函数清空请求 */
  config.cancelToken = new originalAxios.CancelToken((cancel: Canceler) => {
    //创建当前请求放到map中
    pendingRequest.set(requestKey, cancel)
  })
}

//去除所有列表里的请求(用于跳转路由等需要清空请求的情况)
function clearPending() {
  //遍历map对象,用每个取消令牌逐一取消请求,起到清空所有请求的作用
  for (const [requestKey, cancelToken] of pendingRequest) {
    cancelToken(requestKey)
  }
  //清空map对象
  pendingRequest.clear()
}

//请求拦截器(发送之前)
axios.interceptors.request.use(
  /*这里config类型设置为any而不设置为AxiosRequestConfig是因为之后为了取消当前请求使用了return false
  而axios请求拦截器允许返回的类型是AxiosRequestConfig | Promise<AxiosRequestConfig>
  如果返回了Boolean就会报错,为了方便直接设置为any,当然你也可以自己去修改axios的ts类型声明文件,使其允许返回Boolean */
  (config: any) => {
    if (localStorage.token) {
      config.headers['Authorization'] = `Bearer ${localStorage.token}`
    }
    const requestKey = generateReqKey(config)
    if (pendingRequest.has(requestKey)) {
      /*如果请求参数全部相同,表示是重复的请求,那么保留最初的,取消当前的请求
      传统的用token令牌取消的请求,实际上还会发送到后端,只是前端被取消了而已
      但是这里在请求拦截器就return false直接取消了发送请求
      这个请求实际在发送之前就被拦截发送了而不是取消,所以后端是接收不到这个请求的 */
      return false
    }
    // 把当前请求信息添加到pendingRequest对象中
    addPendingRequest(config)
    return config
  },
  error => {
    // 这里出现错误可能是网络波动造成的,清空 pendingRequests 对象
    pendingRequest.clear()
    Toast({
      message: error.message
    })
    return Promise.reject(error)
  }
)
//响应拦截器(发送之后)
axios.interceptors.response.use(
  (response: AxiosResponse) => {
    /*  这里使用JSON.parse()的原因是:响应拦截器中收到的response.config.data还是json字符串格式
   如果直接传入字符串生成key会与之前添加时创建的key不符合(进行了两次JSON.stringify(data),会无法正常删除map的请求 */
    response.config.data = response.config.data && JSON.parse(response.config.data)
    const requestKey = generateReqKey(response.config)
    //根据key删除当前请求
    pendingRequest.delete(requestKey)

    return response
  },
  error => {
    //此处同上
    error.config.data = error.config.data && JSON.parse(error.config.data)

    const requestKey = generateReqKey(error.config)
    // 从pendingRequest对象中移除请求
    pendingRequest.delete(requestKey)

    if (error.message === 'Network Error') {
      Toast({
        message: '网络错误,请检查您的网络是否正常'
      })
    } else if (error.message.includes('timeout')) {
      Toast({
        message: '请求超时,请稍后重试'
      })
    }

    // 添加其它异常处理
    return Promise.reject(error)
  }
)
export default axios
//导出clearnPending,比如在路由守卫中使用,跳转路由清空请求
export { clearPending }

这里我只是基于重复请求封装的axios,你们可以在拦截器中添加各种自己想要的需求,比如请求时自动携带token,判断响应码弹出提示等

  • 遮罩层随着请求开启与关闭的版本

由于重复请求只能拦截相同地址,相同参数的请求,如果是相同地址但参数不同的请求是不会被拦截的,在没有遮罩层的情况下且网络较慢,比如修改密码,用户可能在还没响应之前就多次修改不同的密码,但是异步响应的顺序是不一定的,有可能密码就被改成不是自己最终修改的密码了(像这种涉及用户个人数据等比较重要的请求时,请求发送立即添加遮罩层限制用户的多次操作是最保险和方便的方式)

具体实现思路:

  1. 在App.vue中添加全局遮罩层
  2. 在vuex或pinia中等状态管理库存放一个变量用于管理遮罩层的开启与关闭
  3. 在上面的基础上,把map改为响应式(用于监听正在执行的请求个数),有则开启遮罩层,没有则关闭遮罩层
import { Toast } from 'vant'
//引入pinia的overlay的仓库控制遮罩层的开关
import { useOverlayStore } from '@/store/overlay'
import { reactive, watchEffect } from 'vue'
import originalAxios, { AxiosRequestConfig, Canceler, AxiosResponse } from 'axios'
const overlay = useOverlayStore()
/* 注意:以下直接使用(实例.isCancel和实例.cancelToken)会报错找不到isCancel和cancelToken方法(创建的axios实例里的原型没有这两个方法)
只有最原始从axios库引入的axios有这两个方法,所以引入一个原始的axios命名originalAxios,调用的时候替换即可 */

//创建一个实例,不影响其他的axios请求
const axios = originalAxios.create({
  //添加代理解决跨域问题
  baseURL: '/wisdom',
  //设置超时时间
  timeout: 8000
})
//新建map用于存放正在执行的请求
//创建一个响应式的map,便于后续监听他的变化来开启和关闭遮罩层
const pendingRequest = reactive(new Map())

//generateReqKey:用于根据当前请求的信息,生成请求Key存放到map中作为当前请求的键;
function generateReqKey(config: AxiosRequestConfig) {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}
//addPendingRequest:用于把当前请求信息添加到pendingRequest对象中
function addPendingRequest(config: AxiosRequestConfig) {
  const requestKey = generateReqKey(config)
  /* 这下面的部分不能去除的原因是因为在清空所有请求的时候需要用到cancel令牌
所以必须在添加请求的时候就创建一个CancelToken取消令牌用于之后clearPending函数清空请求 */
  config.cancelToken = new originalAxios.CancelToken((cancel: Canceler) => {
    //创建当前请求放到map中
    pendingRequest.set(requestKey, cancel)
  })
}

//去除所有列表里的请求(用于跳转路由等需要清空请求的情况)
function clearPending() {
  //遍历map对象,用每个取消令牌逐一取消请求,起到清空所有请求的作用
  for (const [requestKey, cancelToken] of pendingRequest) {
    cancelToken(requestKey)
  }
  //清空map对象
  pendingRequest.clear()
}

//请求拦截器(发送之前)
axios.interceptors.request.use(
  /*这里config类型设置为any而不设置为AxiosRequestConfig是因为之后为了取消当前请求使用了return false
  而axios请求拦截器允许返回的类型是AxiosRequestConfig | Promise<AxiosRequestConfig> 
  如果返回了Boolean就会报错,为了方便直接设置为any,当然你也可以自己去修改axios的ts类型声明文件,使其允许返回Boolean */
  (config: any) => {
    if (localStorage.token) {
      config.headers['Authorization'] = `Bearer ${localStorage.token}`
    }
    const requestKey = generateReqKey(config)
    if (pendingRequest.has(requestKey)) {
      /*如果请求参数全部相同,表示是重复的请求,那么保留最初的,取消当前的请求
      传统的用token令牌取消的请求,实际上还会发送到后端,只是前端被取消了而已
      但是这里在请求拦截器就return false直接取消了发送请求
      这个请求实际在发送之前就被拦截发送了而不是取消,所以后端是接收不到这个请求的 */
      return false
    }
    // 把当前请求信息添加到pendingRequest对象中
    addPendingRequest(config)
    return config
  },
  error => {
    // 这里出现错误可能是网络波动造成的,清空 pendingRequests 对象
    pendingRequest.clear()
    Toast({
      message: error.message
    })
    return Promise.reject(error)
  }
)
//响应拦截器(发送之后)
axios.interceptors.response.use(
  (response: AxiosResponse) => {
    /*  这里使用JSON.parse()的原因是:响应拦截器中收到的response.config.data还是json字符串格式
   如果直接传入字符串生成key会与之前添加时创建的key不符合(进行了两次JSON.stringify(data),会无法正常删除map的请求 */
    response.config.data = response.config.data && JSON.parse(response.config.data)
    const requestKey = generateReqKey(response.config)
    //根据key删除当前请求
    pendingRequest.delete(requestKey)

    return response
  },
  error => {
    //此处同上

    error.config.data = error.config.data && JSON.parse(error.config.data)

    const requestKey = generateReqKey(error.config)
    // 从pendingRequest对象中移除请求
    pendingRequest.delete(requestKey)

    if (error.message === 'Network Error') {
      Toast({
        message: '网络错误,请检查您的网络是否正常'
      })
    } else if (error.message.includes('timeout')) {
      Toast({
        message: '请求超时,请稍后重试'
      })
    }

    // 添加其它异常处理
    return Promise.reject(error)
  }
)
export default axios
export { clearPending }
//添加监听器,如果存在请求开启遮罩层,清空请求关闭遮罩层
watchEffect(() => {
  if (pendingRequest.size === 0) {
    //如果没有请求了就重置遮罩层
    overlay.increment(false)
  } else {
    overlay.increment(true)
  }
})


当然不止可以采用遮罩层的方式防止用户发送多个地址相同,参数不同的请求,也可以采取白名单(或黑名单的方式)在请求拦截器中判断,比如某个地址只允许同时一个请求执行(节流),该请求未响应之前,发送的请求全部拦截return false

但是前端应该更应该在视图层(比如给按钮添加loading和遮罩层等方式)而不是拦截器次处理重复请求,不是所有的接口都需要拦截重复请求的所以不能在拦截器全部拦截,但是都在拦截器进行二次判断的话又过于复杂了,个人建议在需要的地方加loading和遮罩足够了,此处只是做个拦截器拦截请求的一种演示,处理重复请求是前后端一起的作业,后端处理主要为了安全性(比如幂等接口),前端的处理是为了在视图层拦截大部分重复请求,如果非页面发起的请求拦截后端也会做好相应的拦截准备(比如常见的加锁)