需求
假设,我们有个表单form要提交,用户点击提交了两次,就产生了两条请求,此时后端就生成了两条数据。(不考虑后端校验)
前言
搜索网上关于axios(ajax)取消重复请求的方案,基本都是一种:
1.定义pending数组,收集请求信息。
2.用axios生成cancel函数和cancelToken。
3.每次axios请求之前判断pending中是否有该请求信息,如果有,则利用axios生成的cancel函数和
cancelToken取消之前请求,再把本次请求信息添加pending。如果没有,则直接添加。
4. 接口返回后,移除pending数组中该请求信息。
这种方案有两个问题:
- 用户如果一直触发ajax请求,那么改接口将会一直被取消,直至用户停止触发。类似防抖的实现原理。
- 适用场景与我的需求场景不符合,无法规避重复表单提交场景。因为取消接口时,有可能是接口发送,但是服务器未响应或者服务器已响应,未返回。无法保证接口在服务器未响应前取消。
其实,这个方案适用的场景是:有一个分页表格,用户从1-10频繁切换页码。在10页停止切换的时候,将请求第10页数据的接口发出,而将前置的接口给取消,保证当前页面数据的准确行。
相关方法和实现方案不在本文讨论范畴,具体信息可自行查阅。
那么,还有没有其他方案来实现需求中的场景呢
实现方案
其实,使用方案的,把刚才提到的方案改造一下就可以了。
分析
axios中拦截重复表单提交,就是说,如果已经存在该接口请求,那么就需要把后续接口请求给拦截,不让其再次请求。所以,我们这块也这样来实现:
1. 定义一个array,用来记录请求的接口。
2. axios中定义一个是否开启拦截请求的开关openPreventRequest,值为true,表示该接口开启截重复请求,默认为false。
3. 在接口请求时,如果openPreventRequest为true,先校验array里是否有相同的请求请求,如果有,则拦截本次请求不发出,如果没有,将该请求信息添加进array里。
4. 在数据返回后(无论成功或者失败),再将array里该请求的信息移除,保证相同请求array里只有一条。
5. 切换路由,重置array。
有了这样的思路,剩下的就是代码实现了。
代码实现
废话不多说,直接码方案
- 定义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
}
- 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
}
- 请求文件中开启拦截重复请求开关 可以按下面方法在定义接口文件的地方配置开关,也可以根据实际情况,直接在业务层中去配置这个开关。前提是你要把这个参数传递进来。
// src/api/list/*.js
import { post } from '@/config/axios'
export const getSomething = body => {
return post('/api/something', {
data: body,
// 请求接口时,打开取消重复请求开关
openPreventRequest: true
})
}
- 在切换路由时,清空收集请求的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~~