在填写注册页面时会出现红色提示文字一闪而过,即使重复输入十遍也不能通过。一般不是因为业务逻辑有问题,而是由于校验层没有做好:本应该用属性来限制的地方没有被限制住,本来可以用脚本来控制的地方却全部靠手写正则表达式来实现,并且提示信息也都是浏览器自带的英文。下面我们来分解一下HTML5表单验证的全过程,包括属性、脚本、默认气泡和自定义提示等等,并尽量给出可以直接复制使用的代码。

原生东西再去学习的话就可以节省掉一半的学习时间了
很多项目一开始就会用到校验库,但是浏览器已经做掉了大部分格式的判别工作了。因此可以先把所有的功能都用上:
当type是email、url、tel、number、date或是search时就会自动做简单的格式检查,并且会弹出手机对应的输入框
minlength和maxlength分别是最小值和最大值
min/max/step:指的是一个数值区间或者时间段,可以设置为0.01以保留两位小数
pattern:使用正则表达式来验证,适用性最广
multiple:与type=email搭配时可以接收多组数据,用逗号分隔
<input type="email" name="mail" required maxlength="64" autocomplete="email">
<input type="number" name="age" min="18" max="120" step="1">
两种容易被忽略的情况是:把 novalidate 放在 <form> 标签中会使得整个表单的所有默认验证都被禁用掉,在需要自己编写提示文字时很有用;给提交按钮添加一个名为 formnovalidate 的属性可以让这个按钮不做任何验证工作,比如“保存草稿”。
另外,minlength 在 type="number" 上不生效的原因是,在标准里明令禁止使用此属性给数字输入框添加限制,因此需要用 pattern 或者用 JavaScript 进行校验。
用正则表达式能够解决大部分格式问题
pattern 默认对整个字符串进行匹配,即包含 ^ 和 $ ,因此在写代码时不需要再加锚点。以下是常用的规则:
<!-- 手机号 -->
<input type="tel" pattern="1[3-9]\d{9}" inputmode="numeric">
<!-- 密码:8-20 位,含字母和数字 -->
<input type="password" pattern="(?=.*[A-Za-z])(?=.*\d).{8,20}">
<!-- 六位数字验证码 -->
<input type="text" pattern="\d{6}" inputmode="numeric" maxlength="6">
<!-- 邮政编码 -->
<input type="text" pattern="[1-9]\d{5}">
用inputmode来调起数字键盘要比用type="number"好得多,后者会出现上下的微调箭头,并且在某些浏览器里会因为滚轮而改变数值。
pattern 的缺点是无法自行编辑提示信息,默认情况下只会给出“请按指示填写”的字样,但是使用者却不知道什么地方出错了,因此这一步需要用脚本来实现。
把原来显示的提示信息换成通俗易懂的文字
每个表单项都有一个 validity 对象,该对象包含了很多个状态标志位,逐个查看就可以得到正确的提示信息:
const mail = document.querySelector('#mail');
const tips = {
valueMissing: '邮箱还没填',
typeMismatch: '邮箱格式不对,检查一下 @ 符号',
tooLong: '最多 64 个字符'
};
function check() {
const v = mail.validity;
for (const key in tips) {
if (v[key]) { mail.setCustomValidity(tips[key]); return; }
}
mail.setCustomValidity(''); // 关键:必须清空才算通过
}
mail.addEventListener('input', check);
setCustomValidity但常常会遇到这样一个让人头疼的BUG:当传入的参数为非空的字符串时,这个字段就一直处于无效的状态,尽管它已经被我们更改过了。因此在每一次的验证前都应将其与相应的已知的标准或已知的结果相对比一番setCustomValidity('')。
另外两种方法各自负责一项工作:checkValidity()只给出一个布尔类型的返回值,并且不显示任何提示信息,在静默模式下使用;而reportValidity()则会在出现第一个错误的时候弹出一个气泡以告知用户错误的位置,在提交前统一调用。
实现自定义规则的途径有三条
放在原始机制上,使用setCustomValidity把自定义结果放入浏览器校验系统里,好处是可以用原生方式实现提交拦截、气泡聚焦等功能,而且开销很小。
2、构建自己的错误状态层级,在对气泡进行定制时就需要建立一个错误映射表来保存这些信息,并且用checkValidity()来进行校验,在自定义元素中给出提示文字:
const rules = {
pwd2: (val) => val === pwd.value || '两次输入的密码不一致'
};
跨字段校验,例如密码、结束时间大于开始时间等等条件,在没有办法通过属性来进行表示的时候就只能采用这种方法了。
3、异步校验:用户名和优惠券的有效性都需要通过网络接口进行查询,在用户填写完数据后做防抖处理,在网络请求过程中给输入框加上loading效果,在提交按钮上再加一次reportValidity()操作,最后要等到异步结果返回之后再做一次同步校验以应对状态变化。
不要让屏幕上全是红色的
CSS伪类:invalid和:valid会在页面加载的时候就被触发了,在没有输入任何数据的情况下就会变成红色,看起来不太美观。解决办法有很多种:
:user-invalid 只有在用户执行了某种操作之后才生效,在最新的Chrome和Edge浏览器里都可以用到了
兼容方式是用监听blur事件给元素加上.touched样式,然后用.touched:invalid{border-color:#e54545}来改变它的边框颜色
无障碍方面增加两个属性:aria-invalid 表示当前元素是否存在问题,aria-describedby 指向提示文字的内容,这样读屏软件就能直接读出错误的信息了。对于错误文案而言,“密码要包含字母、数字,并且长度为8到20个字符”比“格式不对”更有说服力一些。
上线前需要注意的六个陷阱
type="number" 上的 minlength、maxlength 属性无效了,要使用其他的办法来控制它的长度
如果用户输入的内容有前后空格的话,在提交前要用trim()去掉这些空格,否则的话required就会认为是真的但是后端却会认为它为空
maxlength是根据字符数来计算长度,但是中文字符库是用字节来保存数据的,因此会出现被截断的现象,在前后端的数据传输过程中要保证二者长度相同
Safari默认的泡泡样式是无法自行更改的,如果要进行设计还原的话就必须全部接管过来
浏览器自动填充的校验时间不固定,重要信息要在填写完之后再进行一次全面的核对
mobile端type=date返回的结果总是yyyy-MM-dd,如果和后端格式不一致的话就需要做转换
一种可重复利用的层级结构
把静态格式转换成HTML属性、跨字段以及异步逻辑都交给JavaScript去处理,并且在服务器端保留一份同样的规则作为备份。一旦这三者被明确划分开来之后,HTML5表单验证的维护费用就可以大大降低很多了,增加新的字段只需要改动一行pattern就可以了。
校验的目的不是阻止用户访问网站,而是为了让用户在刚开始的时候就正确地填写信息。属性层可以拦截掉大部分低级错误,脚本层处理一些复杂的规则问题,文案层把“格式错误”变成具体的操作指南——如果这三个方面都做好的话,那么表单的转化率就会大幅提高。
