问题
例如用户多次点击按钮,多次请求接口,怎样才能做到将多余的请求筛掉,后端只返回一次结果呢?首先我想到的就是节流处理,多次发起请求,一段时间间隔内只有一次请求会生效,但想了想还有其他更加好的解决方案。
方案一
最容易想到也最朴实无华的方案就是通过axios拦截器,在请求拦截器中开启全屏Loading,这样用户再发起一次请求后,无法再点击按钮重复发起请求了,然后在响应拦截器中关闭全屏Loading。
import axios from "axios"
import { ElLoading } from "element-plus"
const instance = axios.create({
baseURL: "/api",
})
let loadingInstance = null
// 添加请求拦截器
instance.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
loadingInstance = ElLoading.service({fullscreen:true,background:'rgba(50,5,0,0.8)'})
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
loadingInstance.close()
return response
},
function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
}
)
export default instance
问题:当项目中存在一些局部的Loading效果时,就可能会出现两个Loading叠加的情况。
方案二
我们可以把相同的请求给拦截掉,问题是我们要判断什么样的请求是相同的请求?这是关键的地方
一个请求包含请求方法,请求地址,参数以及请求发出的页面hash,我们可以根据这几个参数生成一个唯一的key,来作为一个请求的唯一标识
有了请求的key,我们就可以在请求拦截器中把每次发起的请求给收集起来,后续如果有相同的请求进来,那就看这个请求是否在这个集合中,如果存在,那就说明是一个重复的请求,就直接拦截就行了。当请求完成响应后,再将请求从集合中移除。
import axios from "axios"
const instance = axios.create({
baseURL: "/api",
})
//生成唯一key的方法
function generateUniqueKey(config,hash){
const {method,url,params,data} = config
return [method,url,JSON.stringify(params),JSON.stringify(data),hash].join('&')
}
//存储发送但未响应的请求
const set = new Set()
// 添加请求拦截器
instance.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
let hash = location.hash
//生成唯一key
let reqKey = generateUniqueKey(config,hash)
if(set.has(reqKey)){
return Promise.reject()
}else {
//将请求的key保存在config中,以便在下面的响应拦截器中能够获取到reqKey
config.pendKey = reqKey
set.add(reqKey)
}
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
set.delete(response.config.reqKey)
return response
},
function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
set.delete(error.config.pendKey)
return Promise.reject(error)
}
)
export default instance
问题:这样确实能够解决问题,但是,如果,在请求接口时对错误进行了捕捉处理,也就是用了Promise.catch()方法,那多余的请求发送,就会触发这个方法,比如在屏幕上反馈给用户错误信息,而且,如果在错误捕获中有更多的逻辑处理,那么可能会导致整个程序的异常。
另外,在上面生成key时,把hash也考虑了进去,如果是history路由,可以把pathname加入,这是因为项目中会有一些数据字典型的接口,这些接口可能由不同的页面调用,如果第一个页面请求的字典接口比较慢,第二个页面的接口就被拦截了,最后导致第二个页面逻辑错误。这样想的话,好像并没有问题。
那如果这两个请求是来自同一个页面呢?
例如,一个页面需要同时加载两个组件,而这两个接口需要调某个接口时,那么此时后调接口的组件就无法正确拿到数据!!
方案三
这次我们不把请求直接给挂掉,而是对于相同的请求先给它挂起,等到最先发出去的请求拿到结果之后,把成功或失败的结果共享到后来的相同请求。
注意点:
1.拿到响应结果后,返回给挂起的请求时,用到发布订阅模式。
2.对于挂起的请求,需要将它拦截,不能让它执行正常的逻辑,所以要在请求拦截器中return Promise.reject()来直接中断请求,并做一些特殊的标记,以便在响应拦截器中进行处理
import axios from "axios"
const instance = axios.create({
baseURL: "/api",
})
//发布订阅者模式
class EventEmitter{
constructor(){
this.event = {}
}
//发布
on(type,cbres,cbrej){
if(!this.event[type]){
this.event[type] = [[cbres,cbrej]]
}else {
this.event[type].push([cbres,cbrej])
}
}
//订阅
emit(type,res,ansType){
if(!this.event[type]) return ;
else {
this.event[type].forEach(cbArr => {
if(ansType === 'resolve'){
cbArr[0](res)
}else {
cbArr[1](res)
}
})
}
}
}
//生成唯一key的方法
function generateUniqueKey(config,hash){
const {method,url,params,data} = config
return [method,url,JSON.stringify(params),JSON.stringify(data),hash].join('&')
}
//存储发送但未响应的请求
const set = new Set()
//发布订阅容器
const ev = new EventEmitter()
// 添加请求拦截器
instance.interceptors.request.use(
async function (config) {
// 在发送请求之前做些什么
let hash = location.hash
//生成唯一key
let reqKey = generateUniqueKey(config,hash)
if(set.has(reqKey)){
//如果是相同请求,在这里将请求挂起,通过发布订阅来为请求返回结果
//无论成功与否,都需要return Promise().reject()来中断请求
let res = null
try{
//接口成功响应
res = await new Promise((resolve,reject) => {
ev.on(reqKey,resolve,reject)
})
return Promise.reject({
type:'limiteResSuccess',
val:res
})
}catch(limitFunErr){
//接口报错
return Promise.reject({
type:'limiteResError',
val:limitFunErr
})
}
}else {
config.pendKey = reqKey
set.add(reqKey)
}
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
//将拿到的结果发布给其他接口
handleSuccessResponse_limit(response)
return response
},
function (error) {
return handleErrorResponse_limit(error)
}
)
//接口响应成功
function handleSuccessResponse_limit(response){
const reqKey = response.config.pendKey
if(set.has(reqKey)){
let x = null
try{
x = JSON.parse(JSON.stringify(response))
}catch(e){
x = response
}
set.delete(reqKey)
//将数据共享给挂起的请求
ev.emit(reqKey,x,'resolve')
delete ev.reqKey
}
}
//接口失败响应
function handleErrorResponse_limit(error){
if(error.type && error.type === 'limiteResSuccess'){
return Promise.resolve(error.val)
}else if(error.type && error.type === 'limiteResError'){
return Promise.reject(error.val)
}else {
const reqKey = error.config.pendKey
if(set.has(reqKey)){
let x = null
try{
x = JSON.parse(JSON.stringify(error))
}catch(e){
x = error
}
set.delete(reqKey)
ev.emit(reqKey,x,'reject')
delete ev.reqKey
}
}
return Promise.reject(error)
}
export default instance