前端接口防止重复请求实现方案

文章讨论了如何避免用户频繁点击导致的API请求过多,提出了三种解决方案:使用axios的拦截器实现全屏Loading阻止重复请求,通过唯一标识判断并拦截重复请求,以及采用挂起和发布订阅模式共享请求结果。
摘要由CSDN通过智能技术生成

问题

例如用户多次点击按钮,多次请求接口,怎样才能做到将多余的请求筛掉,后端只返回一次结果呢?首先我想到的就是节流处理,多次发起请求,一段时间间隔内只有一次请求会生效,但想了想还有其他更加好的解决方案。

方案一

最容易想到也最朴实无华的方案就是通过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

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值