axios请求拦截重复请求-阻止表单重复提交

1,503 阅读5分钟

需求

假设,我们有个表单form要提交,用户点击提交了两次,就产生了两条请求,此时后端就生成了两条数据。(不考虑后端校验)

前言

搜索网上关于axios(ajax)取消重复请求的方案,基本都是一种:

1.定义pending数组,收集请求信息。  
2.用axios生成cancel函数和cancelToken。  
3.每次axios请求之前判断pending中是否有该请求信息,如果有,则利用axios生成的cancel函数和  
cancelToken取消之前请求,再把本次请求信息添加pending。如果没有,则直接添加。  
4. 接口返回后,移除pending数组中该请求信息。

这种方案有两个问题:

  1. 用户如果一直触发ajax请求,那么改接口将会一直被取消,直至用户停止触发。类似防抖的实现原理。
  2. 适用场景与我的需求场景不符合,无法规避重复表单提交场景。因为取消接口时,有可能是接口发送,但是服务器未响应或者服务器已响应,未返回。无法保证接口在服务器未响应前取消。

其实,这个方案适用的场景是:有一个分页表格,用户从1-10频繁切换页码。在10页停止切换的时候,将请求第10页数据的接口发出,而将前置的接口给取消,保证当前页面数据的准确行。

相关方法和实现方案不在本文讨论范畴,具体信息可自行查阅。

那么,还有没有其他方案来实现需求中的场景呢

实现方案

其实,使用方案的,把刚才提到的方案改造一下就可以了。

分析

axios中拦截重复表单提交,就是说,如果已经存在该接口请求,那么就需要把后续接口请求给拦截,不让其再次请求。所以,我们这块也这样来实现:

1. 定义一个array,用来记录请求的接口。
2. axios中定义一个是否开启拦截请求的开关openPreventRequest,值为true,表示该接口开启截重复请求,默认为false3. 在接口请求时,如果openPreventRequest为true,先校验array里是否有相同的请求请求,如果有,则拦截本次请求不发出,如果没有,将该请求信息添加进array里。
4. 在数据返回后(无论成功或者失败),再将array里该请求的信息移除,保证相同请求array里只有一条。
5. 切换路由,重置array

有了这样的思路,剩下的就是代码实现了。

代码实现

废话不多说,直接码方案

  1. 定义preventRequest文件
// 缓存请求的接口信息
const requestMap = []
 
/**
 * 检查是不是重复请求
 * @param {Object} config
 */
const checkRepeatRequest = config => {
  const requestInfo = getRequestInfo(config)
  return requestMap.includes(requestInfo)
}
 
/**
 * 添加请求
 * @param {Object} config
 */
const addRequest = config => {
  // 获取当前请求信息
  if (!config.openPreventRequest) return
  const requestInfo = getRequestInfo(config)
  requestMap.push(requestInfo)
}
/**
 * 移除请求
 * @param {Object} config
 */
const removeRequest = config => {
  if (!config.openPreventRequest) return
 
  const requestInfo = getRequestInfo(config)
  const requestIndex = requestMap.indexOf(requestInfo)
  if (requestIndex > -1) {
    requestMap.splice(requestIndex, 1)
  }
}
 
/**
 * 获取请求信息
 * @param {Object} config
 */
 
function getRequestInfo (config) {
 // 重复请求定义为: 相同method,url
 // 可以自行扩展
  const { method, url } = config
  return [
    method,
    url
  ].join('&')
}
 
export default {
  checkRepeatRequest,
  addRequest,
  removeRequest
}
  1. axios中配置
// axios
import axios from 'axios'
import preventRequest from './preventRequest'
 
// axios配置...
// do something
 
 
// 封装axios请求
function request (options) {
  const { url, method, ...other } = options
  // 是否开启拦截重复请求
  if (options.openPreventRequest) {
    // 是否含有重复请求队列
    if (preventRequest.checkRepeatRequest(options)) {
      return new Promise((resolve, reject) => {
        // do something
        reject(new Error('cancelRequest'))
      })
    }
    preventRequest.addRequest(options)
  }
  // axios请求时
  return new Promise((resolve, reject) => {
    axios.request({
      method: method || 'get',
      url,
      ...other
    })
      .then((res) => {
        resolve(res)
      })
      .catch(err => {
        reject(err)
      })
  })
}
 
// axios响应拦截器
axios.interceptors.response.use(response => {
  preventRequest.removeRequest(response.config) // 在请求结束后(http code为200),移除本次请求
  return response
}, error => {
  preventRequest.removeRequest(error.config) // 在请求失败后(http code为非200,超时取消等),移除本次请求
  return Promise.reject(error)
})
 
 // 封装get方法
function get(url, opt) {
  return request({ url, method: 'get', ...opt })
}
 
 // 封装post方法
function post(url, opt) {
  return request({ url, method: 'post', ...opt })
}
 
export {
  get,
  post
}
  1. 请求文件中开启拦截重复请求开关 可以按下面方法在定义接口文件的地方配置开关,也可以根据实际情况,直接在业务层中去配置这个开关。前提是你要把这个参数传递进来。
// src/api/list/*.js 
 
import { post } from '@/config/axios'
 
export const getSomething = body => {
  return post('/api/something', {
    data: body,
    // 请求接口时,打开取消重复请求开关
    openPreventRequest: true
  })
}
  1. 在切换路由时,清空收集请求的array(以vue-router为例)
import preventRequest from '@/config/preventRequest'
 
export default (router) => {
  router.beforeEach(async (to, from, next) => {
    preventRequest.clearRequestMap()
    // do something
    return next()
  })
}

这样一来,就解决了需求中所提到的问题 -- 拦截重复表单请求

Q&A

Q: 使用场景是什么

A: 适用于表单重复提交的场景(或者有类似需要拦截重复提交发起的接口请求等场景)。是解决表单重复提交导致的bug中,axios层面的拦截。

Q: axios中实现,跟在业务层,通过某些指令实现有何区别

A: 从结果角度看,并没有什么区别。业务层,通过封装某些指定,定义一个lock值,接口请求时,lock = true, 接口返回后lock = false,达到拦截后续动作,进而也解决了需求中提到的问题。各有利弊,各有灵活点和局限点,各自斟酌使用。

Q: 业务层是否可以开启openPreventRequest?

A: 业务层可以配置并透传进axios,src/api/list/* 接口文件中使用与在实际业务层使用,从实现角度看并没有什么区别。

Q: axios超时问题如果解决

A: 超时时长跟据各项目配置的时间来做到的,未超时前,走响应拦截器统一处理,超时也会触发axios.interceptors.response中的reject函数进而从requestMap中移除该请求,在reject中通过error.constructor.name来获取error事件触发的函数来区分具体的error事件。

结束

至此,就解决了开头需求中提到的问题。
完成后,组内有位同学提到了一个问题,切换路由清空requestMap的时候, 还需要在vue-router中单独在配置一遍,能不能直接配置在preventRequest文件中,自动来清理。一语点醒,当即就去实现,发现里面还有另一番天地......

To be continued~~