真正防重需前后端协同:前端监听form submit事件统一拦截、禁用所有提交控件并设提示,fetch配AbortController中止旧请求;后端必须校验一次性绑定session的submit_token并立即标记已消费,且表单重置后需服务端幂等校验。

仅靠 disabled 属性禁用按钮,无法真正限制重复点击——它只拦 UI,不拦请求,回车、刷新、脚本调用、多按钮场景全都会绕过。
监听 submit 事件才是唯一覆盖所有提交路径的方式
表单可通过点击按钮、按 Enter 键、form.requestSubmit()、甚至 form.submit()(绕过事件)触发提交。只有监听 form 元素的 submit 事件,才能统一拦截全部路径。
- 别只给某个
button绑onclick:漏掉回车和多按钮场景 - 别在
form.submit()调用前手动禁用:这个方法根本不触发submit事件 - 正确做法:在
submit回调里,先e.preventDefault(),再批量禁用所有提交控件:form.querySelectorAll('button[type="submit"], input[type="submit"]') - 禁用后记得设文字提示(如“提交中”),否则用户可能以为卡死而强制刷新
fetch 提交时必须配 AbortController 中止上一个请求
按钮禁用只锁 UI,挡不住已发出但尚未响应的请求。网速慢时用户连点两下,后端大概率收到两个并发 fetch 请求。
- 声明一个全局变量
currentController(类型为AbortController) - 每次提交前执行
currentController?.abort(),再新建实例并赋值 - 把新实例的
signal传进fetch的options,并在catch中显式过滤AbortError,避免误当业务错误 - IE 不支持
AbortController,老项目需降级用Promise.race([fetch(), timeoutPromise])
后端 submit_token 必须一次性消费且绑定 session
前端所有控制都可被绕过。真正防重靠后端,但随便塞个 Math.random() 到隐藏字段等于没做。
-
submit_token必须由后端生成(如带签名的 UUID),有效期建议 5–15 分钟 - 必须绑定当前用户
session_id和具体表单类型(如user_register_form),不可跨表单复用 - 服务端收到后,先查 Redis/DB 是否存在且未使用;校验通过立即标记为“已消费”,再次提交返回
400 Bad Request - 若用
fetch提交,需手动取document.querySelector('[name="submit_token"]').value拼进 body,不能依赖表单自动携带
最容易被忽略的是:表单重置(form.reset())会把 disabled 状态还原,导致防重失效;而服务端若没做幂等校验,哪怕 token、abort、禁用全上了,重复数据照样入库。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











