防重复提交需动态禁用按钮、监听submit事件、用abortcontroller终止并发请求,并配合服务端一次性submit_token校验。

为什么只给按钮加 disabled 属性没用
写 disabled="disabled" 或在 HTML 里静态设 disabled,页面一加载就禁用,用户根本点不了——这不是防重复,是直接废功能。真正要拦的是“提交动作触发后、响应还没回来前”的那几次连点。浏览器不会因为你写了这个属性,就自动在每次 submit 时帮你设一次;它只是个初始状态开关。
常见错误现象:按钮点了没反应、表单压根不提交,或者点第一下就卡死。原因往往是把 disabled 当成防重开关硬编码进 HTML,而不是在 JS 中动态控制。
- 必须在
submit事件或按钮click事件中,用 JS 执行btn.disabled = true - 禁用目标不能只盯一个按钮:要批量处理
form.querySelectorAll('button[type="submit"], input[type="submit"]') -
fieldset可以递归禁用子控件,但别给form标签加disabled——HTML 规范不支持,无效
监听 submit 事件比监听 click 更可靠
只监听按钮 click 会漏掉回车提交、form.requestSubmit()、JS 调用 form.submit() 这些路径。而 form.addEventListener('submit', ...) 是唯一覆盖所有提交入口的方案。
注意:如果业务代码里存在直接调用 form.submit()(比如某些 UI 库或校验后手动触发),它不会触发 submit 事件,此时禁用逻辑就失效了。稳妥做法是统一收口到一个提交函数,里面先禁用再发请求。
- 在事件处理器开头立即禁用所有提交控件,不要等
fetch或校验完成后再做 - 禁用前建议先调
form.checkValidity(),避免校验失败后按钮被永久锁死 - 成功提交后通常跳转或清空表单,不必恢复按钮;但失败或取消时,必须在
finally块里恢复btn.disabled = false
fetch 场景下必须配 AbortController
按钮禁用只拦 UI,挡不住快速点击触发的多个并发 fetch 请求。尤其在慢网或接口延迟高时,用户点两下,后端就收到两个请求——因为第一个 fetch 还没返回,第二个已经发出去了。
解决方法是用 AbortController 主动中止上一个请求:
- 声明一个全局变量:
let currentController = null - 每次提交前执行:
currentController?.abort(),再新建new AbortController()并赋值 - 把
currentController.signal传进fetch的options - 在
catch中显式判断err.name === 'AbortError',避免把中止当成业务错误上报
后端 submit_token 必须一次有效且绑定 session
前端所有操作都可被绕过:刷新页面、F5、开发者工具删掉 disabled、用 curl 或 Postman 直接发请求……所以 submit_token 不是可选配件,是必选项。
关键不在字段本身,而在服务端逻辑:
-
submit_token必须由后端生成(如bin2hex(random_bytes(16))或 UUID),绝不能是Math.random()或时间戳拼接 - 必须存入
$_SESSION['form_token'](PHP)或 Redis(带过期时间),并绑定当前用户 session 和表单类型 - 提交时比对
$_POST['submit_token'] === $_SESSION['form_token'],通过则立刻unset或DEL - 若用
fetch提交,需手动取值:document.querySelector('[name="submit_token"]').value,否则表单不会自动携带
最容易被忽略的是 token 绑定粒度:不能整个站点共用一个 token,也不能跨表单复用;订单页的 token 和评论页的 token 必须隔离,否则一个页面的重复提交可能意外解锁另一个页面的提交权限。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











