ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HTML5原生表单校验:少写80%表单JS的实战指南

HTML5原生表单校验:少写80%表单JS的实战指南 如果你维护过几个中后台项目大概率见过这种注册页一个手机号 input 框光实时校验就写了二三十行 JavaScript——正则、错误文案、防抖、失焦提醒、输对了才解锁提交按钮。需求改一次表单逻辑代码就得跟着搬一次。我在项目里也经历过这个阶段直到某天认真读了读 HTML5 规范里那 22 个 input 类型才发现自己写给用户的表单处理逻辑浏览器早就替我写完了。这篇文章就聊聊怎么把这批原生能力捡回来哪些场景能做到“少写 80%”哪些地方仍然要自己兜底。适合正在被表单校验折磨的前端同学参考。标题里的“回归”指的是在框架和第三方校验库之外重新把 HTML 本身当成一等公民。这不是让你放弃前端框架而是先把浏览器自带的能力吃透再决定哪些代码值得自己写。1. 表单 JS 越写越多问题出在“没吃透原生能力”1.1 你以为的“自定义校验”其实是重复造轮子先说个最常见的例子邮箱校验。手写 JS 时很多人会这样处理const email document.getElementById(email); function validateEmail(value) { return /^[^\s][^\s]\.[^\s]$/.test(value); } email.addEventListener(input, () { const ok validateEmail(email.value); email.classList.toggle(invalid, !ok); // 还要手动更新错误文本 });但input typeemail本身就内置了邮箱格式校验浏览器会按照一个比较接近 RFC 5322 的规则去判断“这到底是不是一个合法的邮箱地址”还支持multiple属性一次解析多个邮箱。你把那些正则、事件监听和错误状态全部删掉换成一个类型声明就行。类似的需求还有很多手机号、URL、日期、数字范围、滑块、颜色选择、文件过滤、多选、必填、长度限制、格式限制……这些在过去往往需要你写大量的addEventListener、正则和状态同步但 HTML5 的表单体系里它们都有对应的原生字段属性和校验规则。换句话说你一直在重复造轮子轮子其实出厂就带。1.2 为什么很多人还是选择“全用 JS 接管”我给自己的项目做技术复盘时总结了一下原因。第一是历史阴影。早期浏览器对 HTML5 表单支持不一致一些老版本浏览器完全不认typeemail提交时不会拦截任何东西。那时候团队被迫用 JS 全量接管校验“不能依赖原生”就成了默认经验一代一代传下来。第二是框架受控组件的习惯。React、Vue 普及后表单值都在状态里顺手就在onChange里做校验加上错误对象、按钮禁用、loading 状态一套都写在组件里。框架给了很强的掌控感反而让原生 DOM 能力变得透明。第三是对原生能力边界不清楚。很多人以为原生表单只能弹一个“请填写此字段”的气泡处理不了业务规则。实际上setCustomValidity、:user-invalid、validity对象这些能力叠加起来比大多数手写方案更细、更可控。回归原生不是否定 JS而是把校验工作“声明化”。你在 HTML 里声明规则浏览器的约束验证系统去执行规则就在 DOM 上可读、可维护还天然支持动态新增的表单控件。2. 22 个 input 类型逐个盘点哪些能直接砍掉校验代码先纠正一个说法准确地说标准 HTML 里有 22 种input的type取值。下面按它们能帮你消灭哪种重复劳动来分组说明。2.1 文本与搜索输入text / search / email / url / tel / passwordtext没什么好讲的但这里有个被忽略的属性list配合datalist可以给普通文本框加联想补全不用自己维护一个 div 弹层。移动端还能配合inputmodenumeric、inputmodedecimal换数字键盘比用typenumber在某些场景下更稳。search同样是文本框但语义上表示“搜索”WebKit 和 Blink 内核的浏览器会自动渲染一个清除小叉点击后触发search事件。以前你写搜索框要监听input事件手动判断值是否为空再决定要不要显示清除按钮现在这个交互原生给你了。email自带格式校验和multiple多邮箱支持。要注意的是它只校验“像不像邮箱”不会校验域名是否存在也不会去查 MX 记录。现实里有个坑某些内部系统允许userlocalhost这样的值typeemail会直接判错这种情况就需要先用原生把关再在 JS 里做特例匹配。url必须是带协议、结构完整的绝对地址才算合法比如https://example.com。如果产品要求用户填写“不带 https 的网址”这个类型会适得其反你得自己加 pattern 放宽规则。移动端它还能唤起 URL 键盘对输入体验帮助很大。tel不校验具体格式这是有意为之因为全球电话号码格式太多样。它的价值是配合pattern做业务规则校验同时通过inputmodetel在手机上弹出电话拨号键盘。比如中国大陆手机号就是pattern1[3-9][0-9]{9}一个属性解决。password不提供强度校验但有两个容易被忽视的能力autocompletenew-password可以阻止浏览器自动填充旧密码配合密码管理器的行为更可控现代浏览器对typepassword也普遍内置了显示/隐藏明文切换按钮省了你手动实现 toggle 的逻辑。如果你还自己写“小眼睛”来切换密码可见状态可以先去掉除非你连浏览器默认的样式都要覆盖。2.2 数值与时间输入number / range / date / month / week / time / datetime-localnumber自带上下微调按钮支持min、max、step三个属性完成范围校验。一个常见的坑是step默认值为 1用户输入 1.5 会触发stepMismatch报错所以需要小数时要么写step0.01要么改用text加inputmodedecimal。另外如果你只想要数字键盘而不想要微调按钮和口味奇特的滚动行为number反而会带来困扰这时候直接textinputmodenumeric更合适。range滑块是原生自带的min、max、step直接决定拖动范围不需要自己监听 mousedown、mousemove、touchmove 去算坐标也不用自己做节流。页面里想要滑块实时显示当前值用一个output元素接收变化即可代码量少得可怜。date在移动端会唤起系统日期选择器体验非常接近原生 App桌面端各浏览器也已经有成熟表现。以前做日期选择要么引日历组件要么自己做弹层现在很多场景下原生控件就够了。别忘了配合min和max做范围约束比如“出生日期不能晚于今天”。month选择年月week选择周time选择时刻这几个都是“专职”输入类型适合相对冷门但确实存在的需求比如排班选时间、信用卡有效期选年月。它们的桌面端支持情况参差不齐尤其是 Safari 一直比较慢使用前要做特性检测或者只把它当作移动端增强方案。datetime-local用于同时选择本地日期和时间比如预约上门时间。它的值格式是2025-01-01T10:30不需要你拼接字符串。需要注意这个类型不能很好处理“开始时间要早于结束时间”这种跨字段比较这属于原生约束验证不擅长的领域后面专门讨论。2.3 勾选与取色输入checkbox / radio / colorcheckbox除了checked还有一个很少人用的半选状态indeterminate。配合 CSS 伪类:indeterminate你可以很方便地实现“全选/半选/不选”的视觉切换。虽然全选逻辑仍然要 JS 维护但半选的样式和状态展示不用自己画了。radio同一name下的单选框天然互斥选 A 自动取消 B这就是一个隐藏的“状态管理系统”。很多人用按钮组模拟单选还得自己写点击高亮、互斥逻辑其实原生 radio label 就能做到。required对 radio 组也生效同一组里一个都没选提交时就会报valueMissing。color是原生取色器点击后弹一个系统级颜色面板返回#rrggbb格式的值。后台系统的主题色、标签色选择以前要引第三方取色组件现在直接一个input typecolor就解决了。缺点是它不支持透明通道也不允许弹出自定义样式的面板这类场景才需要自己造。2.4 文件、隐藏值与按钮file / hidden / submit / reset / button / imagefile用accept过滤文件类型用multiple支持多选用capture在移动端指定打开相机。文件选择交互、类型限制、多选这些都是原生能力你要处理的核心只是拿到FileList之后的上传逻辑。它的样式难改是出了名的常见做法是把原 input 隐藏再通过 label 的for属性触发选择器这个技巧稳定好用。hidden保存不需要用户看到的表单值用来提交一些服务端需要但界面不展示的数据。它也有form属性可以挂在表单外部属于很基础但容易忽略的细节。submit是默认的提交按钮类型点击后表单自动进入提交流程先跑约束验证、再触发submit事件。同一张表单里多个提交按钮时按下的那个按钮的name/value会随表单一起提交可以用来区分“保存草稿”和“正式提交”这类操作。reset一键把表单所有控件重置为初始值。注意是重置为 HTML 里的defaultValue而不是清空成空白。很多手写表单重置逻辑的人先遍历控件再手动置空其实一个原生 reset 按钮就能做到还省了维护初始值状态的麻烦。button本身没有任何默认行为但配合form表单id属性可以放在页面任意位置去触发表单提交这比把按钮硬塞进form结构里灵活得多。image是图片形式的提交按钮点击后会把按钮的name.x和name.y坐标一起提交适合做图片热区点击场景虽然现在用得少但偶尔遇到地图标注类的需求会非常顺手。3. 约束验证 API让浏览器替你执行一套“声明式审批”3.1 约束从哪来四条来源叠加成一个状态约束验证Constraint Validation是 HTML5 表单里最核心的机制它把校验规则分成四类来源类型约束比如email必须是邮箱格式url必须是合法 URLnumber必须是数字。必填约束required属性让空值非法。范围与步进约束min、max、step控制数字、日期、时长的合法区间。格式约束pattern用正则声明自定义格式。这些约束一起作用时浏览器会实时计算每个控件是否合法并把结果暴露在控件的validity对象上。你不需要维护一个formState来记录每个字段是否出错DOM 自己就是状态容器。3.2 validity 对象每个校验失败的精确原因每个表单控件都有一个validity对象它包含一组只读布尔属性。开发时最有用的就是靠它区分错误类型然后定制提示文案。属性含义触发示例valueMissing必填但空白required 空值typeMismatch类型格式错误typeemail 非邮箱字符串patternMismatch不匹配 patternpattern1[3-9][0-9]{9} 其他tooLong/tooShort长度超限maxlength/minlengthrangeUnderflow/rangeOverflow数值或日期低于/高于边界min/maxstepMismatch步进不合法step0.1 0.15badInput用户输入无法解析typenumber “abc”customError由开发者主动设置setCustomValidity(...)配合checkValidity()和reportValidity()两个方法你可以在需要的时机手动触发检查也可以让浏览器在提交时自动完成。reportValidity()还会把第一条错误信息显示成气泡相当于免费的错误提示 UI。const emailInput document.querySelector(#email); emailInput.addEventListener(blur, () { if (emailInput.validity.typeMismatch) { // 自己定制格式错误的提示 emailInput.setCustomValidity(邮箱格式好像不太对); } else { emailInput.setCustomValidity(); } });3.3 动态新增的 input同样自动进入校验体系这是原生方案很容易被忽略的优势约束验证是挂在控件属性上的跟事件监听器无关。你用innerHTML或框架的v-for动态渲染一大堆带required/pattern的 input浏览器会自动把它们纳入校验机制不需要你手动给新控件逐个注册校验函数。对比一下手写方案每次动态加一行表单你可能要重新绑定input、blur事件、同步错误文案、再检查提交按钮状态。原生方案里这些工作全部消失新增一个控件就是多几个属性的事。4. pattern 与 title把正则从 JS 挪进 HTML 配置4.1 pattern 的匹配逻辑和容易踩的坑pattern接受的是一段正则表达式。这里有一个必须理解的关键点浏览器是拿整个值去匹配正则的不是“包含关系”。比如patternabc用户输入“xxabcxx”也会被判为不合法因为整个字符串不能完整匹配成功。所以写 pattern 时你实际上已经有了隐式的^和$。另一个坑是pattern只在值非空时生效。配合required才能同时拦截“空值”和“格式错误”如果字段本来就是可选的只有用户填了内容才会检查格式这通常是符合预期的但很容易被误解成“pattern 失效了”。4.2 可直接抄的常用 pattern 清单以下这些是我在实际项目里用过、验证过的 pattern直接复制到pattern属性里就能用。注意正则里的反斜杠在 HTML 属性里不需要额外转义但在 JS 字符串里写同样的正则就得写成双反斜杠。业务场景pattern 写法说明中国大陆手机号1[3-9][0-9]{9}基本覆盖主流号段不含分机号用户名字母开头[a-zA-Z][a-zA-Z0-9_]{3,15}4-16 位字母开头的数字/下划线组合登录密码(?.*[A-Za-z])(?.*\d)[A-Za-z\d]{8,20}8-20 位同时包含字母和数字中文姓名[\u4e00-\u9fa5]{2,8}2-8 个汉字不考虑生僻字扩展区固定电话0\d{2,3}-?\d{7,8}支持区号连字符可选身份证号\d{17}[\dXx]18 位最后一位支持大小写 x银行卡号\d{16,19}常见主流卡号长度4.3 title 属性给校验失败提供一份“说明书”很多人不知道pattern对应的错误提示文本默认是“请与请求的格式保持一致”这对用户来说完全不可读。解决办法是给同一个 input 加上title属性。当patternMismatch发生时浏览器会把 title 内容作为错误提示显示在气泡里。input nameusername pattern[a-zA-Z][a-zA-Z0-9_]{3,15} title用户名需以字母开头可包含数字和下划线总长 4-16 位 required 这段代码就能实现“输入格式不对 → 气泡里提示具体规则”而整个前端逻辑里一个正则也没有出现。规则写在 HTML 里需求变的时候只改一行属性这不就是声明式编程的价值。5. 一个注册表单的实战对比从 180 行 JS 缩到 20 行5.1 最初的手写方案事件、正则、状态同步一个不能少假设要做一个注册表单字段包括用户名、邮箱、手机号、年龄、密码、确认密码、同意协议。手写方案常见会长这样这里只展示核心骨架完整版往往更长const form document.getElementById(registerForm); const username document.getElementById(username); const email document.getElementById(email); const mobile document.getElementById(mobile); const age document.getElementById(age); const password document.getElementById(password); const confirm document.getElementById(confirm); const submitBtn document.getElementById(submitBtn); const validators { username: () /^[a-zA-Z][a-zA-Z0-9_]{3,15}$/.test(username.value), email: () /^[^\s][^\s]\.[^\s]$/.test(email.value), mobile: () /^1[3-9]\d{9}$/.test(mobile.value), age: () !Number.isNaN(age.valueAsNumber) age.valueAsNumber 18 age.valueAsNumber 120, password: () password.value.length 8, confirm: () confirm.value.length 0 password.value confirm.value, }; function validateField(field) { const ok validators[field](); form.elements[field].classList.toggle(invalid, !ok); updateSubmitState(); } [username, email, mobile, age, password, confirm].forEach((field) { form.elements[field].addEventListener(input, () validateField(field)); form.elements[field].addEventListener(blur, () validateField(field)); }); function updateSubmitState() { const allOk Object.keys(validators).every((key) validators[key]()); submitBtn.disabled !allOk; } form.addEventListener(submit, (e) { let firstInvalid null; for (const key of Object.keys(validators)) { if (!validators[key]()) { e.preventDefault(); firstInvalid firstInvalid || form.elements[key]; } } if (firstInvalid) firstInvalid.focus(); });这还不算错误文案管理、防抖、自定义错误提示和提交 loading 状态。说它要 180 行一点都不夸张。而且字段一旦变化你要同步改正则、改事件监听、改提交校验牵一发动全身。5.2 原生方案属性声明规则JS 只做业务补充同样的表单用原生约束验证去做HTML 长这样form idregisterForm input nameusername required pattern[a-zA-Z][a-zA-Z0-9_]{3,15} title用户名需以字母开头可含数字和下划线总长4-16位 input typeemail nameemail required input typetel namemobile required pattern1[3-9][0-9]{9} inputmodenumeric maxlength11 input typenumber nameage required min18 max120 input typepassword namepassword required minlength8 autocompletenew-password input typepassword nameconfirm required autocompletenew-password labelinput typecheckbox nameagree required 同意服务协议/label button typesubmit注册/button /formJS 里剩下的工作只有两个确认密码的一致性、提交请求const form document.getElementById(registerForm); const password form.elements.password; const confirm form.elements.confirm; form.addEventListener(input, () { confirm.setCustomValidity( confirm.value password.value ! confirm.value ? 两次输入的密码不一致 : ); }); form.addEventListener(submit, (e) { e.preventDefault(); const fd new FormData(form); fd.delete(confirm); fetch(/api/register, { method: POST, body: fd }); });浏览器会先自动检查所有约束包括required、typeemail、pattern、min/max以及confirm上的customError。全部通过才会触发submit事件进入上面的fetch。校验部分几乎全部消失剩下的 20 行左右全是“业务规则”和“提交动作”。5.3 实战效果与改进空间这样改造之后我自己的体感是提交按钮的禁用状态不需要维护了每个字段的错误提示由浏览器统一输出字段错误时聚焦定位也不是我管的新增一个校验字段只要加一行属性不用动任何 JS。如果你要“用户输入有效后才显示错误”可以用:user-invalid这种 CSS 伪类来控制报错样式而不是自己在input事件里判断 touched 状态。input:user-invalid { border-color: #d33; }:user-invalid只有在用户真正交互过这个字段后才触发样式避免页面刚打开时所有必填项都红成一片。不过这个伪类在 Safari 上的支持还不完整生产环境使用前要确认你的目标浏览器列表。6. 原生救不了的部分跨字段、异步、强度与样式6.1 跨字段校验确认密码这类逻辑还是得自己写原生约束验证针对的是单个控件它没法表达“两个字段之间的关系”。确认密码、开始日期早于结束日期、新密码不能等于旧密码这些都是跨字段规则最稳妥的补法就是我上面写的那种监听相关字段的input事件用setCustomValidity临时注入一个自定义错误。这样做依旧会被纳入原生校验时机体系浏览器提交时同样会自动拦截。注意一点setCustomValidity()必须执行否则自定义错误会一直“粘”在控件上即使问题已经修复表单照样提交不了。这个坑我踩过不止一次。6.2 异步校验“用户名是否已存在”还是需要 fetch判断邮箱是否已注册、用户名是否被占用必须请求服务端。原生的pattern和required都做不了异步逻辑。做法是监听blur或防抖后的input发起fetch然后同样用setCustomValidity注入“该用户名已被占用”的错误。注意并发请求的顺序问题如果用户连续快速输入最后返回的请求未必是最后一次输入的结果建议用请求序号或者取消旧请求避免旧响应覆盖新状态。6.3 密码强度单个 pattern 表达不了一个渐进评分pattern适合表达“规则全有或全无”的校验但很多产品要求的是渐进式强度弱、中、强三档提示再配合进度条。这种需求原生没法直接给需要自己写评分函数在input事件里实时算分。我的做法是先用pattern兜底最低规则比如至少 8 位且包含字母和数字再用 JS 做增量强度评分和视觉提示两者配合而不是二选一。6.4 样式与兼容性原生气泡、取色器、日期控件绕不开的现实问题原生校验气泡在不同浏览器里的文案风格不一致也无法完全定制成和设计稿一模一样。如果团队对表单错误样式有严格要求可以走noValidate路线关闭原生自动校验只在提交时用脚本检查form.checkValidity()然后手动渲染错误提示。这样你仍然是在用原生约束系统判断“错没错”只是把展示层换成了自己的 UI。date、color、month这类控件的外观基本由浏览器决定在桌面端的风格统一性上确实不如自己实现的组件。如果产品对视觉细节特别在意老老实实用隐藏 input 自己画选择器原生控件当作无障碍和语法层面的基础。另外Safari 对week、month支持一直不算好使用前建议用特性检测遇到不支持的环境降级成普通文本输入。给团队留一个安全网原生校验永远只是前端体验的一部分服务端必须再次校验所有提交数据。前端负责让用户少提交错误数据服务端负责确保数据最终合法这两件事从来不能互相替代。我现在写表单的第一反应是先问自己一段需求能用哪些原生type和属性表达而不是打开一个第三方校验库。真正常用的校验规则HTML 早就替你想好了一大半剩下那 20% 的 JS聚焦在真正属于你业务的地方就好。
返回列表