event.preventDefault() 必须是submit事件处理器第一行,否则验证失败会导致页面刷新;setCustomValidity()需配合checkValidity()和preventDefault()才生效;后端字段名须与input[name]严格一致才能映射错误提示。

submit 事件里漏了 event.preventDefault() 就别谈验证失败处理
表单验证失败后页面刷新、错误提示不显示、请求根本没发出去——八成是这一行漏了。它不是锦上添花,而是异步提交和手动接管验证的硬性前提。
常见错误包括:
- 把
event.preventDefault()写在验证逻辑后面,结果验证失败时根本没执行到 - 在按钮
onclick里发请求,却没监听form的submit事件 - 用内联
onsubmit="handle()"却没在函数末尾return false
正确姿势只有一条:event.preventDefault() 必须是事件处理器第一行,雷打不动。
setCustomValidity() 调用后没反应?检查这三件事
这个 API 看似简单,但实际生效依赖隐含状态:它只改当前元素的 validity 状态,不触发 UI 反馈,也不阻止提交——除非你在 submit 里显式调用 form.checkValidity() 并配合 preventDefault()。
立即学习“前端免费学习笔记(深入)”;
-
setCustomValidity("错误信息")才表示失败;setCustomValidity("")才代表“通过” - 调用后必须再触发一次验证(比如
input.blur()或form.submit()),否则:invalid伪类不会更新 - 多个字段校验时,别只检查第一个——要用
Array.from(form.elements).every(el => el.checkValidity())
后端返回 400 错误,怎么把字段级提示塞进对应 input
最常卡住的地方不是“怎么显示”,而是“映射错位”和“旧提示残留”。后端返回的字段名(如 {"user_email": ["邮箱已被注册"]})和前端 input[name] 不一致,JS 就找不到目标节点。
- 服务端字段名必须和
input[name]完全一致(区分大小写、下划线/驼峰) - 每个
input后紧跟一个<span class="error-message"></span>,不要用全局 toast 替代字段级定位 - 渲染新错误前,先清空所有旧提示:
form.querySelectorAll(".error-message").forEach(el => el.remove()) - 校验失败后,记得
input.focus(),否则用户可能不知道哪错了
移动端 Safari 里 checkValidity() 不触发?换时机
这个方法在桌面浏览器里通常没问题,但在 iOS Safari 上对触发时机敏感:等 submit 时才查,往往太晚——用户已经点了两次提交,UI 还没更新。
- 在
input或blur事件中主动调用input.checkValidity(),并同步更新aria-invalid和提示文案 - 给出错字段加
aria-invalid="true"和aria-describedby="xxx-error" - 错误提示 DOM 插入后,
input.focus()必须放在requestAnimationFrame里才可靠 - 用
aria-live="polite"包裹错误提示区域,否则读屏器可能跳过动态插入的内容
真正难的不是写几行校验逻辑,而是让每个字段的状态(CSS 伪类、ARIA 属性、DOM 提示、焦点位置、读屏反馈)在任意时间点都保持同步。尤其当跨字段校验(比如密码一致性)和后端错误叠加时,漏掉任何一个环节,用户就会卡在“知道错了,但找不到在哪”。



















