上一篇把“防重复提交”拆成了两个目标:时间上不连发,数据上不重复落库。这一篇把两个目标落成三种机制,讲清楚各自管哪一段、放在哪一层、怎么验收。

三种机制名字相近,职责却不重叠。混用最常见的结果是:前端节流加了一层又一层,重复数据依然产生。

节流:管的是时间,落在前端按钮

节流保证同一时刻只有一个请求在飞。它的实现位置在前端,作用于按钮或请求入口。

最简单的写法是点击后置灰:

const onSave = async (form) => {
  if (saving.value) return;
  saving.value = true;
  try {
    await saveApi(form);
  } finally {
    saving.value = false;
  }
};

finally 保证成功和异常都会复位,这是节流最容易被漏掉的一半。只写 then 里的复位,异常时按钮就卡死。

web-skills 把这段交给 API 配置 btnState,在请求拦截器打开、响应和异常里关闭,作用等同于上面的 saving,只是把开关搬到了请求层。目标项目用组件状态还是请求配置都行,关键是开关的关闭要覆盖所有结束路径,thencatchfinally 一个都不能少。还有一层要单独核对:btnStateweb-skills 里是一份全局布尔状态,多个按钮共用时,一个按钮的请求结束会不会把另一个正在等待的按钮也提前复位,这属于节流之外的归属问题。

节流不关心两次点击之间隔了多久,只关心“这一刻有没有请求在飞”。所以它挡得住连点,挡不住提交成功后过一会儿再来一次。

去重:管的是同一份未完成的请求

去重解决的是前端自己造成的重复发出:同一个操作,在旧请求还没结束时又被触发了一次。

它解决的不是重复数据,是重复请求,两者别混。

它和节流有重叠,但口径更细。节流看“有没有请求”,去重看“是不是同一份请求”。比如用户连点两次翻页,节流可以挡,但更合适的是取消前一个未完成的翻页请求,只保留最新一个,而不是把新的也拦掉。

结构示意:

let currentRequestId = 0;
​
const loadList = async (params) => {
  const requestId = ++currentRequestId;
  const res = await listApi(params);
  if (requestId !== currentRequestId) return;
  commitList(res);
};

这不算真正取消请求,只是让过期响应失去提交资格。前面分页和加载两篇都提过这个思路,这里它承担的角色是:同一块区域只认最新一次操作的结果,旧结果不再覆盖新状态。

去重管的是前端并发,挡不住用户刷新页面后重新提交,也挡不住超时后人为重试。那些场景里,前端旧请求早就结束了,没有“未完成”可以去重。

幂等:管的是数据,落在后端

幂等保证同一份数据最终只落一次,无论请求发了几遍。它的实现位置在后端,前端只能配合,不能替代。

后端常见的落法是给提交请求带一个幂等键,或者用业务唯一约束兜底。比如保存一条记录,前端生成一次性的请求编号,后端收到相同编号就不重复写入。

const idempotencyKey = crypto.randomUUID();
await saveApi(form, { headers: { 'Idempotency-Key': idempotencyKey } });

这里 crypto.randomUUID() 只是示意,项目要用自己约定的幂等键生成方式。关键是同一笔操作、同一次重试,键要一致;用户主动做的第二笔新操作,键要不同。键的语义错了,幂等形同虚设。

幂等解决的是刷新再提交、超时重发、网关重发这类“前端已经无法控制”的重复。前端能做的只有一件事:把幂等键生成对、传对,真正拦截重复的判定必须由后端完成。

三个机制怎么选

重复场景 首选机制 实现位置 说明
连点 节流 前端按钮或请求层 请求期间置灰
慢请求重复点击 节流 前端请求层 状态覆盖整个 settle
同一区域并发操作 去重 前端请求入口 只认最新请求的结果
提交后刷新再提交 幂等 后端 唯一约束或幂等键
超时重发 幂等 后端为主 前端配合传幂等键

这张表不是让每行只选一种。一个完整的保存流程,往往前端节流加后端幂等同时用:节流挡连点,幂等兜底重发。缺的是把两层分开想清楚,而不是在一个按钮上堆三个 disabled。

验收要分静态和运行

静态能查的:节流开关的复位是否覆盖成功和异常;幂等键是否每次提交都重新生成;去重判断是否影响正常刷新。

要跑页面才能确认的:慢接口下连点是否只发一次;提交成功后刷新再提交是否产生第二条记录;超时重试会不会重复落库。最后两条依赖后端环境,没有条件就把它们列为未验证,别在文章里替结果下结论。

交给 Codex 的任务模板

请为这个保存流程整理防重复提交方案,不先改代码:
​
1. 列出可能产生重复提交的场景:连点、慢请求重复点击、提交后刷新、超时重试、同一区域并发操作。
2. 为每个场景标注对应的机制:节流、去重或幂等。
3. 说明当前项目已有的防护:btnState 或类似节流、请求去重、后端幂等,各自覆盖到哪一层。
4. 指出缺口:哪些重复场景目前没有任何机制拦截。
5. 对缺失部分给出最小方案,标注实现位置是前端还是后端。
​
先输出场景与机制对照表,再给修改方案。不要在前端重复堆叠节流来替代后端幂等,也不要绕开项目已有的请求封装。

这条任务的前半段是盘点,不是直接开写。防重复提交的坑,多数是没盘清楚“哪些场景已有防护、哪些是空的”,就急着在按钮上补状态。

写在最后

节流、去重、幂等各管一段:节流管时间,去重管并发,幂等管数据。把三件事混成一个“防重复”,结果往往是前端加满、后端漏掉。

先按场景选出机制,再按位置落到前端或后端。前端能做的是节流和去重,幂等这道关只能交给后端。

下一篇进入列表接口异常的处理,看请求失败时页面应该怎样退化:空状态、错误提示和重试各自该怎么设计,而不是一个空 catch 了事。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐