做了几年后台管理系统,基本每个项目都绕不开 layui。前两天帮同事查一个问题:页面上一个输入框明明写了lay-verify="required",点击提交却不提示,表单直接就把空字符串发出去了。我检查半天,发现是他把按钮上的lay-submit属性漏掉了,表单验证根本没被触发。这类细节看起来是小问题,实际是很多人写 layui 输入框验证规则时最容易卡住的地方。
layui 的表单验证体系说简单也简单,就是lay-verify挂规则、form.verify()定义规则、lay-submit触发校验;说复杂也复杂,内置规则边界、自定义规则顺序、动态渲染、异步校验、联动校验,每个环节都有坑。这篇文章我把验证链路的底层机制、内置规则的边界、自定义规则的写法,以及我实测踩过的动态表单和异步校验问题一次性说清楚。适合正在用 layui 做后台管理、中台系统、运营后台的朋友,也适合刚从前端框架转过来想快速摸清表单验证思路的人。
1. 先搞清楚验证的触发链路,不然规则写了也是白写
很多人在单个输入框上写了lay-verify="required",然后在浏览器里刷新页面、点按钮,发现没有反应。问题多半不在规则本身,而是表单验证的触发链路断了。layui 的验证不是一个“属性一挂就自动生效”的魔法,它依赖一套完整的事件链路:class="layui-form"开启表单组件,lay-verify声明规则,lay-submit按钮触发校验,form.on('submit(filter)')处理通过后的逻辑。任何一个环节断了,规则都白写。
1.1 一个能跑起来的表单长什么样
先看一个最基础的完整示例:
<form class="layui-form" action=""> <div class="layui-form-item"> <label class="layui-form-label">用户名</label> <div class="layui-input-block"> <input type="text" name="username" lay-verify="required" placeholder="请输入用户名" autocomplete="off" class="layui-input" /> </div> </div> <div class="layui-form-item"> <button class="layui-btn" lay-submit lay-filter="submitForm">提交</button> </div> </form> <script> layui.use(['form'], function () { var form = layui.form; form.on('submit(submitForm)', function (data) { console.log('校验通过,提交的数据:', data.field); return false; // 阻止页面跳转 }); }); </script>这里有几个缺一不可的要素:
class="layui-form"必须挂在<form>上,layui 靠它识别表单作用域。如果你把输入框放在<form>外面,或者放进了class="layui-form"这个类名以外的容器,验证规则就不会绑定。lay-verify="required"写在输入框上,它声明这个字段需要被校验。- 提交按钮必须带
lay-submit和lay-filter。lay-submit告诉 layui 这是一个提交型按钮,点击时先跑验证;lay-filter是事件过滤器,用于form.on('submit(...)')标识对应表单。 form.on('submit(submitForm)', ...)在验证通过之后才会触发。验证失败时 layui 会阻止事件,显示错误提示,不会进入这个回调。
1.2 触发链路上的三个关键点
第一点是表单容器。我见过不少同事把输入框放在一个普通的<div>里,然后在外部包了一个<form class="layui-form">,结构上是父子嵌套,layui 也能识别;但如果你用了多个<form>嵌套,或者表单内容是异步加载后插到class="layui-form"容器外部的,规则就失效。最简单的检查方法:打开浏览器开发者工具,看校验失败时的提示 DOM 是否出现在输入框附近。如果没有提示,先确认容器类名。
第二点是提交按钮。lay-submit这个属性很容易被忽略,尤其是页面里已经有用 jQuery 绑定的click事件时。按钮上没有lay-submit,layui 根本不会介入提交流程,你写的lay-verify自然也只是一个普通属性。顺带说一句,如果你在按钮上同时绑了click和lay-submit,要注意触发两次提交的问题,我一般只用 layui 自带的事件体系。
第三点是layui.use(['form'], ...)的初始化。layui 组件模块化,没有在use回调里加载form模块,lay-verify同样不生效。这个看起来基础,但在我排查的问题里至少占三成。
1.3 排查模板化问题:为什么我的规则没有生效
遇到“规则没生效”,我一般按照这个顺序检查,基本五分钟内能定位:
- 输入框是否在
class="layui-form"容器内。 - 提交按钮是否有
lay-submit,lay-filter是否与form.on('submit(过滤器)')一致。 layui.use是否加载了form模块。- 输入框的
name属性是否存在。data.field是以name为 key 的,没有name的字段提交后根本拿不到值,但这个不影响校验触发。 - 如果是动态插入的表单,插入后是否调用了
form.render()。 - 如果用了自定义规则,规则名是否与内置规则重名,或者
form.verify是否在页面加载时正常执行。
提示:排查的时候不要只盯规则本身,先用一个最简单的
lay-verify="required"试。如果简单规则能触发,说明链路没问题,问题出在自定义规则上;如果简单规则也不行,说明链路断了,优先查容器和按钮。
2. 内置验证规则逐个拆解:正则来源、使用边界、最容易翻车的地方
layui 内置了几条常用验证规则:required、phone、email、url、number、date、identity。这些规则覆盖了后台系统最常见的输入类型,但每一条都有自己的边界。理解这些边界,能帮你少写很多无用的自定义规则,也能避免在用户输入合法内容时误拦截。
2.1 内置规则速查表
| 规则 | 作用 | 典型判定逻辑 | 注意点 |
|---|---|---|---|
required | 必填项 | 去除首尾空格后非空 | 中间空格不影响;\u00a0这类特殊空格可能绕过 |
phone | 手机号 | 11 位数字且以 1 开头 | 只认大陆手机号,不认固话和带+86的号码 |
email | 邮箱 | 格式校验 | 正则较宽松,不校验域名是否存在 |
url | 网址 | 必须含http://或https:// | 输入www.example.com可能会失败 |
number | 数字 | 非负整数 | 不支持小数、负数、科学计数法 |
date | 日期 | 可被日期解析的格式 | 优先按yyyy-MM-dd使用 |
identity | 身份证号 | 15 位或 18 位 | 最后一位可接受x或X,但只校验格式 |
2.2 几个内置规则的实际边界
required是使用频率最高的规则。它的判定逻辑是“删除首尾空格后不为空”,所以用户输入" "这种纯空格会被拦截,但输入"a b"这种中间带空格的字符串会通过。这在大多数场景下没问题,不过如果你要做敏感词过滤或格式校验,务必在required之后再挂一条规则。
phone规则默认只验证大陆手机号,类似/^1\d{10}$/。如果你的系统要兼容座机号、国际号码、带区号的号码,这条规则就不够用了。正确做法是自定义规则,或者干脆把phone放掉,只做required加前端宽松判断。做海外业务的后台尤其要注意,用户填一个美国手机号可能带+1,这时phone规则会直接提示格式错误,体验很糟糕。
email是宽松的格式校验,它只负责检查字符串长得像不像邮箱,不会去验证域名是否存在、能不能收信。对于大多数后台系统,这就够了。真要验证邮箱可达性,需要后端发验证邮件,前端做不了也不该做。
number这条规则有迷惑性。很多人以为它接受任意数字,包括小数和负数,实际上它默认只接受非负整数。用户在金额输入框里填12.50,会直接提示“只能填写数字”。金额、价格、评分这类有小数或负数的字段,需要自己写规则:
form.verify({ money: function (value) { if (value === '') return; if (!/^\d+(\.\d{1,2})?$/.test(value)) { return '金额最多保留两位小数'; } } });date规则的边界比较隐蔽。它支持的日期格式接近yyyy-MM-dd,比如2025-05-15能通过。但如果你用 laydate 组件选择了带时分秒的2025-05-15 18:00:00,再挂lay-verify="date",就会提示日期格式不正确。我建议:用了 laydate 带时间的选择器,输入框校验要么只挂required,要么自定义一个datetime规则,不要直接套date。
identity规则只做 15 位和 18 位的长度与字符校验,最后一位允许x或X。它不会校验你生日和校验位是否正确,也就是说11010519491231002X这种格式能过,但身份证号码本身不一定合法。后台录入访客身份证信息时要有这个预期。
2.3 提示模式与组合使用
lay-vertype可以控制校验失败后的提示方式:
tips:默认模式,在输入框旁边浮出气泡。alert:弹窗提示,比较重,会打断操作。msg:顶部的消息通知,轻量且不遮挡输入框。
用法很简单:
<input type="text" name="phone" lay-verify="phone" lay-vertype="msg" class="layui-input" />提示模式的选择也需要结合实际场景。tips的缺点是当输入框在弹层里或者页面有滚动时,气泡的位置偶尔会错位;alert最显眼,但连续填写多个字段时容易被弹窗打断;msg比较温和,适合后台表单这种高频填写的场景。
多条规则可以组合使用,用竖线|分隔:
<input type="text" name="email" lay-verify="required|email" placeholder="请输入邮箱" class="layui-input" />规则按顺序执行:先required后email,空值时会提示“必填项不能为空”,填了非空字符串再判断邮箱格式。这个顺序很重要,如果把email放前面,空值也会被格式规则拦截,用户会看到“邮箱格式不正确”而不是“必填”,语义就不对了。
3. 自定义验证规则才是真正的“业务规则”:两种写法与执行顺序
内置规则解决的是通用格式问题,真正的业务校验一定要用form.verify。比如用户名不能包含特殊字符、金额必须大于零、两次密码要一致、日期范围不能倒挂,这些都需要自定义规则。没必要为了一个简单判断在提交回调里写一堆if,layui 的校验层可以做得更干净。
3.1 form.verify 的写法与执行顺序
form.verify的用法是传入一个对象,key 是规则名,value 是一个函数。函数接收当前输入框的值作为参数,如果校验通过就返回undefined或空,如果校验失败就返回一个字符串,这个字符串会作为错误提示显示给用户。
layui.use(['form'], function () { var form = layui.form; form.verify({ username: function (value) { if ($.trim(value) === '') return; if (!/^[a-zA-Z_][a-zA-Z0-9_]{3,15}$/.test(value)) { return '用户名为4-16位字母、数字、下划线,且不能以数字开头'; } }, money: function (value) { if (value === '') return; if (!/^\d+(\.\d{1,2})?$/.test(value)) { return '金额最多保留两位小数'; } } }); });我在这里做了一个细节处理:先把空值直接return,让required规则去处理空值场景。如果你的规则挂在lay-verify="username"上,没有required,那么空值也会进入username函数,此时直接放行是合理的,因为“非必填但填了就必须合规”是常见业务。
规则执行的顺序由lay-verify中的排列决定,不是由form.verify里定义的先后决定。lay-verify="required|username"会先执行内置的required,再执行自定义的username。这个顺序的价值在于:必填校验永远优先,避免空值被格式规则拦截后给出误导性的错误信息。
提示:规则函数里
return;表示通过,return '错误信息'表示失败。千万不要在通过时return '',会因为字符串非空而被判定为错误信息。
form.verify的回调还会接收第二个参数,即当前输入框的 DOM 元素。通过这个参数可以拿到当前字段的上下文,做一些复杂判断。不过更常见的做法是直接用 jQuery 选择器取同表单其他字段的值,代码更直观。
3.2 两个常用自定义规则:用户名、金额
拿用户名为例,这几乎是我在每个后台系统里都会写的一条规则。需求通常是:只能包含字母、数字、下划线,不能以数字开头,长度 4-16 位。对应的正则和上面代码里一致:
/^[a-zA-Z_][a-zA-Z0-9_]{3,15}$/这里有个容易出错的地方:{3,15}是字符数,配合第一个字符[a-zA-Z_],实际总长度是 4-16 位。很多同事会在这里写错,导致长度校验不符合预期。建议写完正则后在浏览器 Console 里先用几个边界值测试:a(长度不足)、1abc(数字开头)、abc_def(合法)、a加 15 个字母(刚好 16 位合法)。
金额校验也是高频需求。后台录入佣金、折扣、价格时,需求通常是“大于等于 0,最多两位小数”,可以用:
/^\d+(\.\d{1,2})?$/如果要限制“必须大于 0”,再叠加一个判断:
if (parseFloat(value) <= 0) { return '金额必须大于0'; }注意顺序:先把格式判断做了,再判断大小。如果直接parseFloat('abc')拿到NaN,再比较就会产生诡异的结果。
3.3 自定义规则与动态场景结合时的注意事项
自定义规则定义在form.verify里,它是一个全局规则池。页面里任意表单的输入框都可以通过lay-verify="规则名"引用。规则名不要和内置规则重名,否则会覆盖内置规则,比如你定义了一个叫phone的规则,内置手机号校验就被替换了。
动态生成的表单,如果在插入 DOM 之后调用了form.render(),自定义规则同样生效,不需要重新定义。这是一个很优雅的设计:规则定义一次,全局复用。但要注意另一个问题,规则函数内部如果用 jQuery 选择器取同表单其他字段的值,在动态渲染场景下选择器可能匹配到多个元素。比如多个弹层同时存在,$('#pwd').val()可能拿错弹层的值。这种情况我建议在规则函数里通过第二个参数item向上查找最近的form容器,再在里面找目标字段,避免跨表单串值。
4. 动态表单、异步校验与联动校验:真实业务里最难的部分
静态页面的验证规则,官方文档和网上教程都讲得很清楚。真正让很多人头疼的是三件事:动态插入的表单为什么校验失灵?用户名查重这类异步校验怎么和form.verify配合?两个密码一致性这类联动校验该怎么写?这些我在实际项目里都踩过,下面展开讲。
4.1 动态插入表单后验证为什么经常失灵
后台系统的表单很多不是页面写死的,而是通过 AJAX 拉取模板再插入页面。常见的错误写法是这样:
$.get('/api/form-template', function (html) { $('#app').html(html); // 忘了调用 form.render() });HTML 插进去了,看起来输入框都在,样式也正常,但点击提交时校验不生效。原因在于 layui 的select、checkbox、radio等组件需要渲染成特定的 DOM 结构和事件绑定,动态插入的内容如果没有调用form.render(),layui 不认为这是一个已经初始化的表单。虽然lay-verify="required"这个属性在 DOM 上,但验证触发依赖的组件初始化没有完成。
正确写法是插入完成后补一次渲染:
$.get('/api/form-template', function (html) { $('#app').html(html); form.render(); // 重新渲染表单 });form.render()不传参数时渲染整个页面的表单元素;也可以传指定类型,比如form.render('select')只渲染下拉框。如果页面里有多个表单,只更新了某一个区域的 DOM,精确传参可以减少不必要的全局开销,但大多数后台页面表单数量有限,直接全量渲染问题不大。
4.2 异步校验(如用户名查重)的常见实现
form.verify中的规则函数是同步的,它执行完立刻返回结果。但“用户名是否被占用”这种校验必须等 AJAX 返回结果,于是很多新手会这样写:
form.verify({ username: function (value) { $.get('/check-username', { username: value }, function (res) { if (res.exists) { return '用户名已被占用'; } }); } });这样写是不会生效的。因为$.get是异步的,规则函数执行到$.get这一行就直接结束了,没有返回任何错误信息,AJAX 回调里的返回值不会传递给验证流程。这是一个典型的“看着逻辑没错,实际完全不工作”的坑。
正确做法是把“查重状态”先存到一个标记变量里,用输入框的blur事件发起异步校验,更新标记;form.verify中只读取这个标记:
var usernameTaken = false; $('#username').on('blur', function () { var value = $.trim(this.value); if (value === '') return; $.get('/check-username', { username: value }, function (res) { if (res.exists) { usernameTaken = true; layer.msg('用户名已被占用', { icon: 2 }); } else { usernameTaken = false; layer.msg('用户名可用', { icon: 1 }); } }); }); form.verify({ username: function (value) { if ($.trim(value) === '') return; if (usernameTaken) { return '用户名已被占用'; } // 其他格式校验 } });这个方法在大多数场景下可用,但有一个隐患:如果用户在 AJAX 请求返回之前点击提交按钮,usernameTaken还是旧值,验证可能误判。我在实际项目里做了个兜底:点击提交时如果查重请求还没返回,就提示“正在校验用户名,请稍候”,并阻止提交。简单来说,就是给“是否正在异步校验”再加一个标记:
var checkingName = false; $('#username').on('blur', function () { checkingName = true; var value = $.trim(this.value); if (value === '') return; $.get('/check-username', { username: value }, function (res) { usernameTaken = res.exists; checkingName = false; }); }); form.on('submit(submitForm)', function (data) { if (checkingName) { layer.msg('正在校验用户名,请稍候'); return false; } // 继续后续逻辑 });4.3 联动校验与特殊组件
联动校验最典型的是两次密码一致性。规则函数通过 jQuery 取另一个输入框的值来对比:
form.verify({ confirmPwd: function (value) { var pwd = $('#pwd').val(); if (value !== pwd) { return '两次输入的密码不一致'; } } });对应的 HTML:
<input type="password" name="pwd" id="pwd" lay-verify="required|pwd" class="layui-input" /> <input type="password" name="confirmPwd" lay-verify="required|confirmPwd" placeholder="确认密码" class="layui-input" />这里有一个容易被忽略的点:如果两个输入框在同一个表单里,并且使用了name="pwd"和name="confirmPwd",当用户先填写“确认密码”,再修改“密码”时,联动校验不会自动重新触发。也就是说,确认密码输入框在失焦的时候校验一次,之后你再改密码,确认密码的错误提示不会实时更新。需要手动监听密码输入框的变化,重新触发确认密码的校验。
特殊组件方面,select和radio也能用lay-verify="required"做必选校验:
<select name="type" lay-verify="required"> <option value="">请选择类型</option> <option value="1">类型一</option> <option value="2">类型二</option> </select>select的required校验逻辑是判断选中值是否为空字符串。所以必须给默认的“请选择”项设置空的value,否则第一项有值,校验永远通过。
laydate 和date规则的冲突在第 2 章提过。这里补充一个更稳妥的方案:时间控件输入框只挂required,格式校验在提交回调里用Date.parse判断一次。这样既不会误拦截带时分秒的合法输入,也能防止用户在手动输入时填写乱码。
4.4 特殊场景:隐藏字段与弹层表单
后台系统经常把表单项放在弹层里。此时要注意,如果弹层初始状态是display:none,tips模式的错误提示位置可能错乱,因为 layui 计算气泡位置时会参考输入框的坐标,而隐藏元素没有有效坐标。这种场景我一般把lay-vertype换成alert或msg,能规避大多数定位问题。
还有一种情况是表单里有隐藏字段参与校验。比如用户协议勾选使用了隐藏的checkbox,把lay-verify="required"放在隐藏元素上,校验失败后提示气泡会出现在不可见区域,用户完全看不到。处理办法有两种:一是把校验规则挂到可见的容器元素上;二是在提交回调里单独判断隐藏字段的合法性,用layer.msg提示。我更推荐第二种,因为隐藏字段本质上不适合用默认的字段级校验流程。
提示:layui 的
lay-ignore属性可以让字段跳过所有验证规则。它常用于“这个输入框只是展示数值,不参与提交”的场景。注意lay-ignore会让你的自定义规则完全不执行,不要指望用它“部分跳过”。
5. 高频问题对照表与调试习惯
这一章我把实际排查中遇到的高频问题整理成表格,方便你快速对照。发生问题的时候,先看现象,再看可能原因,不用从头开始读文档。
5.1 高频问题对照表
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 点击提交没有任何校验提示,请求直接发出 | 按钮缺少lay-submit属性 | 给按钮加上lay-submit和lay-filter |
| 动态插入的表单不校验 | 插入后没有重新渲染 | 调用form.render() |
| 输入框填了手机号但提示格式错误 | phone规则不认带区号或国际号码 | 自定义手机号规则 |
数字输入框填了12.50提示格式错误 | 内置number规则只接受非负整数 | 自定义数字规则,允许小数 |
| 日期选择带时间后被拦截 | date规则不认带时分秒格式 | 时间字段只挂required,或自定义日期时间规则 |
| 提交回调里拿不到某个字段的值 | 输入框缺少name属性 | 给输入框加上name |
| 校验失败的提示出现在奇怪的位置 | 输入框在隐藏容器或滚动区域,tips定位错乱 | 改用lay-vertype="msg"或alert |
| 所有字段都空着却提示邮箱格式错误 | 规则顺序写反了,格式规则跑到了required前面 | 调整lay-verify顺序,required放最前面 |
5.2 保留一个调试入口,比反复刷新省时间
我调试 layui 表单时,习惯在form.on('submit(...)')回调里先把data.field打出来:
form.on('submit(submitForm)', function (data) { console.log('校验通过,提交的数据:', data.field); console.log('当前触发表单的元素:', data.elem); // 后续业务逻辑 return false; });data.field是表单所有name字段的集合,能直观看到哪些值被正确收集、哪些值是空、哪些字段没有name。很多时候校验看起来“不生效”,其实不是验证问题,而是字段值压根没在预期位置。这个调试习惯帮我省了大量时间。等你确认data.field结构正确,再写具体的提交逻辑。
5.3 性能与安全:前端验证只是第一道门
这句话我用过很多次,但值得再说一遍:layui 的验证规则只是前端体验,不是安全边界。任何前端校验都可以被绕过,用户完全可以关掉 JavaScript 或者直接构造 HTTP 请求。所以涉及资金、权限、用户数据的接口,后端必须做参数校验和权限校验。
这不意味着前端验证不重要。恰恰相反,前端验证决定了用户体验的顺畅度。合理的做法是:前端负责格式、必填、联动这些即时反馈,后端负责业务规则、数据完整性、安全边界。前后端校验规则要保持一致,否则就会出现“前端能过、后端报错”的割裂体验。我在项目里通常会和后端同学约定一份公共规则文档,字段名、格式、错误提示都统一对齐,避免两边各写一套。
6. 把验证规则工程化的一些建议
写到最后,分享几个我在项目里沉淀下来的习惯。这些不是 layui 官方文档的必需内容,但长期维护下来收益很大。
6.1 把规则收口到一个公共文件
不要把form.verify的定义散落在每个页面的layui.use里。规则多了以后,页面之间互相复制粘贴,改一处漏一处的现象非常常见。我通常单独建一个formRules.js,统一管理所有自定义规则:
// formRules.js layui.define(['form'], function (exports) { var form = layui.form; form.verify({ username: function (value) { if ($.trim(value) === '') return; if (!/^[a-zA-Z_][a-zA-Z0-9_]{3,15}$/.test(value)) { return '用户名为4-16位字母、数字、下划线,且不能以数字开头'; } }, money: function (value) { if (value === '') return; if (!/^\d+(\.\d{1,2})?$/.test(value)) { return '金额最多保留两位小数'; } }, confirmPwd: function (value) { var pwd = $('#pwd').val(); if (value !== pwd) { return '两次输入的密码不一致'; } } }); exports('formRules'); });页面里加载这个模块:
layui.use(['form', 'formRules'], function () { var form = layui.form; // 直接使用 form.verify 中定义的规则 });这个做法的好处是:新增字段时不用翻历史代码,去公共文件里加一条规则,模板上挂lay-verify就能生效;修改规则时也只需要改一处,全局生效。
6.2 为每个输入框想清楚“业务边界”
我在写表单验证时有一个习惯:先不急着写正则,而是把字段的业务规则列成清单。比如金额字段,我会问自己几个问题:必填还是选填?能不能为 0?能不能为负数?最多几位小数?要不要限制最大值?字符串前后空格要不要处理?这些问题想清楚了,规则函数基本就是十几行代码的事。
想不清楚就动手写正则,往往会出现边界情况漏判。比如“金额最多两位小数”和“必须大于 0”是两条独立约束,如果合并到一个正则里,正则就会变得很复杂,而且提示语也没法分别给出。拆开写,每个规则返回对应的错误信息,用户看到的反馈也会更准确。
6.3 我实测下来的一些体会
最后聊点个人经验。layui 的表单验证体系算得上轻量好用,但也因为轻量,很多能力需要自己补。动态表单的form.render()是一个必须记住的要点;异步校验要绕过同步限制,用标记变量配合blur事件是主流方案;而lay-verify的顺序、name属性、lay-submit这三个细节,能避免一半以上的低级问题。
从我踩过的坑来看,出问题的往往不是规则本身,而是“这台机器跑起来需要哪些条件”没搞清。校验规则写得再漂亮,触发链路断了也白搭。所以如果你现在被某个 layui 输入框验证问题卡住,先把链路的五个检查点过一遍,再回头审规则。遇到拿不准的正则,直接在 Console 里验证边界值,比反复刷新页面猜答案高效得多。