真正防重提交需前后端协同:前端监听表单submit事件批量禁用提交控件并中止并发请求,后端校验一次性submit_token并做幂等处理。

限制用户仅提交一次,不能只靠前端禁用按钮——它只是防止误点的第一道屏障,真正起作用的是后端对 submit_token 的一次性校验和幂等设计。
监听 form.submit 事件统一禁用所有提交控件
只给按钮绑 onclick 或只禁用当前点击的元素,会漏掉回车提交、form.requestSubmit()、多个 type="submit" 按钮等场景。必须监听表单本身的 submit 事件:
- 在事件回调里先调
form.checkValidity(),校验失败时不执行禁用,避免用户输错后按钮永久灰掉 - 用
form.querySelectorAll('button[type="submit"], input[type="submit"]')批量禁用,不漏任何一个提交入口 - 禁用动作必须放在
e.preventDefault()之后、实际请求发起之前;否则异步请求还没发,按钮就已解锁 - 禁用后不要自动恢复——成功通常要跳转或清空表单;失败才需手动
btn.disabled = false
用 AbortController 中止并发 fetch 请求
按钮禁用只锁 UI,挡不住快速点击下已发出的多个 fetch。关键不是“防点”,而是“中止上一个”:
- 声明一个全局变量
currentController,每次提交前先执行currentController?.abort() - 新建
new AbortController(),把signal传进fetch的options -
catch中显式过滤AbortError,避免把中止当成业务错误上报 - IE 不支持
AbortController,老项目得用Promise.race([fetch(), timeoutPromise])模拟
后端必须校验绑定 session 的 submit_token
前端所有控制都可被绕过。真正防重靠后端——但随便塞个 Math.random() 进隐藏字段等于没做:
-
submit_token必须由后端生成(如 UUID),带签名、有时效(建议 5–15 分钟)、绑定当前session_id和表单类型(如user_register_form) - 服务端收到后,查 Redis/DB 是否存在且未使用;校验通过立即标记为“已消费”,再次提交返回
400 Bad Request - HTML 中只需写:
<input type="hidden" name="submit_token" value="f8a2e7d1-9c4b-4f0a-b3e5-1a2c3d4e5f6g">,值由后端每次渲染时注入 - 若用
fetch提交,需手动取document.querySelector('[name="submit_token"]').value拼进 body,不能依赖表单自动携带
最容易被忽略的是:后端没做幂等校验时,前端哪怕加了十层防护,重复数据照样入库。数据库唯一约束(如 user_id + order_time 组合索引)和接口级去重(如 X-Idempotency-Key 缓存)必须同步落地。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











