学完这篇,你能用原生 HTML 约束把大部分脏数据挡在提交之前,并且让错误提示从生硬的浏览器默认文案变成自己写的、用户看得懂的中文提示。
第一步:先用原生约束属性处理最常见问题
能不用 JS 就不用。下面这几项写在标签上,就能直接启用浏览器校验,并且这些约束属性会被 form.checkValidity() 和 CSS 的 :invalid 自动识别:
<form>
<input name="title" required minlength="4" maxlength="60">
<input name="email" type="email" required>
<input name="site" type="url">
<input name="age" type="number" min="18" max="99" step="1">
<input name="phone" type="tel" required pattern="1[3-9]\d{9}">
<textarea name="content" required minlength="10"></textarea>
<button>提交</button>
</form>
其中 type="email"、type="url"、type="number" 自带格式校验,比你自己写正则更可靠,因为浏览器对 URL 和数字的判定规则复杂,自己实现容易漏掉边界情况。required 用在 checkbox 上则表示“必须勾选”。
第二步:写好 pattern,并避开它的三个问题
pattern 的值是一段正则表达式,浏览器会自动把它当成“整个值必须匹配”,相当于替你加了 ^(?:...)$。所以:
<!-- 正确写法 -->
<input pattern="1[3-9]\d{9}">
<!-- 多余写法,虽然不报错,但容易让人误以为只匹配一部分 -->
<input pattern="^1[3-9]\d{9}$">
注意:pattern 只对文本类输入(text / search / tel / url / email / password)生效。type="number" 上写 pattern 会被直接忽略,数字范围请用 min/max;<textarea> 和 <select> 也不支持 pattern,只能用 minlength/maxlength 和 required。
注意:正则里不要用 /.../ 包裹,也不要加 g 标志,pattern 属性只接受纯正则字面内容。
第二个坑是提示文案。用户看到“请匹配要求的格式”这种提示会一头雾水,所以一定要配 title:
<input name="phone" required pattern="1[3-9]\d{9}"
title="请输入 11 位手机号,例如 13800138000">
Chrome 等浏览器会把 title 拼在默认提示气泡里,这算不写 JS 也能获得的最简自定义提示。
第三步:用 setCustomValidity 自定义验证消息
需要更明确的错误原因提示时,监听 invalid 事件设置消息:
const phone = document.querySelector('[name="phone"]');
phone.addEventListener('invalid', () => {
// 关键:先清空,否则上一次设置的消息会一直保留
phone.setCustomValidity('');
if (phone.validity.valueMissing) {
phone.setCustomValidity('手机号是必填项');
} else if (phone.validity.patternMismatch) {
phone.setCustomValidity('手机号应为 11 位,且以 1 开头');
}
});
// 用户一改动就清空,避免带着旧错误继续输入
phone.addEventListener('input', () => phone.setCustomValidity(''));
注意:setCustomValidity('任意非空字符串') 会让该字段永久判定为无效,即使格式已经正确,表单也提交不了。这最容易导致表单“怎么都提交不了”的问题,所以每次设置前先 setCustomValidity(''),并在 input 事件里再清一次。
同理,自定义 pattern 覆盖不到的跨字段规则时,也可以这么做,比如“两次密码必须一致”:
confirmPwd.addEventListener('input', () => {
confirmPwd.setCustomValidity(
confirmPwd.value === pwd.value ? '' : '两次输入的密码不一致'
);
});
第四步:接管提交时机
默认行为是点提交按钮时才校验并弹气泡。想自己汇总错误列表(比如在表单顶部显示红色提示条),就在 <form novalidate> 上关掉原生气泡,然后手动触发:
form.addEventListener('submit', (e) => {
if (!form.checkValidity()) { // 静默校验,不弹气泡
e.preventDefault();
form.reportValidity(); // 需要气泡时再显示
// 或者遍历 form.elements,读每个控件的 validity 自己渲染
return;
}
// 校验通过,正常提交
});
novalidate 只关闭自动弹出,约束本身仍然有效,checkValidity() 照常返回结果。
第五步:把状态反馈到样式上
input:invalid { border-color: #d33; }
注意::invalid 在页面刚加载、用户还没输入时就会命中(因为空值违反 required),满屏飘红很难看。改用 :user-invalid(已输入过或尝试提交过才命中)体验会好得多,旧浏览器可退回 input:not(:placeholder-shown):invalid 这类写法。
小结
- 能用
required、type、min/max、minlength 解决的就别写 JS;pattern 只用于格式校验。
pattern 隐式全串匹配,不用写 ^...$;在 type="number"、textarea、select 上不生效;务必配 title。
- 自定义消息走
invalid 事件 + setCustomValidity,每次设置前先清空,并在 input 里清空,否则表单会锁死。
- 自己控制提交流程用
novalidate + checkValidity() / reportValidity()。
- 样式提示优先用
:user-invalid,避免页面一加载就满屏报错。