必须监听form的submit事件并立即调用event.preventDefault(),才能覆盖回车、form.submit()、requestSubmit等所有提交路径;禁用按钮需批量操作且配合服务端token校验。

监听 submit 事件而不是 click 事件
跨表单提交(比如按钮在 form 外、或多个表单共用一个提交入口)时,只监听 click 会漏掉回车提交、form.submit() 调用、甚至其他 JS 触发的提交路径。真正覆盖所有入口的,只有 submit 事件。
常见错误是给按钮绑 click,然后手动调用 form.submit() —— 这会导致:校验通过后点一次,既走 click 逻辑又触发 submit 逻辑,请求发两次。
- 统一监听目标表单的
submit事件,哪怕按钮不在 form 内部 - 如果按钮在 form 外,用
form.requestSubmit()触发(比form.submit()更规范,会触发 submit 事件并尊重checkValidity()) - 禁用按钮逻辑必须放在
submit回调里,且要立即执行,不能等异步请求开始后再设disabled
禁用所有 type="submit" 元素,不只当前按钮
跨表单场景下,一个页面可能有多个表单,每个表单又有“保存”“提交并退出”“预览”等不同按钮。只禁用点击的那个 button,其他 type="submit" 按钮仍可触发重复提交。
例如用户点了“预览”按钮(已禁用),但马上又按回车,依然会触发表单默认提交 —— 因为回车激活的是整个表单的 submit 行为,不是某个按钮。
- 在
submit事件处理函数中,用form.querySelectorAll('button[type="submit"], input[type="submit"]')批量禁用 - 别依赖
this指向按钮,this在submit事件里是form元素 - 文字提示建议统一设为“提交中…”,避免用户误以为卡死而刷新页面
后端必须校验 submit_token,前端禁用只是障眼法
前端禁用按钮对绕过 JS 的请求完全无效。攻击者或自动化脚本可直接 POST 表单数据,或用 curl、Postman 重放请求。所以跨表单提交的防重底线,永远在服务端。
关键不是 token 存在哪,而是它怎么生成和消费:每次页面加载,后端必须下发新 token;提交时校验该 token 是否未使用、未过期、签名匹配;校验通过后立刻标记为“已消费”,不可复用。
- token 必须绑定 session + 表单唯一标识(如
form_id="order_create"),不能全局通用 - 前端只需把
塞进对应表单,提交时自动带上 - 若用
fetch提交而非原生 form,需手动取document.querySelector('[name="submit_token"]').value并传入 body
避免 fastclick 或移动端双触发导致的“禁用失效”
在 iOS Safari 或某些安卓 WebView 中,fastclick 库可能把一次物理点击解释为 touchstart + click 两个事件。如果禁用逻辑只写在 click 里,第二个事件进来时按钮还没禁用完,就又发了一次请求。
这个问题在跨表单场景更隐蔽:你监听的是 A 表单的 submit,但 B 表单的按钮被 fastclick 双触发,结果 B 的 submit 事件也进了同一个 handler,造成混淆。
- 禁用逻辑必须放在
submit事件内,且用let isSubmitting = false做内存锁,比仅靠 DOMdisabled状态更可靠 - 在
submit处理器开头就判断if (isSubmitting) { e.preventDefault(); return; } - 确保
finally块里重置isSubmitting = false和按钮状态,否则失败后按钮永久锁定
跨表单提交最难缠的点,往往不是逻辑写错,而是多个表单共享同一套 JS 防重代码时,isSubmitting 变量没做作用域隔离,或者 token 字段名写死成通用名(如 token),导致 A 表单提交后 B 表单的 token 校验意外通过。每个表单的防重状态和凭证,得真正独立。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











