基于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://www.mzph.cn/news/779499.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

android安卓餐厅点餐课设

一、引言 随着移动互联网的快速发展&#xff0c;手机应用已经成为我们日常生活中不可或缺的一部分。餐饮行业也积极借助移动应用的力量&#xff0c;提供更便捷、高效的点餐服务。本文将介绍一个基于安卓系统开发的餐厅点餐APP的课程设计项目&#xff0c;探讨其设计理念、功能特…

【容器源码篇】Map容器(HashTable,HashMap,TreeMap的特点)

文章目录 ⭐容器继承关系&#x1f339;Map容器&#x1f5d2;️HashTable源码解析构造方法put方法remove方法rehash扩容 &#x1f5d2;️HashMap源码解析构造函数get方法put方法详解 扩容方法详解 &#x1f5d2;️TreeMap源码解析 ⭐容器继承关系 &#x1f339;Map容器 键值对映…

如何在 Mac 上打开、编辑、复制、移动或删除存储在 Windows NTFS 格式 USB 驱动器上的文件 Tuxera NTFS for Mac使用教程

当您获得一台新 Mac 时&#xff0c;它只能读取 Windows NTFS 格式的 USB 驱动器。要将文件添加、保存或写入您的 Mac&#xff0c;您需要一个附加的 NTFS 驱动程序。Tuxera 他可以帮忙实现这一功能&#xff01; Tuxera可以轻松转换驱动器&#xff1a;无论使用Windows PC还是Mac&…

OpenGL的MVP矩阵理解

OpenGL的MVP矩阵理解 右手坐标系 右手坐标系与左手坐标系都是三维笛卡尔坐标系&#xff0c;他们唯一的不同在于z轴的方向&#xff0c;如下图&#xff0c;左边是左手坐标系&#xff0c;右边是右手坐标系 OpenGL中一般用的是右手坐标系 1.模型坐标系&#xff08;Local Space&…

42 ajax 下载文件未配置 responseType blob 导致的文件异常

前言 这是一个最近的关于文件下载碰到的一个问题 主要的情况是, 基于 xhr 发送请求, 获取下载的文件 然后 之后 xhr 这边拿到 字节序列之后, 封装 blob 来进行下载 然后 最开始我们这边没有配置 responseType 为 blob, arraybuffer, 然后 导致下载出来的 文件大小超过了…

Image-Adaptive YOLO for Object Detection in Adverse Weather Conditions(IA-YOLO)

1、总体概述 基于深度学习的目标检测在常规条件的数据集可以获得不错的结果&#xff0c;但是在环境、场景、天气、照度、雾霾等自然条件的综合干扰下&#xff0c;深度学习模型的适应程度变低&#xff0c;检测结果也随之下降&#xff0c;因此研究在复杂气象条件下的目标检测方法…

警务数据仓库的实现

目录 一、SQL Server 2008 R2&#xff08;一&#xff09;SQL Server 的服务功能&#xff08;二&#xff09;SQL Server Management Studio&#xff08;三&#xff09;Microsoft Visual Studio 二、创建集成服务项目三、配置“旅馆_ETL”数据流任务四、配置“人员_ETL”数据流任…

k8s安装traefik作为ingress

一、先来介绍下Ingress Ingress 这个东西是 1.2 后才出现的&#xff0c;通过 Ingress 用户可以实现使用 nginx 等开源的反向代理负载均衡器实现对外暴露服务&#xff0c;以下详细说一下 Ingress&#xff0c;毕竟 traefik 用的就是 Ingress 使用 Ingress 时一般会有三个组件: …

基于SSM的高校普法系统(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的高校普法系统&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring Spri…

inkscape中文版本 G代码生成器(支持中英文及数字)使用

inkscape G代码生成器&#xff08;支持中英文及数字&#xff09;使用 1 inkscape安装1. 界面介绍2. 基本操作3. 图形编辑4. 图层管理5. 文件操作6. 高级功能7. 学习资源 2 laserengraver插件安装3 inkscape 使用candle 验证G代码效果 1 inkscape安装 跟着提示默认按键即可。 软…

HTML网站的概念

目录 前言&#xff1a; 1.什么是网页&#xff1a; 2.什么是网站&#xff1a; 示例&#xff1a; 3.服务器&#xff1a; 总结&#xff1a; 前言&#xff1a; HTML也称Hyper Text Markup Language&#xff0c;意思是超文本标记语言&#xff0c;同时HTML也是前端的基础&…

Linux 环境安装Nginx—源码和Dokcer两种安装方式

一、源代码编译安装Nginx 1.下载最新nginx源码 以nginx-1.25.3.tar.gz为例&#xff1a; 可以使用命令(联网)&#xff1a;curl -O http://nginx.org/download/nginx-1.25.3.tar.gz或在官网下载.tar.gz 2.解压缩 tar -zxvf nginx-1.25.3.tar.gz cd nginx-1.25.3/ 3.安装依赖…

HarmonyOS实战开发-实现自定义弹窗

介绍 本篇Codelab基于ArkTS的声明式开发范式实现了三种不同的弹窗&#xff0c;第一种直接使用公共组件&#xff0c;后两种使用CustomDialogController实现自定义弹窗&#xff0c;效果如图所示 相关概念 AlertDialog&#xff1a;警告弹窗&#xff0c;可设置文本内容和响应回调…

【javaWeb 第八篇】后端-Mybatis(万字详细学习)

Mybatis框架 前言MybatisMybatis入门配置SQL提示JDBC数据库连接池lombok Mybatis基础操作日志输出Mybatis的动态SQL 前言 这篇是作者学习数据持久层框架Mybatis的学习笔记&#xff0c;希望对大家有所帮助&#xff0c;希望大家能够与作者交流讨论 Mybatis Mybatis是一款优秀的…

Android 开发 Spinner setSelection 不起作用

问题 Android 开发 Spinner setSelection 不起作用 详细问题 笔者进行Android项目开发&#xff0c;根据上一个页面用户选择数据&#xff0c;显示当前页面Spinner选项&#xff0c;调用 Spinner setSelection 不起作用。 相关java代码 spinner.setAdapter(adapter); …

uniapp对接萤石云 实现监控播放、云台控制、截图、录像、历史映像等功能

萤石云开发平台地址&#xff1a;文档概述 萤石开放平台API文档 (ys7.com) 萤石云监控播放 首先引入萤石云js js地址&#xff1a;GitHub - Ezviz-OpenBiz/EZUIKit-JavaScript-npm: 轻应用npm版本&#xff0c;降低接入难度&#xff0c;适配自定义UI&#xff0c;适配主流框架 vi…

C语言例4-35:鸡翁一,值钱五;鸡母一,值钱三;鸡雏三,值钱一。百钱买百鸡、问鸡翁、鸡母和鸡雏各几何?

方法一&#xff1a; 代码如下&#xff1a; //鸡翁一&#xff0c;值钱五&#xff1b;鸡母一&#xff0c;值钱三&#xff1b;鸡雏三&#xff0c;值钱一。百钱买百鸡、问鸡翁、鸡母和鸡雏各几何&#xff1f; //方法一&#xff1a; #include<stdio.h> int main(void) {int x…

简易挛生分拣系统设计

1 工效组合展示 2 方案规划设计 3 数字挛生建模 基础建模、动画设计、模型导出 4 软件体系架构 5 Web交互设计 5.1 页面架构 5.2 初始构造 5.3 模型运用 5.4 WS通信 5.5 运行展现 6 服务支撑编码 6.1 整体调度 6.2 WS服务 6.3 C/S通信 7 系统级调试完善

李雅普诺夫函数

李雅普诺夫函数是一种用于描述动力系统稳定性的数学工具。它在动力系统和控制理论中具有广泛的应用&#xff0c;尤其是在研究非线性系统的稳定性方面。 李雅普诺夫函数通常用于证明动力系统在一些条件下是稳定的。一个李雅普诺夫函数是一个实数值函数&#xff0c;通常表示为 V…

使用mybatis的@Interceptor实现拦截sql

一 mybatis的拦截器 1.1 拦截器介绍 拦截器是一种基于 AOP&#xff08;面向切面编程&#xff09;的技术&#xff0c;它可以在目标对象的方法执行前后插入自定义的逻辑。 1.2 语法介绍 1.注解Intercepts Intercepts({Signature(type StatementHandler.class, method “…