前端接口防止重复请求实现方案
前言
前段时间老板心血来潮,要我们前端组对整个的项目都做一下接口防止重复请求的处理(似乎是有用户通过一些快速点击薅到了一些优惠券啥的)。。。听到这个需求,第一反应就是,防止薅羊毛最保险的方案不还是在服务端加限制吗?前端加限制能够拦截的毕竟有限。可老板就是执意要前端搞一下子,行吧,搞就搞吧。
虽然大部分的接口处理我们都是加了loading的,但又不能确保真的是每个接口都加了的,可是如果要一个接口一个接口的排查,那这维护了四五年的系统,成百上千的接口肯定要耗费非常多的精力,根本就是不现实的,所以就只能去做全局处理。下面就来总结一下这次的防重复请求的实现方案:
方案一
这个方案是最容易想到也是最朴实无华的一个方案:通过使用axios拦截器,在请求拦截器中开启全屏Loading,然后在响应拦截器中将Loading关闭。
这个方案固然已经可以满足我们目前的需求,但不管三七二十一,直接搞个全屏Loading还是不太美观,何况在目前项目的接口处理逻辑中还有一些局部Loading,就有可能会出现Loading套Loading的情况,两个圈一起转,头皮发麻。
方案二
加Loading的方案不太友好,而对于同一个接口,如果传参都是一样的,一般来说都没有必要连续请求多次吧。那我们可不可以通过代码逻辑直接把完全相同的请求给拦截掉,不让它到达服务端呢?这个思路不错,我们说干就干。
首先,我们要判断什么样的请求属于是相同请求:
一个请求包含的内容不外乎就是请求方法,地址,参数以及请求发出的页面hash。那我们是不是就可以根据这几个数据把这个请求生成一个key来作为这个请求的标识呢?
// 根据请求生成对应的key
function generateReqKey(config, hash) {const { method, url, params, data } = config;return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}
有了请求的key,我们就可以在请求拦截器中把每次发起的请求给收集起来,后续如果有相同请求进来,那都去这个集合中去比对,如果已经存在了,说明就是一个重复的请求,我们就给拦截掉。当请求完成响应后,再将这个请求从集合中移除。合理,nice!
具体实现如下:
是不是觉得这种方案还不错,万事大吉?
这个方案虽然理论上是解决了接口防重复请求这个问题,但是它会引发更多的问题。
比如,我有这样一个接口处理:
那么,当我们触发多次请求时:
这里我连续点击了4次按钮,可以看到,的确是只有一个请求发送出去,可是因为在代码逻辑中,我们对错误进行了一些处理,所以就将报错消息提示了3次,这样是很不友好的,而且,如果在错误捕获中有做更多的逻辑处理,那么很有可能会导致整个程序的异常。
而且,这种方案还会有另外一个比较严重的问题:
我们在上面在生成请求key的时候把hash考虑进去了(如果是history路由,可以将pathname加入生成key),这是因为项目中会有一些数据字典型的接口,这些接口可能有不同页面都需要去调用,如果第一个页面请求的字典接口比较慢,第二个页面的接口就被拦截了,最后就会导致第二个页面逻辑错误。那么这么一看,我们生成key的时候加入了hash,讲道理就没问题了呀。
可是倘若我这两个请求是来自同一个页面呢?
比如,一个页面同时加载两个组件,而这两个组件都需要调用某个接口时:
那么此时,后调接口的组件就无法拿到正确数据了。啊这,真是难顶!
方案三
方案二的路子,我们发现确实问题重重,那么接下来我们来看第三种方案,也是我们最终采用的方案。
延续我们方案二的前面思路,仍然是拦截相同请求,但这次我们可不可以不直接把请求挂掉,而是对于相同的请求我们先给它挂起,等到最先发出去的请求拿到结果回来之后,把成功或失败的结果共享给后面到来的相同请求。
思路我们已经明确了,但这里有几个需要注意的点:
-
我们在拿到响应结果后,返回给之前我们挂起的请求时,我们要用到发布订阅模式(日常在面试题中看到,这次终于让我给用上了
-
对于挂起的请求,我们需要将它拦截,不能让它执行正常的请求逻辑,所以一定要在请求拦截器中通过
return Promise.reject()
来直接中断请求,并做一些特殊的标记,以便于在响应拦截器中进行特殊处理。
最后,直接附上完整代码:
import axios from "axios"let 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]) returnelse {this.event[type].forEach(cbArr => {if(ansType === 'resolve') {cbArr[0](res)}else{cbArr[1](res)}});}}
}
// 根据请求生成对应的key
function generateReqKey(config, hash) {const { method, url, params, data } = config;return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}// 存储已发送但未响应的请求
const pendingRequest = new Set();
// 发布订阅容器
const ev = new EventEmitter()// 添加请求拦截器
instance.interceptors.request.use(async (config) => {let hash = location.hash// 生成请求Keylet reqKey = generateReqKey(config, hash)if(pendingRequest.has(reqKey)) {// 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果// 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器let res = nulltry {// 接口成功响应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{// 将请求的key保存在configconfig.pendKey = reqKeypendingRequest.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.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}
}// 接口走失败响应
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.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 instance;
补充
到这里,这么一通操作下来上面的代码讲道理是万无一失了,但不得不说,线上的情况仍然是复杂多样的。而其中一个比较特殊的情况就是文件上传。
可以看到,我在这里是上传了两个不同的文件的,但只调用了一次上传接口。按理说是两个不同的请求,可为什么会被我们前面写的逻辑给拦截掉一个呢?
我们打印一下请求的config:
可以看到,请求体data中的数据是FormData类型,而我们在生成请求key的时候,是通过JSON.stringify
方法进行操作的,而对于FormData类型的数据执行该函数得到的只有{}
。所以,对于文件上传,尽管我们上传了不同的文件,但它们所发出的请求生成的key都是一样的,这么一来就触发了我们前面的拦截机制。
那么我们接下来我们只需要在我们原来的拦截逻辑中判断一下请求体的数据类型即可,如果含有FormData类型的数据,我们就直接放行不再关注这个请求就是了。
function isFileUploadApi(config) {return Object.prototype.toString.call(config.data) === "[object FormData]"
}
最后
到这里,整个的需求总算是完结啦!不用一个个接口的改代码,又可以愉快的打代码了,nice!
相关文章:

前端接口防止重复请求实现方案
前言 前段时间老板心血来潮,要我们前端组对整个的项目都做一下接口防止重复请求的处理(似乎是有用户通过一些快速点击薅到了一些优惠券啥的)。。。听到这个需求,第一反应就是,防止薅羊毛最保险的方案不还是在服务端加…...
【leetcode面试经典150题】13.除自身以外数组的乘积(C++)
【leetcode面试经典150题】专栏系列将为准备暑期实习生以及秋招的同学们提高在面试时的经典面试算法题的思路和想法。本专栏将以一题多解和精简算法思路为主,题解使用C语言。(若有使用其他语言的同学也可了解题解思路,本质上语法内容一致&…...

网络编程核心概念解析:IP地址、端口号与网络字节序深度探讨
⭐小白苦学IT的博客主页 ⭐初学者必看:Linux操作系统入门 ⭐代码仓库:Linux代码仓库 ❤关注我一起讨论和学习Linux系统 本节重点 认识IP地址, 端口号, 网络字节序等网络编程中的基本概念; 1.前言 网络编程,作为现代信息社会中的一项核心技术&…...
突破编程_C++_网络编程(TCPIP 四层模型(网络层(1))
1 网络层概述 TCP/IP 四层模型中的网络层是模型中的核心组成部分,它主要负责处理数据包的路由和转发,确保数据能够在源主机和目标主机之间准确地传输。 一、主要功能 网络层的主要功能是实现数据包的选路和转发。当数据从应用层传输到传输层后&#x…...

Java | Leetcode Java题解之第9题回文数
题目: 题解: class Solution {public boolean isPalindrome(int x) {// 特殊情况:// 如上所述,当 x < 0 时,x 不是回文数。// 同样地,如果数字的最后一位是 0,为了使该数字为回文࿰…...

极简云验证 download.php 文件读取漏洞复现
0x01 产品简介 极简云验证是一款开源的网络验证系统,支持多应用卡密生成:卡密生成 单码卡密 次数卡密 会员卡密 积分卡密、卡密管理 卡密长度 卡密封禁 批量生成 批量导出 自定义卡密前缀等;支持多应用多用户管理:应用备注 应用版…...

红黑树路径长度分析:证明与实现
红黑树路径长度分析:证明与实现 一、红黑树的基本性质二、证明:最长路径至多是最短路径的2倍2.1 证明思路2.2 证明过程 三、伪代码实现四、 C语言代码实现5、 结论 红黑树作为一种高效的自平衡二叉搜索树,在计算机科学领域中被广泛应用于各种…...
esp32 gpio初识(一)
目录 功能介绍 实操 功能介绍 引脚又叫管脚,英文叫 Pin, 就是从集成电路(芯片以及一些电子元件)内部电路引出与外围电路的接线的接口。 在我们的 ESP32 开发板上, 我们可以把这些称为引脚, 这些引脚其实是从 ESP32 芯片内部引出来的, 我们…...
python 自制黄金矿工游戏(设计思路+源码)
1.视频效果演示 python自制黄金矿工,细节拉满沉浸式体验,看了你也会 2.开发准备的工具 python3.8, pygame库(python3.5以上的版本应该都可以) 图片处理工具,美图秀秀 截图工具,电脑自带的 自动抠图网页:https://ko…...

Splunk Attack Range:一款针对Splunk安全的模拟测试环境创建工具
关于Splunk Attack Range Splunk Attack Range是一款针对Splunk安全的模拟测试环境创建工具,该工具完全开源,目前由Splunk威胁研究团队负责维护。 该工具能够帮助广大研究人员构建模拟攻击测试所用的本地或云端环境,并将数据转发至Splunk实例…...

OpenCV入门例程:裁剪图片、模糊检测、黑屏检测
初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的,可以在任何平台上使用。 本例程运行环境为CentOS7&…...
opencv-python库 cv2边界填充resize图片
文章目录 边界填充改变图片大小 边界填充 在OpenCV中,边界填充(Border Padding)是指在图像周围添加额外的像素,以扩展图像的尺寸或满足某些算法(如卷积)的要求。OpenCV提供了cv2.copyMakeBorder()函数来进…...

Java代码基础算法练习-负数个数统计-2024.04.04
任务描述: 从键盘输入任意10个整型数(数值范围-100000~100000),统计其中的负数个数 任务要求: 代码示例: package April_2024;import java.util.Scanner;// 从键盘输入任意10个整型数(数值范围…...
【算法刷题day17】Leetcode:110.平衡二叉树 257. 二叉树的所有路径 404.左叶子之和
110.平衡二叉树 文档链接:[代码随想录] 题目链接::110.平衡二叉树 题目: 给定一个二叉树,判断它是否是 平衡二叉树 注意: 判断两棵子树高度差是否大于1 class Solution { public:int result;bool isBalanced(TreeNode…...

C++ | Leetcode C++题解之第10题正则表达式匹配
题目: 题解: class Solution { public:bool isMatch(string s, string p) {int m s.size();int n p.size();auto matches [&](int i, int j) {if (i 0) {return false;}if (p[j - 1] .) {return true;}return s[i - 1] p[j - 1];};vector<…...
职场迷航?MBTI测试为你指明方向,找到最匹配的职业!
MBTI简介 MBTI的全名是Myers-Briggs Type Indicator。它是一种迫选型、自我报告式的性格评估工具,用以衡量和描述人们在获取信息、作出决策、对待生活等方面的心理活动规律和性格类型。 类型指标 美国的凯恩琳布里格斯和她的女儿伊莎贝尔布里格斯迈尔斯研制了迈尔…...

hive 慢sql 查询
hive 慢sql 查询 查找 hive 执行日志存储路径(一般是 hive-audit.log ) 比如:/var/log/Bigdata/audit/hive/hiveserver/hive-audit.log 解析日志 获取 执行时间 执行 OperationId 执行人 UserNameroot 执行sql 数据分隔符为 \001 并写入 hiv…...

Vue - 2( 10000 字 Vue 入门级教程)
一:Vue 1.1 绑定样式 1.1.1 绑定 class 样式 <!DOCTYPE html> <html><head><meta charset"UTF-8" /><title>绑定样式</title><style>......</style><script type"text/javascript" src&…...

Cisco交换机安全配置
Cisco交换机安全配置 前提 我们以下命令一般都要先进入Config模式 S1> enable S1# conf t S1(config)#端口安全保护 禁用未使用的端口 以关闭fa0/1到fa0/24的端口为例 S1(config)# interface range fa0/1-24 S1(config-if-range)# shutdown缓解MAC地址表攻击 防止CAM…...

LLM大模型可视化-以nano-gpt为例
内容整理自:LLM 可视化 --- LLM Visualization (bbycroft.net)https://bbycroft.net/llm Introduction 介绍 Welcome to the walkthrough of the GPT large language model! Here well explore the model nano-gpt, with a mere 85,000 parameters. 欢迎来到 GPT 大…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...

Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...

3.3.1_1 检错编码(奇偶校验码)
从这节课开始,我们会探讨数据链路层的差错控制功能,差错控制功能的主要目标是要发现并且解决一个帧内部的位错误,我们需要使用特殊的编码技术去发现帧内部的位错误,当我们发现位错误之后,通常来说有两种解决方案。第一…...
基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架,支持"一次开发,多端部署",可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务,为旅游应用带来…...

HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一)
宇树机器人多姿态起立控制强化学习框架论文解析 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(一) 论文解读:交大&港大&上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化…...

零基础设计模式——行为型模式 - 责任链模式
第四部分:行为型模式 - 责任链模式 (Chain of Responsibility Pattern) 欢迎来到行为型模式的学习!行为型模式关注对象之间的职责分配、算法封装和对象间的交互。我们将学习的第一个行为型模式是责任链模式。 核心思想:使多个对象都有机会处…...

CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...