如何避免用户手速过快多次点击触发 api 多次调用

一、修复「极速连点并发间隙漏洞」

问题根源

点击判断 if (!loading) 和赋值 loading=true 中间存在执行间隙,超快双击会同时进入逻辑,发起多次请求。

优化写法:先锁,再判断,用原子锁逻辑

复制代码
let loading = false;
async handleSubmit() {
  // 1. 先抢占锁,再判断,消除间隙
  if (loading) return;
  loading = true; 

  try {
    // 接口逻辑
    await requestApi();
  } catch (err) {
    // 异常统一处理
    message.error('提交失败');
  } finally {
    // 无论成功/失败/报错,强制释放锁
    loading = false;
  }
}

关键点:loading = true 提前到最顶部,没有任何中间代码,杜绝并发间隙。

进阶:封装全局请求锁(统一处理,不用每个按钮写 finally)

基于 axios 请求拦截器自动加 loading 锁,统一释放:

复制代码
const pendingMap = new Map();
// 生成唯一key:接口url + 请求方式 + 参数
function getReqKey(config) {
  return `${config.method}-${config.url}-${JSON.stringify(config.data)}`;
}

// 请求拦截
axios.interceptors.request.use(config => {
  const key = getReqKey(config);
  if (pendingMap.has(key)) {
    // 相同请求正在执行,直接取消本次重复请求
    const cancel = pendingMap.get(key);
    cancel();
  }
  // 创建取消令牌
  const controller = new AbortController();
  config.signal = controller.signal;
  pendingMap.set(key, controller.abort);
  return config;
});

// 响应拦截(成功/失败都清除锁)
axios.interceptors.response.use(res => {
  pendingMap.delete(getReqKey(res.config));
  return res;
}, err => {
  if (err.config) pendingMap.delete(getReqKey(err.config));
  return Promise.reject(err);
})

效果:短时间相同参数重复调用 API,直接取消重复请求,从网络层面拦截。

二、解决「异常 / 页面跳转导致 loading 永久锁死」

  1. 强制使用 finally:所有异步请求必须在 finally 重置 loading,禁止只在成功回调释放;
  2. 页面销毁自动清除 loading(Vue/React)

vue

复制代码
// Vue3
onUnmounted(() => {
  loading = false;
  // 取消页面所有pending请求
  pendingMap.clear();
})

jsx

复制代码
// React useEffect清理函数
useEffect(() => {
  return () => {
    setLoading(false);
  }
}, [])
  1. 超时自动解锁兜底 增加延时兜底,弱网卡死时自动恢复按钮:

js

复制代码
async handleSubmit() {
  if (loading) return;
  loading = true;
  // 兜底:10秒强制解锁,防止永久卡死
  const timer = setTimeout(() => loading = false, 10000);
  try {
    await api();
  } finally {
    clearTimeout(timer);
    loading = false;
  }
}

三、解决「前端禁用可被控制台绕过」

前端只做体验拦截,必须搭配双层防护

1. 前端增加函数级防重,不只依赖 UI 禁用

哪怕用户手动解除按钮 disabled,重复调用函数也会被拦截:

js

复制代码
async handleSubmit() {
  if (loading) return; // 函数入口锁,不受DOM属性控制
  loading = true;
  // ...
}

用户删除按钮 disabled,但直接调用 handleSubmit() 依然会被 loading 拦截。

2. 配合防抖 / 节流二次兜底

对点击事件做防抖(300~500ms),双保险:

js

复制代码
// 工具函数防抖
function debounce(fn, delay = 500) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  }
}
const submit = debounce(handleSubmit);

3. 最终防线:后端幂等校验(不可缺少)

前端所有优化都只是体验优化,无法防爬虫、Postman、抓包重放。

  • 前端每次请求携带 idempotentKey(UUID + 时间戳);
  • 后端 Redis SETNX 校验,相同 key 短时间内直接拒绝;
  • 核心业务(下单、支付)增加数据库唯一索引。

四、解决「多标签 / 多窗口状态不同步」

前端内存变量 loading 只作用当前页面,多窗口失效,两种优化方案:

方案 1:LocalStorage + storage 事件同步 loading 状态

点击提交时写入 storage 标记,其他页面监听 storage 事件同步按钮禁用:

js

复制代码
// 提交时
localStorage.setItem('submit_lock', Date.now());
// 其他页面监听
window.addEventListener('storage', (e) => {
  if (e.key === 'submit_lock') {
    // 5秒内禁止提交
    setLoading(true);
    setTimeout(() => setLoading(false), 5000);
  }
})

方案 2:依赖后端幂等统一控制

不依赖前端同步,跨页面重复提交由后端直接拦截,成本更低、更稳定。

五、解决「多按钮共用 loading、无按钮场景无法拦截」

  1. 每个按钮独立 loading 状态 不要全局共用一个 loading 变量,每个操作单独定义:

js

复制代码
const submitLoading = ref(false);
const saveLoading = ref(false);
  1. 无按钮场景统一封装请求锁 表格点击、回车快捷键、弹窗自动请求统一使用上面的 pendingMap 请求拦截,不依赖 DOM 按钮;
  2. 全局封装通用提交组件 封装通用 SubmitButton,内置 loading、防抖、自动清除逻辑,全局复用,避免手写遗漏。

六、解决「弱网 pending 交互差」体验问题

  1. 增加加载文字提示:提交中,请稍候...,告知用户当前状态;
  2. 增加取消请求按钮,支持中断 pending 接口;
  3. 设置统一接口超时时间(6~10s),超时自动释放 loading 并提示用户。

七、完整最优组合(工业级标准)

前端三层拦截(体验层)

  1. UI 层:按钮 disabled + loading 视觉拦截;
  2. 函数层:函数开头 loading 锁 + 防抖节流,绕过 DOM 也拦不住;
  3. 请求层:axios 拦截器 pendingMap 取消重复相同请求。

兜底优化(解决体验 bug)

  • finally 强制释放 loading + 页面卸载清理锁 + 超时兜底解锁;
  • 多操作独立 loading 变量,封装通用按钮组件减少重复代码。

后端一层终极拦截(安全层,必不可少)

幂等 ID + Redis 防重 + 数据库唯一索引 + 接口限流。

优化前后对比总结

原有缺点 优化方案
控制台解除 disabled 可重复点击 函数入口 loading 锁 + 后端幂等
极速连点并发多发请求 最顶部抢占 loading 锁 + axios 取消重复请求
报错 / 跳转按钮永久置灰 finally 释放 + 页面卸载清理 + 超时兜底
多标签页可重复提交 storage 同步锁 / 后端统一幂等拦截
多按钮共用 loading 互相干扰 每个操作独立 loading 变量
无按钮场景无法拦截 axios 全局 pending 请求拦截
弱网卡住无反馈 加载文案 + 接口超时自动解锁
只能防普通用户,防不住爬虫 后端幂等、限流、唯一索引兜底
相关推荐
wuqingshun3141592 小时前
什么是责任链模式,一般用在什么场景?
java·责任链模式
世界哪有真情3 小时前
拿人类意识卡 AI?等于用 bug 验收正式产品
前端·人工智能·后端
:-)3 小时前
算法-归并排序
java·开发语言·数据结构·算法·排序算法
wuqingshun3141593 小时前
说一下消息队列的模型有哪些?
java
Listen·Rain3 小时前
Vue3:setup详解
前端·javascript·vue.js
fīɡЙtīиɡ ℡4 小时前
布隆过滤器
java
Patrick_Wilson4 小时前
修好 bug 只是开始:一次由监控需求反向重构日志结构的复盘
前端·监控·数据可视化
yaoxin5211234 小时前
462. Java 反射 - 获取声明类与封闭类
java·开发语言·python
灯琰14 小时前
STM32F4+W5500 移植与开发常见问题
网络
kyriewen5 小时前
面试官让我优化一个卡死的表格,我打开 Claude 五秒重写,他放下咖啡问我要不要来当组长
前端·javascript·面试