ARTICLE DETAIL

资讯详情

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

HTML5表单属性实战:从required到pattern的原生校验指南

HTML5表单属性实战:从required到pattern的原生校验指南 上个月我帮朋友公司做一个内部活动报名页需求听起来很简单姓名、邮箱、手机号必填邮箱格式要校验手机号要限制11位提交前确认协议勾选。我一开始按老思路写了一套jQuery校验blur的时候判断、submit的时候全部判断、出错了给每个input append一个红色的span。写完大概200行JS结果第一天就出了问题——某个浏览器上正则写岔了一个用户填了“123abc”也过了还有一次校验提示的容器放错了位置把布局顶烂了。改到第四版的时候我干脆全部删掉换成HTML5原生表单属性代码量直接缩到原来的十分之一。这篇内容没别的意思就是想把这次重写过程中沉淀下来的东西整理清楚。HTML5给表单带来的不只是几个新标签而是一整套“属性化”的表单能力required控制必填、pattern控制格式、min/max控制范围、list关联候选值、multiple支持多选更不用说typeemail、typedate这些新输入类型。哪怕你是拿来做html5网页设计作业把这些属性组合起来提交体验也会比手写一堆监听事件稳得多。下面按我的实际使用经验从设计思路到具体写法再到踩坑记录一次讲透。1. 从需求场景说起为什么我开始认真用HTML5表单属性1.1 一个改了四版的报名表单最初那版报名表单的代码现在回想起来有点可笑每个输入框都绑了blur事件和change事件提交按钮又单独绑click每个字段的错误提示都是一个动态插入的span还得给它们统一加CSS类。邮箱格式的正则我从网上抄了三份每一份行为还不太一样手机号校验、协议勾选、日期范围全部手动判断。表面上看“控制力很强”实际上项目一跑起来全是破绽报错提示的位置在窄屏上乱跳、某些输入法输入中文时blur触发时机怪异、回车提交和按钮click提交会走两条不同的校验路径。第四个版本我决定把校验层全部换成HTML5原生能力只保留一条AJAX提交逻辑页面瞬间清爽了。我现在面对类似需求时写表单的第一选择永远是HTML5表单属性。required声明必填pattern声明格式min/max声明范围typeemail/url/number交给浏览器做类型检查。这套东西不是新概念但很多同学还在用老一套的JS校验一方面是因为习惯另一方面是不太清楚这些属性在不同场景下的边界和坑。这篇文章就把它们的使用边界、组合方式、踩坑记录都摊开讲。1.2 我理解的HTML5表单属性设计思路很多人把“表单属性”单纯理解成“少写几行JS”其实它的核心思路是把高频、通用、跟业务无关的表单行为从脚本层下沉到浏览器层。你想一下必填校验、邮箱格式、数字范围这种东西在每一个项目里都是同一套逻辑反复用JS实现没有意义。浏览器把它做成原生能力之后开发者只需要在标签上“声明”规则浏览器负责执行。这跟你装修房子把电线预埋在墙里一样施工阶段麻烦一点后面每个开关都直接可用。这套设计还有一个关键原则叫渐进增强老浏览器不认识pattern、required没关系它只是不校验页面照样能用现代浏览器则会自动拦截错误提交。所以我的建议也是我实际项目的做法业务规则校验、异步校验继续用JS但能把“这一项不能为空”“这个邮箱格式不对”这种通用判断交给原生属性的就不要自己写。前端校验从来不是安全防线后端校验永远都要有但原生校验能省掉大量模板代码这是实打实的收益。2. 常用表单属性逐个拆解写法、参数与隐藏细节2.1 required、placeholder、autofocus最常用但细节最多这三个属性可能是大家最早认识的HTML5表单属性但它们各自的细节不少。required是最典型的“声明式校验”。它直接让浏览器在提交时拦截空值。有几个情况值得注意第一required只对用户可编辑的控件有意义对typehidden没有校验效果因为隐藏域的值本来就是程序填的浏览器不认为用户有义务去填它。第二对于同一name的一组radio或一组checkbox只要其中一个加上了required整组就共享“必须勾选一个”的规则不需要每个radio都写但实际项目里多数人会全写效果一样这是符合浏览器行为的。第三单个的“同意协议”checkbox要勾选直接给它加required就行这也是最常见的做法。再说placeholder。它的作用是给输入框一个示例性提示比如“请输入邮箱”但很多新手把它当label用这是不对的。屏幕阅读器不会稳定地把placeholder读给用户听而且一旦用户开始输入提示文字就消失了用户容易忘记这里原来要求什么。我习惯的写法是label正常放外面placeholder只放格式示例比如“nameexample.com”这样两者职责不冲突。autofocus这个属性我建议谨慎使用。它确实能让页面加载后自动聚焦到某个输入框省一次点击但一个表单里只能有一个生效放多了浏览器行为会不一致更麻烦的是在移动端autofocus会让虚拟键盘自动弹起来可能把首屏内容完全顶出可视区域。我的经验是PC后台系统里可以用移动端页面尽量别用如果表单不在首屏位置autofocus还会让页面自动滚动到该输入框用户一进来就跳走观感很差。2.2 pattern、min、max、step把校验交给浏览器这一组才是HTML5表单属性里真正值钱的家伙。pattern属性接收一段正则表达式用来约束输入的格式。这里有一个网上流传很广的误区很多教程说“pattern默认是部分匹配所以要自行加^和$锚定”这个说法在大部分现代浏览器里不准确。HTML规范要求的是“整个值匹配该正则”而不是“值中包含匹配的子串”。举个例子pattern写[0-9]{6}用户输入123456abc浏览器会判定不匹配并拦截如果在JS里直接用RegExp.test(123456abc)判断返回的才是true因为JS的test是部分匹配。所以问题不在于HTML5需要锚点而在于你把同一个正则复制到JS里做历史校验时行为不一致。为了让前后端、跨团队复用同一正则时不踩坑我的团队规范是在pattern里也统一写成^[0-9]{6}$这样的完整形式至少不会因为环境不同产生两种结果。pattern还有一个特点它只对“非空值”做匹配。也就是说一个字段如果没加required用户不填反而合法填了就必须符合pattern。这个特性用好了很灵活比如“选填但填了必须规范”的手机号、QQ号等字段。min/max/step这三个属性主要用于number、range以及date、time、month这一组时间类输入。step的细节比较多typenumber时step默认是1但如果你设了min0.5step不写的话合法的值按步长1计算输入0.5就不合法得显式写step0.5。处理金额时我一般写min0 step0.01配合typenumber但要注意浮点数的精度陷阱比如step0.1用户输入0.15按理论应该是“10的倍数”其实0.15除以0.1等于1.5不是整数倍浏览器会报错这个没问题可有些浏览器在处理0.30000000000000004这类浮点时可能出现误判所以涉及资金的字段我建议用“分”做单位、step1或者用textarea配合专用校验库不要完全依赖原生step。min/max还有一个容易犯的错如果min大于max那这个字段永远不可能合法提交永远被拦截。我见过一个同事把月份范围写成max1 min12结果线上表单三天收不到一条数据。另外时间类输入框的value格式是严格固定的date必须是yyyy-mm-ddmonth必须是yyyy-mmtime必须是hh:mm或者hh:mm:ss这跟页面显示格式是两回事很多新手被这个搞晕。2.3 listdatalist、multiple、form三个容易被遗漏的“外挂”这三个属性相对冷门但用对场景非常香。list属性可以给任意input挂一个datalist让输入框获得一组候选建议。写法很直接input上写listcityList页面里放一个idcityList的datalist里面是若干option。要注意它和select的本质区别datalist只是建议用户仍然可以输入列表里没有的内容如果你要求用户必须从候选里选那就用select。候选列表里option的value是填入输入框的值label是显示辅助尽量不要让value为空否则不同浏览器处理不一致。这个方案适合“城市选择”“常见问题类型”这类需要兼顾自由输入和快捷选择的场景。multiple属性在两种情况下用得多。一个是typeemail允许多个邮箱地址用英文逗号分隔浏览器会对每个邮箱单独做格式校验另一个是typefile允许一次选择多个文件。注意multiple在select标签上也有用于下拉多选但表达的语义不同别搞混。form属性是我个人很喜欢的一个“外挂”它允许一个输入框从物理位置上脱离form标签但仍然归属某个表单。弹窗里的搜索框、自定义布局里的筛选条件、跨表格的附加字段都能靠这个属性参与提交。具体写法是给form加id然后在任意位置的input上写form该id。这个属性对老兼容性差一些但现代浏览器基本都能用。再补充两个提交相关的控制属性formaction、formmethod可以覆盖form上默认的action和method比如一个表单里放“保存草稿”和“正式发布”两个按钮分别提交到不同接口novalidate加在form标签上会禁用整个表单的原生校验formnovalidate加在某个提交按钮上则只让这一个按钮跳过校验这两个在“先存草稿、不强制校验”的场景里特别有用。3. 新输入类型与属性的组合玩法3.1 email、url、number、range、date等新类型的实际表现HTML5给input增加了好几个语义化type它们自带校验逻辑和特殊键盘比单纯用text正则再套一层要自然得多。我按实际项目里的体会逐个说。typeemail会自动检查邮箱格式用户在PC端Chrome里输错会给出中文气泡提示在移动端会调起带键的邮箱键盘。有个细节如果字段不是必填用户留空是合法的如果填了就要满足格式。typeurl也一样它要求完整的URL比如https://example.com用户如果只填www.example.com会被判不合法。真实业务里“网址”字段经常希望放宽这种时候我只保留required不用typeurl改成textpattern自己定义规则。typenumber和typerange是两兄弟。number自带上下调节按钮min/max/step都有效range渲染出一个滑块默认范围是0到100不加min/max时value就在这个区间里。range本身不会显示当前值通常需要配一个span用oninput把值同步出来。注意range的value默认是50如果你设了min0 max10不设value滑块会停在中间偏左实际上默认value会取min和max的中点也就是5你在代码里最好显式写value避免歧义。很多同学忽略了range的step滑块默认按1跳动需要更细的粒度就改step0.1或0.01。时间类里面typedate是最常用的。它的value永远要写yyyy-mm-dd浏览器在不同平台上弹出的控件完全不一样PC Chrome是日历面板iOS上是滚轮部分安卓旧机型甚至没有原生控件。如果产品要求所有用户看到的日历长得一样那就不能指望原生要引入第三方日期组件。typetime、month、week、datetime-local都有类似的格式和兼容问题我的经验是能用原生就原生不能接受风格差异才上组件别一开始就给每个input套组件库。typetel和typesearch也值得提一下。tel不校验电话格式它最大的价值在于移动端会调起数字拨号键盘你要真正限制电话号码格式还得靠patternsearch在PC端某些浏览器会有清除按钮逻辑上跟text没区别但语义更明确。我整理了一张常用类型和属性的搭配表方便对照参考input类型有效属性自带行为注意点textpattern、maxlength、minlength、list基础文本输入常见字段默认类型search同上部分浏览器显示一键清除样式可定制性差emailmultiple、required自动邮箱格式校验多邮箱时用逗号分隔urlrequired自动URL格式校验要求完整协议前缀telpattern、required移动端调起数字键盘不校验格式需配合patternnumbermin、max、step上下按钮 数字键盘浮点精度需注意rangemin、max、step滑块展示默认0~100建议显式valuedatemin、max、required弹出日历控件value固定yyyy-mm-ddtimemin、max、step时间选择控件value固定hh:mmcolorrequired取色器value固定#rrggbbfileaccept、multiple文件选择accept只是建议过滤checkboxrequired勾选状态同name多个可共享至少选一radiorequired单选状态同name组共享至少选一3.2 CSS状态选择器与校验API让原生校验更“听话”原生校验的反馈气泡是浏览器自己画的样式没法改但输入框本身的状态可以通过CSS感知。:valid和:invalid这两个伪类就是干这个的。我最初写CSS时直接给:invalid加红边框结果页面加载出来所有必填空框全红了因为初始状态它就是invalid根本没等用户操作。后来才发现问题是“没有区分未触碰和填错”。比较新的浏览器支持:user-invalid它只在用户真正交互过之后才判定体验就自然很多对老浏览器我的兜底方案是用JS给表单加一个submitted类提交后再给.:invalid套上红框。CSS写法大概是input:user-invalid, textarea:user-invalid { border-color: #e03131; background: #fff5f5; }这个伪类写起来很爽但兼容性还不是百分之百上线前要在目标浏览器里过一遍。除了CSS还有几个原生校验API值得记一下。form.checkValidity()返回整个表单是否合法form.reportValidity()会逐个弹错误气泡input.setCustomValidity(自定义文案)可以覆盖默认的“请填写此字段”想清除时传空字符串就行。真正提交前想拦截做AJAX时我习惯在submit事件里判断checkValidity不合法的直接return。有一个点很容易踩form.submit()这个方法会绕过一切原生校验直接提交数据所以如果你需要在提交前由浏览器先校验一把要用form.requestSubmit()它是最近几年的标准API会先跑校验流程、触发submit事件再提交。想用JS模拟用户点击提交按钮时这个差别会直接决定表单能不能拦住脏数据。4. 一个完整可跑的“活动报名”表单示例4.1 完整HTML代码与基础样式代码看再多不如跑一遍。下面是我整理的一个“活动报名”表单基本把上面提到的属性都串起来了。你直接在本地建一个html文件复制进去浏览器打开就能体验原生校验的完整链路。form ideventForm action/api/event/signup methodpost h2活动报名/h2 p label forname姓名/label input typetext idname namename required maxlength20 placeholder你的真实姓名 autocompletename /p p label foremail邮箱/label input typeemail idemail nameemail required multiple placeholdernameexample.com autocompleteemail /p p label forphone手机号/label input typetel idphone namephone required pattern^1[3-9][0-9]{9}$ title请输入11位手机号 placeholder13800138000 autocompletetel /p p label forage年龄/label input typenumber idage nameage required min1 max120 step1 value18 /p p label forcity所在城市/label input typetext idcity namecity listcityList required placeholder输入或选择 datalist idcityList option value北京/option option value上海/option option value广州/option option value深圳/option option value杭州/option /datalist /p p span感兴趣的岗位/span labelinput typecheckbox nameinterest valuefrontend required前端/label labelinput typecheckbox nameinterest valuebackend required后端/label labelinput typecheckbox nameinterest valuedesign required设计/label /p p label fordate期望参与日期/label input typedate iddate namedate required min2025-01-01 max2025-12-31 /p p label forlevel熟悉程度/label input typerange idlevel namelevel min0 max10 step1 value5 output idlevelOutput forlevel5/output /p p label forremark备注/label textarea idremark nameremark maxlength200 placeholder选填200字以内/textarea /p p label input typecheckbox nameagreement valueyes required 我已阅读并同意活动规则 /label /p p button typesubmit提交报名/button button typesubmit formnovalidate保存草稿/button button typereset重置/button /p /form配套一个最基础的CSS只用来说明上面提到的状态样式input:user-invalid, textarea:user-invalid { border: 1px solid #e03131; outline: none; } input:user-valid, textarea:user-valid { border: 1px solid #2f9e44; } ::placeholder { color: #868e96; font-size: 14px; }4.2 代码逐段拆解每个属性为什么这么写这个表单里每一个属性都不是摆设我挑关键的过一遍。姓名、邮箱、手机号都是required这是基础防线。邮箱我故意加了multiple单个邮箱也能过但它允许用户填多个备用邮箱这只是个示例你们按业务决定。手机号用typetel完全是为了移动端键盘真正卡格式的是pattern^1[3-9][0-9]{9}$因为tel本身不校验内容必须靠pattern。title请输入11位手机号也别忘了浏览器弹原生校验气泡时会优先展示title里的描述。年龄用typenumber加min/max/step这个组合能保证数字在合理区间且为整数。默认value18避免用户不碰也能通过也给range那个字段做个对照。城市字段是listdatalist的典型用法既能手动输入也能从下拉候选里选。这里我要强调一下datalist不是必须项所以平时字段可填可不填但我在这个示例里给它加了required说明业务上要求用户必须填城市这跟“是否提供候选列表”是两个独立维度。“感兴趣的岗位”用了三个同名checkbox每一个都加required。按HTML5规范同名checkbox组只要有一个被勾选这组就算满足必填。这是原生能力不需要写额外JS。你实际测试时会发现一个都不勾选提交浏览器会拦下来勾其中任意一个校验立刻通过。但要注意这种“至少选一项”的语义只对同名组有效不同名就得自己处理。日期字段的value格式是固定的yyyy-mm-dd所以我在html里写min和max也用同样格式。用户从日期控件里选择的值本来就是标准格式不用额外转换。这里没做自定义校验因为原生min/max已经能拦住“超出活动期间”的日期。range和output是固定搭配。range本身不会显示当前值我放了一个output forlevel然后在里面默认写了5但还要一个监听事件同步滑块位置。你直接看HTML可能觉得这字段没值打开页面拖动滑块就明白了。如果想演示联动加一句内联JS就行input typerange idlevel namelevel min0 max10 step1 value5 oninputdocument.getElementById(levelOutput).value this.value最后两个提交按钮是关键。第一个“提交报名”正常触发原生校验第二个“保存草稿”我加了formnovalidate它会在点击时跳过所有校验把当前填写内容交到action地址。很多系统里“存草稿”和“发表”就是这么分的formnovalidate比手动注释required简单多了。5. 踩坑记录浏览器差异与移动端适配5.1 placeholder、autocomplete、date这些跨端“老大难”裸写HTML5表单属性踩坑是必然的我把几个高频问题放一起说。placeholder的样式是最典型的小问题。PC端Chrome默认是浅灰色Firefox、Safari也各不相同Edge更离谱有的版本直接不显示placeholder的背景色。想要统一样式必须写完整的伪元素选择器::-webkit-input-placeholder { color: #a0a0a0; } ::-moz-placeholder { color: #a0a0a0; opacity: 1; } :-ms-input-placeholder { color: #a0a0a0; } ::placeholder { color: #a0a0a0; }Firefox的placeholder默认自带opacity透明度不重置的话颜色看起来比预期浅。还有placeholder在聚焦输入后消失删除输入内容后又会重新出现这是默认行为是再正常不过的事不要试图用CSS去阻止。autocomplete是另一个重灾区。Chrome对autocompleteoff经常视而不见尤其是姓名、邮箱、手机号这种敏感字段它为了用户体验会强行弹自动填充。我以前试过加随机id、随机name效果都有限目前相对可靠的办法是普通字段用autocompleteoff配合name不叫email/name等关键词密码框用autocompletenew-password原本用于注册场景能阻止旧密码自动填充。另外如果你并不想禁用自动填充正确的做法是明确写语义化值autocompletename、autocompleteemail、autocompletetel这样浏览器能准确匹配上下文。总之一句话autocomplete控制的是“浏览器自动填什么”不是“填不填”想全禁是跟浏览器对着干。typedate在移动端的兼容性最抓狂。iOS Safari的日期选择器是滚轮安卓WebView里可能就是一个光秃秃的文本输入框更极端的是部分旧版iPhone上typedate根本不弹日期选择器用户只能手输字符串。所以我遇到移动端要求高度统一的场景原生date基本不敢直接用会换成第三方日期组件。当然内部管理系统、PC后台这种环境原生date完全够用。移动端虚拟键盘还牵扯一个问题是typenumber。在iOS上number类型键盘确实弹数字但很多安卓机型会把小数点藏在二级页面用户填金额时找半天。更好的做法是用typetext加inputmodedecimalinputmode是个专门控制键盘形态的属性decimal能调出带小数点的数字键盘。同样输入纯数字验证码时可以用inputmodenumeric。这个是渐进增强老浏览器不支持也不影响输入。5.2 隐藏字段required、pattern锚定、step精度等几个高发坑隐藏字段和required的组合是很多人第一次上生产环境才会发现的雷。我前面提过typehidden加required不会触发校验但如果你用CSS把某个typetext输入框display:none藏起来同时它又带着required原生校验依然会拦截提交提示一个用户根本看不见的字段“请填写此字段”。处理逻辑是这样的多步骤表单里暂时隐藏的字段等它可见时再校验才合理被隐藏只是因为布局需要的字段可以改成visibility:hidden或者用只读初始化。总之不要指望display:none能躲过required。pattern“整值匹配”的误会我前面已经拆过了这里再强调一遍实际后果你会看到“怎么我写了pattern它还能提交非法值”的反馈十有八九不是pattern失效而是JS里的判断逻辑和HTML5原生判断不一致或者字段空值被放过了。真要在JS里做同样校验要么保持正则一致、都带锚点要么直接调用input.checkValidity()别自己再写一套RegExp.test。step的浮点精度问题在金额字段里特别阴。比如min0 step0.1用户输入0.7能过输入0.15会被拦因为0.15不是0.1的整数倍。但如果你做分单位step0.01某些浏览器会出现浮点误差导致一个明明像“1.05”的值判定非法。我见过有人转头骂浏览器其实大多数情况是正则或step组合没设计好。我的建议是能用整数就用整数比如金额存“分”再展示成“元”原生校验只卡整数范围实在要小数务必在不同浏览器里把边界值都测一遍。再一个我经常帮人排查的问题是明明点了提交浏览器就是不弹校验气泡。先用node里打开DevTools看form标签有没有意外被加上novalidate再看按钮是不是真typesubmit。很多弹窗表单里按钮被习惯性写成typebutton然后自己绑click事件click里又调用form.submit()。form.submit()是不走原生校验的直接发送请求所以校验形同虚设。这种情况的正确姿势是button保持typesubmitclick事件里用requestSubmit()代替submit()。6. 常见问题排查速查表6.1 症状与原因对照表我整理了一份排查速查表是我平时给同事排障时最常用的思维路径直接对照使用症状常见原因处理办法写了pattern还是能提交HTML5对空值不校验或JS里正则与pattern写法不一致确认是否需要required统一正则加^$锚点required字段空白却能提交form可能意外存在novalidate属性或者提交走了form.submit()检查form标签属性改用requestSubmit()隐藏起来的输入框一直提示必填该字段用display:none隐藏且带required改成可见或换成typehidden交由程序填充autocompleteoff完全不生效Chrome对敏感字段强制自动填充用autocompletenew-password或随机name规避页面加载后必填空框全是红色使用了:invalid未区分初始状态改用:user-invalid或提交后再加invalid类移动端数字输入没有小数点typenumber的键盘缺小数键用textinputmodedecimal表单Value格式对但date控件不显示日期格式不符合yyyy-mm-dd统一用ISO日期格式用JS格式化后再赋值弹窗里的表单无法提交弹窗DOM在form外没有form关联用form属性指向表单id或把DOM挪进form多个提交按钮行为不同却总走同一个默认提交第一个按钮或没区分formaction需要不同行为时用formaction/formmethod分别指定6.2 我自己的表单排查习惯排查表单问题我固定走三步。第一步在浏览器里直接空提交一次看看原生气泡弹在哪、提示是什么这一步能定位绝大多数required、pattern、type组合问题。第二步打开DevTools检查form和button的上下文重点看有没有多余的novalidatebutton的type是不是submit事件里有没有调用submit()。第三步看样式层是不是CSS把字段隐藏了或者用:invalid提前把必填项标红造成了“一加载就报错”的观感。还有一个非常容易被忽略的问题form标签不能嵌套。你写了一个form里面又套了一个form浏览器在解析时会把内层form自动截断或者忽略导致字段归属混乱提交时数据缺一大块。这个问题在“弹窗里嵌表单”的场景很常见。正确做法是一个页面一个逻辑表单弹窗里要么复用外层form要么用form属性单独关联绝不嵌套。我在团队里还会要求所有人注意一件事原生校验只是前端体验的一部分后端校验永远不能省。前端校验为了让用户早点发现错误后端校验才是数据安全边界。原生属性甚至能帮后端省掉一些重复判断但它替代不了业务上的异步检查比如“手机号是否已报名”“邮箱是否重复”这些该写接口还是得写接口。最后分享一个小习惯我每次写新表单之前会先在脑子里过一遍哪些字段需要required、哪些格式能靠type解决、哪些格式必须pattern、字段之间有没有联动关系。联动关系比如省份和城市、职位和级别原生属性帮不上忙必须上JS纯粹的静态校验能买原生的绝不自己造轮子。这样拆完之后代码结构会非常清晰不需要再靠注释去解释每一行校验逻辑。表单属性这套东西吃透之后你会明显感觉写页面更快踩坑更少这也是我建议所有做前端的同学认真过一遍的原因。
返回列表