基于Axios封装请求---防止接口重复请求解决方案

 一、引言

前端接口防止重复请求的实现方案主要基于以下几个原因:

  1. 用户体验:重复发送请求可能导致页面长时间无响应或加载缓慢,从而影响用户的体验。特别是在网络不稳定或请求处理时间较长的情况下,这个问题尤为突出。

  2. 服务器压力:如果前端不限制重复请求,服务器可能会接收到大量的重复请求,这不仅增加了服务器的处理负担,还可能导致资源浪费。

  3. 数据一致性:对于某些操作,如表单提交,重复请求可能导致数据重复插入或更新,从而破坏数据的一致性。

为了实现前端接口防止重复请求,可以采取以下方案:

  1. 设置请求标志:在发送请求时,为请求设置一个唯一的标识符(如请求ID)。在请求处理过程中,可以通过检查该标识符来判断是否已存在相同的请求。如果存在,则取消或忽略重复请求。

  2. 使用防抖(debounce)和节流(throttle)技术:这两种技术都可以用来限制函数的执行频率。防抖是在一定时间间隔内只执行一次函数,而节流是在一定时间间隔内最多执行一次函数。这两种技术可以有效防止用户频繁触发事件导致的重复请求。

  3. 取消未完成的请求:在发送新的请求之前,可以检查是否存在未完成的请求。如果存在,则取消这些请求,以避免重复发送。这通常可以通过使用Promise、AbortController等技术实现。

  4. 前端状态管理:使用状态管理工具(如Redux、Vuex等)来管理请求状态。在发送请求前,检查状态以确定是否已存在相同的请求。这种方案可以更加灵活地控制请求的行为。

  5. 后端接口设计:虽然前端可以采取措施防止重复请求,但后端接口的设计也非常重要。例如,可以为接口设置幂等性,确保即使多次调用接口也不会产生副作用。此外,还可以使用令牌(token)等机制来限制请求的重复发送。

综合使用这些方案,可以有效地防止前端接口的重复请求,提高用户体验和系统的稳定性。

 二、取消未完成的请求

  1、Axios内置的 axios.CancelToken

import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import axios from 'axios'const CancelToken = axios.CancelToken
const queue: any = [] // 请求队列const service = axios.create({baseURL: '/api',timeout: 10 * 60 * 1000,headers: {'Content-Type': 'application/json;charset=UTF-8',},
})// 取消重复请求
const removeRepeatRequest = (config: AxiosRequestConfig) => {for (const key in queue) {const index = +keyconst item = queue[key]if (item.url === config.url &&item.method === config.method &&JSON.stringify(item.params) === JSON.stringify(config.params) &&JSON.stringify(item.data) === JSON.stringify(config.data)) {// 执行取消操作item.cancel('操作太频繁,请稍后再试')queue.splice(index, 1)}}
}// 请求拦截器
service.interceptors.request.use((config: InternalAxiosRequestConfig) => {removeRepeatRequest(config)config.cancelToken = new CancelToken(c => {queue.push({url: config.url,method: config.method,params: config.params,data: config.data,cancel: c,})})return config},error => {return Promise.reject(error)}
)// 响应拦截器
service.interceptors.response.use((response: AxiosResponse): any => {removeRepeatRequest(response.config)return Promise.resolve(response)},error => {return Promise.reject(error)}
)export default service

 2、发布订阅方式

💡灵感来源: 前端接口防止重复请求实现方案

/** @Author: LYM* @Date: 2024-03-28 14:12:54* @LastEditors: LYM* @LastEditTime: 2024-03-28 14:56:44* @Description: 封装axios*/
import { gMessageError, gMessageWarning, gMessageSuccess } from '@/plugins/naiveMessage'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
import axios from 'axios'
import { ContentTypeEnum } from './httpEnum'
import { checkResponseHttpStatus, loginStatusExpiresHandler } from './statusHandler'
import type { IRequestOptions, IResult } from './types'const baseURL = import.meta.env.VITE_USER_BASE_URLlet isRefreshing: boolean = false
let retryRequests: any[] = []// 发布订阅
class EventEmitter {[x: string]: {}constructor() {this.event = {}}on(type: string | number, cbres: any, cbrej: any) {if (!this.event[type]) {this.event[type] = [[cbres, cbrej]]} else {this.event[type].push([cbres, cbrej])}}emit(type: string | number, res: any, ansType: string) {if (!this.event[type]) returnelse {this.event[type].forEach((cbArr: ((arg0: any) => void)[]) => {if (ansType === 'resolve') {cbArr[0](res)} else {cbArr[1](res)}})}}
}// 根据请求生成对应的key
const generateReqKey = (config: { method: string; url: string; params: string; data: string },hash: string
) => {const { method, url, params, data } = configreturn [method, url, JSON.stringify(params), JSON.stringify(data), hash].join('&')
}// 判断是否为上传请求
const isFileUploadApi = (config: { data: any }) => {return Object.prototype.toString.call(config.data) === '[object FormData]'
}// 存储已发送但未响应的请求
const pendingRequest = new Set()
// 发布订阅容器
const ev = new EventEmitter()const service = axios.create({baseURL: import.meta.env.VITE_BASE_URL,timeout: 10 * 60 * 1000,headers: {'Content-Type': ContentTypeEnum.FORM_URLENCODED,},
})// 请求拦截器
service.interceptors.request.use(async (config: any) => {const hash = location.hash// 生成请求Keyconst reqKey = generateReqKey(config, hash)if (!isFileUploadApi(config) && pendingRequest.has(reqKey)) {// 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果// 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器let res = nulltry {// 接口成功响应res = await new Promise((resolve, reject) => {ev.on(reqKey, resolve, reject)})return Promise.reject({type: 'limitResSuccess',val: res,})} catch (limitFunErr) {// 接口报错return Promise.reject({type: 'limitResError',val: limitFunErr,})}} else {// 将请求的key保存在configconfig.pendKey = reqKeypendingRequest.add(reqKey)}return config},error => {return Promise.reject(error)}
)// 响应拦截器
service.interceptors.response.use((response: AxiosResponse): any => {const res = response.data || {}// 将拿到的结果发布给其他相同的接口handleSuccessResponse_limit(response)switch (res.code) {case 206:// 旧密码不正确breakcase 401:// 业务系统未登录,调用login接口登录return loginStatusExpiresHandler(response, request, service)case 402:// 登录失败gMessageWarning({content: '登录失败,请联系管理员',})breakcase 403:// 无权限,跳转到无权限页面gMessageWarning({content: res.msg || '权限不足',})breakcase 404:// 获取csrfToken,重新释放请求if (res.msg === '丢失服务器端颁发的CSRFTOKEN' ||res.msg === '请求中请携带颁发的CSRFTOKEN') {if (!isRefreshing) {isRefreshing = true// 请求tokenrequest({ url: '/csrfToken', baseURL }).then((data: any) => {if (data.code === 200) {// 遍历缓存队列 发起请求 传入最新tokenretryRequests.forEach(cb => cb())// 重试完清空这个队列retryRequests = []}})}return new Promise(resolve => {// 将resolve放进队列,用一个函数形式来保存,等token刷新后调用执行retryRequests.push(() => {resolve(service(response.config))})})}breakcase 500:// 服务器错误gMessageError({content: '服务器错误,请联系管理员',})return}return Promise.resolve(response)},error => {const { code, message } = errorif (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {gMessageError({content: '接口请求超时,请刷新页面重试!',})return}const err = JSON.stringify(error)if (err && err.includes('Network Error')) {gMessageError({content: '网络异常,请检查您的网络连接是否正常!',})return}// http 状态码提示信息处理const isCancel = axios.isCancel(error)if (!isCancel) {checkResponseHttpStatus(error.response && error.response.status, message)}return handleErrorResponse_limit(error)}
)// 接口响应成功
const handleSuccessResponse_limit = (response: any) => {const reqKey = response.config.pendKeyif (pendingRequest.has(reqKey)) {let x = nulltry {x = JSON.parse(JSON.stringify(response))} catch (e) {x = response}pendingRequest.delete(reqKey)ev.emit(reqKey, x, 'resolve')delete ev.reqKey}
}// 接口响应失败
const handleErrorResponse_limit = (error: { type: string; val: any; config: { pendKey: any } }) => {if (error.type && error.type === 'limitResSuccess') {return Promise.resolve(error.val)} else if (error.type && error.type === 'limitResError') {return Promise.reject(error.val)} else {const reqKey = error.config.pendKeyif (pendingRequest.has(reqKey)) {let x = nulltry {x = JSON.parse(JSON.stringify(error))} catch (e) {x = error}pendingRequest.delete(reqKey)ev.emit(reqKey, x, 'reject')delete ev.reqKey}}return Promise.reject(error)
}export default serviceexport const request = (config: AxiosRequestConfig, options?: IRequestOptions) => {return new Promise((resolve, reject) => {service(config).then((response: AxiosResponse<IResult>) => {// 返回原始数据 包含http信息if (options?.isReturnNativeResponse) {resolve(response)}// 返回的接口信息const msg = response.data.msg// 是否显示成功信息if (options?.isShowSuccessMessage) {gMessageSuccess({content: options.successMessageText ?? msg ?? '操作成功',})}if (options?.isShowErrorMessage) {gMessageError({content: options.errorMessageText ?? msg ?? '操作失败',})}resolve(response.data)}).catch(error => {reject(error)})})
}

 httpEnum.ts

/*** @description: ContentType类型*/
export enum ContentTypeEnum {// jsonJSON = 'application/json;charset=UTF-8',// jsonTEXT = 'text/plain;charset=UTF-8',// form-data 一般配合qsFORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',// form-data  上传FORM_DATA = 'multipart/form-data;charset=UTF-8',
}/*** @description: 请求方法*/
export enum MethodEnum {GET = 'GET',POST = 'POST',PATCH = 'PATCH',PUT = 'PUT',DELETE = 'DELETE',
}

 naiveMessage.ts 基于naive-ui分装提示

/** @Author: LYM* @Date: 2023-03-28 08:47:39* @LastEditors: LYM* @LastEditTime: 2023-04-25 08:58:25* @Description: naive message提示*/
import { createDiscreteApi, lightTheme, type ConfigProviderProps } from 'naive-ui'
import { computed } from 'vue'
import { IconWarningFill, IconInfoFill, IconCircleCloseFilled, IconSuccessFill } from '@/icons'const configProviderPropsRef = computed<ConfigProviderProps>(() => ({theme: lightTheme,
}))const { message } = createDiscreteApi(['message'], {configProviderProps: configProviderPropsRef,
})// 警告
export const gMessageWarning = (params?: any) => {const {content = '这是一条message warning信息!',icon = IconWarningFill,duration = 5000,} = params || {}message.warning(content, {icon: () => h(icon, null),duration,})
}// 成功
export const gMessageSuccess = (params?: any) => {const {content = '这是一条message success信息!',icon = IconSuccessFill,duration = 5000,} = params || {}message.success(content, {icon: () => h(icon, null),duration,})
}// 失败
export const gMessageError = (params?: any) => {const {content = '这是一条message error信息!',icon = IconCircleCloseFilled,duration = 5000,} = params || {}message.error(content, {icon: () => h(icon, null),duration,})
}// 信息
export const gMessageInfo = (params?: any) => {const {content = '这是一条message info信息!',icon = IconInfoFill,duration = 5000,} = params || {}message.info(content, {icon: () => h(icon, null),duration,})
}// loading
export const gMessageLoading = (params?: any) => {const { content = '这是一条message Loading信息!', duration = 5000 } = params || {}message.loading(content, {duration,})
}const gMessageObj = {info: {icon: IconInfoFill,},warning: {icon: IconWarningFill,},success: {icon: IconSuccessFill,},error: {icon: IconCircleCloseFilled,},
}//  合并
export const gMessage = (params?: any) => {const { content = '这是一条message信息!', duration = 5000, type = 'info' } = params || {}message.create(content, {duration,type,icon: () => h(gMessageObj[type], null),})
}

checkResponseHttpStatus请求状态码收集处理---自行分装

loginStatusExpiresHandler登录过期或者token失效收集处理---自行分装

注意: 心跳、轮询等请求可以在入参中透传随机key值解决

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2906749.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

MySql实战--MySQL为什么有时候会选错索引

前面我们介绍过索引&#xff0c;你已经知道了在MySQL中一张表其实是可以支持多个索引的。但是&#xff0c;你写SQL语句的时候&#xff0c;并没有主动指定使用哪个索引。也就是说&#xff0c;使用哪个索引是由MySQL来确定的。 不知道你有没有碰到过这种情况&#xff0c;一条本来…

有手就会Anaconda下载与安装

​1.Anaconda 介绍 Anaconda&#xff08;官网&#xff1a;https://www.anaconda.com/&#xff09; 是一个开源 Python 发行版本&#xff0c;Anaconda 包括 Conda、Python 以及一大堆安装好的工具包&#xff0c;比如&#xff1a;numpy、pandas 等&#xff0c;是数据分析&#x…

代码随想录算法训练营第二十四天|77.组合、216.组合Ⅲ

文档链接&#xff1a;https://programmercarl.com/ LeetCode77.组合 题目链接&#xff1a;https://leetcode.cn/problems/combinations/ 思路&#xff1a; 回溯三部曲&#xff1a; 第一步&#xff1a;确定函数返回值和参数类型 第二步&#xff1a;确定终止条件 第三步&a…

保理业务产品方案

常见的信贷业务流程 产品架构 一般分为贷前、贷中、贷后三个部分&#xff1a; 贷前一般处理客户入驻、资质审批、授信项目准入&#xff1b; 贷中一般处理处理具体的融资申请、审批、中登登记、资产锁定、放款事务&#xff1b; 贷后一般处理客户还款冲销、账款跟踪、到期日调整…

windows 远程连接(mstsc)无法复制粘贴文件

目录 问题 1. 打开远程连接(mstsc) 方式一&#xff1a; 方式二&#xff1a; 2. 打开【显示选项】 3. 选择【本地资源】 > 【详细信息】 4. 选择需要操作的本机磁盘 5. 重新打开远程即可 问题 使用win自带的远程桌面连接&#xff0c;无法复制粘贴文件&#xff0c;解…

Go微服务实战——metrics指标监控(Prometheus框架与Grafana可视化)

安装Prometheus 参考官网 安装完后访问http://IP:9090如下所示&#xff1a; 这是Prometheus自带的UI。 该地址是数据监控地址http://localhost:9090/metrics所有输出的监控项。 可以正常浏览上述信息是表示安装完成。 Promethus简介 promethus中文网 Prometheus中文文档 …

整理git上的模板框架

vite-vue3.0-ts-pinia-uni-app 技术栈的app框架 功能&#xff1a;基于 uni-app&#xff0c;一端发布多端通用&#xff0c;目前已经适配 H5、微信小程序、QQ小程序、Ios App、Android App。 taro3vue3tsnutuipinia taro3 框架小程序跨端平台 vue3.0-element-vite-qiankun 后台…

雷军分享造车故事:储备1363亿元的现金,吊打特斯拉Model 3

小米召开新车发布会&#xff0c;正式发布小米 SU7。该车定位中大型纯电轿车&#xff0c;有 SU7、SU7 Pro、SU7 Max 三个版本&#xff0c;车身尺寸 4997/1963/1455mm&#xff0c;轴距 3000mm。售价 21.59-29.99 万。 在小米汽车SU7发布会后&#xff0c;小米集团的创始人、董事长…

马蹄集第九周

MT3011 代码 #include<bits/stdc.h> using namespace std; const int N 1e3 7;int n; struct NODE{vector<int> v;int ind 0; }g[N];int main( ) {cin >> n;int x;for(int i 1; i < n; i){for(int j 1; j< n-1; j){cin >> x;g[i].v.push_…

深入浅出MHA(MySQL Master High Availability)集群:原理、部署与实践

目录 引言 一、MHA集群介绍 &#xff08;一&#xff09;什么是MHA &#xff08;二&#xff09;MHA集群原理 &#xff08;三&#xff09;同步方式 &#xff08;四&#xff09;管理节点与数据节点 二、实现MHA &#xff08;一&#xff09;搭建主从复制环境 1.搭建时间同…

C语言例4-32:利用for语句实现循环次数未知的例子

从键盘输入若干个整数&#xff0c;求其中的最大者和最小者&#xff0c;直到输入“0”为止 算法分析&#xff1a; 读取第一个整数i&#xff0c;并假设它是当前最大整数max&#xff0c;也是当前最小整数min当,则重复执行以下操作&#xff0c;若i<min&#xff0c;则mini;从键…

Linux课程____Linux防火墙

一、包、过滤防火墙 包过滤内核&#xff1a;netfilter 规则管理工具&#xff1a;firewalld ,老版本linux: iptables工具 firewalld网络区域&#xff1a; 常用区域&#xff1a;trusted、home、public、external、block 二、格式 格式&#xff1a;firewall-cmd 【参数】 --per…

【软件安装】(十五)Ubuntu22.04+Anaconda安装labelimg

一个愿意伫立在巨人肩膀上的农民...... LabelImg是一款开源的图片标注工具&#xff0c;使用Python编写&#xff0c;基于PyQt5框架。它提供了一个直观的图形用户界面&#xff0c;方便用户对图片进行标注&#xff0c;并生成标注结果。LabelImg支持多种常见的标注格式&#xff0c;…

线程中的核心操作

线程中的核心操作 1:start()2:中断(终止)一个线程2.1:自己定义线程结束的代码2.1.1 存在的问题 2.2:使用Thread提供的interrupt()方法和isInterrupted()2.2.1 继续执行2.2.2 立即结束2.2.3 打印异常信息,再立即结束2.2.1 继续执行 22三级目录 1:start() start() 真正的创建线程…

数据资产的计量方式和后续计量如何确定?

对与数据资产的计量&#xff0c;可分为初始计量和后续计量两大环节来考虑。 一、在初始计量环节可采用按历史成本法计量和按公允价值计量两种方式 目前数据资产的计量属性主要包含历史成本、公允价值。企业数据资产可考虑从用途角度划分为内部开发型和外购型。 &#xff08;…

Android TargetSdkVersion 30 安装失败 resources.arsc 需要对齐且不压缩。

公司项目&#xff0c;之前targetSDKVersion一直是29&#xff0c;近期小米平台上架强制要求升到30&#xff0c;但是这个版本在android12上安装失败&#xff0c;我用adb命令安装&#xff0c;报错如下图 adb: failed to install c: Program Files (x86)(0A_knight\MorkSpace \Home…

Springboot构建测试类Test出现错误:Test class should have exactly one public constructor

&#xff08;1&#xff09;在SpringBoot中&#xff0c;分为Spring4和Spring5&#xff08;或Spring5以上版本&#xff09;&#xff0c;Spring4的Test测试类需要加上两个注解&#xff1a; SpringBootTest RunWith(SpringRunner.class) 导入的包是: import org.junit.Test; &am…

App地推统计神器,Xinstall让数据说话

App地推&#xff0c;作为移动应用推广的重要手段&#xff0c;一直以来都备受关注。然而&#xff0c;随着移动工具App进入存量时代&#xff0c;用户粘性降低&#xff0c;行业竞争日益激烈&#xff0c;地推的效果评估和提升成为了开发者们亟待解决的问题。在这个背景下&#xff0…

[C++初阶] 爱上C++ : 与C++的第一次约会

&#x1f525;个人主页&#xff1a;guoguoqiang &#x1f525;专栏&#xff1a;我与C的爱恋 本篇内容带大家浅浅的了解一下C中的命名空间。 在c中&#xff0c;名称&#xff08;name&#xff09;可以是符号常量、变量、函数、结构、枚举、类和对象等等。工程越大&#xff0c;名称…

Flink CDC 同步数据到Doris

Flink CDC 同步数据到Doris Flink CDC 是基于数据库日志 CDC(Change Data Capture)技术的实时数据集成框架,支持了全增量一体化、无锁读取、并行读取、表结构变更自动同步、分布式架构等高级特性。配合 Flink 优秀的管道能力和丰富的上下游生态,Flink CDC 可以高效实现海量…