HTML原生校验仅负责字段级格式与必填检查,业务完整性校验(如用户名唯一性)应通过责任链模式解耦:原生校验保留在input/blur时即时反馈,异步业务校验只在submit且checkValidity()通过后串行执行,各Handler专注单一职责、不操作DOM。

HTML原生校验 API(checkValidity()、reportValidity()、setCustomValidity())只负责字段级格式与必填检查,它和业务完整性校验天然不在同一层——强行把“用户名是否已被注册”塞进 input 的 pattern 或 setCustomValidity() 里,只会让逻辑缠绕、测试失效、灰度失控。
为什么不能在 submit 事件里堆砌所有校验逻辑
常见错误现象是:表单 submit 事件监听器中,先调 form.checkValidity(),再手动 fetch 校验用户名唯一性,再判断密码强度,再比对两次输入……这种写法表面可行,实则埋下三类隐患:
- 校验顺序硬编码,新增一个风控接口就得改主流程,违反开闭原则
- 异步校验(如用户名查重)和同步校验(如
required)混在同一回调里,错误反馈节奏不一致,用户感知混乱 - 无法按入口维度开关校验项:小程序端要跳过邮箱验证,App端要强制设备指纹,但代码全挤在同一个
submit处理函数里
用责任链模式接管校验流程,而非覆盖原生API
核心思路是:保留 form 的原生约束(required、type="email" 等),让它继续承担「即时反馈」职责;把真正需要服务端参与或跨字段依赖的业务逻辑,抽成独立的 CheckHandler 实例,由责任链统一调度。这样既不破坏浏览器默认行为,又解耦了业务规则。
-
CheckHandler接口必须返回boolean,false表示中断链(如未登录直接拒交),不抛异常 - 每个 Handler 只做一件事:比如
UsernameUniquenessCheckHandler只管调fetch("/api/user/check?username=xxx"),不处理 UI 提示 - UI 层统一监听
form.reportValidity()失败后,再触发责任链执行;链中任一 Handler 返回false,就调event.preventDefault()阻止提交
如何让原生验证与自定义链协同工作而不打架
最容易踩的坑是:用户还没输完,input 就因 pattern 不匹配被标红,同时责任链又在后台发起查重请求,造成双重干扰。关键在于分层触发时机:
立即学习“前端免费学习笔记(深入)”;
- 原生约束(
required、minlength、type)靠浏览器自动触发,响应在input、blur时 - 轻量级自定义同步校验(如密码强度正则)放在
input事件里,用setCustomValidity()+ 立即清空控制 - 重量级异步校验(如用户名唯一性、手机号实名核验)只在
submit事件中、且原生校验通过后才启动,避免无效请求 - 所有异步 Handler 必须带
AbortController,防止用户连点导致多个请求并发,最后只取最新一次响应
表单提交前的最终校验整合点设计
不要在 submit 里写 if-else 判断一堆 Promise,而是封装一个 validateForm(form) 函数,它内部按序执行两件事:
- 第一步:
if (!form.checkValidity()) return false;—— 借浏览器原生能力快速筛掉明显非法输入 - 第二步:按预设顺序执行激活的
CheckHandler数组,每个 Handler 的handle(context)返回Promise<boolean></boolean>,用for await...of串行等待,任一失败即退出 - context 对象必须包含
formData(new FormData(form))、abortSignal、source(标识 App/小程序等入口),供各 Handler 按需提取字段或决策是否跳过
真正难的不是写链,而是让每个 Handler 的边界足够干净:它不操作 DOM、不管理 loading 状态、不拼接错误文案——这些都交给统一的表单包装组件或拦截器处理。否则所谓“解耦”,只是把面条从一处拉到了另一处。



















