☰
Element UI多层级对象表单校验:prop路径与规则配置全解析
2026/10/14 7:04:05 网站建设 项目流程

前端做表单,只要涉及到对象嵌套,校验基本都会栽跟头。Element UI 的表单校验(el-form+el-form-item)本身并不复杂,但一旦数据是form.user.address.phone这种多层级结构,很多人会把rules配了、prop也写了,结果页面毫无反应,或者直接冒出[object Object]这种令人摸不着头脑的提示。这篇文章就把多层级对象表单校验的底层逻辑、正确写法、动态场景的处理方式,以及我实际踩过的坑和排查思路完整捋一遍,希望能帮你少走弯路。

1. 先搞明白 rules 校验是怎么“找到”表单字段的

很多新手配置校验失败,根源不在规则写得不对,而是没理解 Element UI 的校验寻址机制。

1.1 prop 是校验的“寻址协议”

在 Element UI 中,表单组件(el-form、el-form-item、el-input等)之间的联动关系,核心就是el-form-item上的prop属性。这个prop就像是给当前表单项起的一个“字段名”,Element UI 内部拿到这个字段名之后,会去el-form上绑定的model对象里取对应的值。

这里有一个关键点:el-form-item上的prop不是随便写的,它必须能够直接在model对象上定位到数据。它内部做的事情大概可以理解成“按路径从对象里取值”。

举一个最简单的例子:

如果表单数据是这样:

form: { name: '' }

那么prop写成name就能取到form.name,校验规则中的name字段也会绑定到这个值上。

但如果是多层级:

form: { user: { name: '' } }

此时prop必须写成user.name,而不能只写name或者form.user.name。

只写name的问题在于:它在form的一级属性里找不到name,user是存在的,但name不存在,所以校验规则静默失效。写成form.user.name的问题在于:Element UI 默认不会从form这个根对象再往前追溯,它会认为form这个对象里有一个叫form的字段,里面的user.name才是目标值,同样取不到。

这个逻辑对很多新手来说有点反直觉,因为平时取值都是this.form.user.name,习惯了带根路径。

提示:prop的路径是相对于el-form上绑定的:model值而言的,从该对象的第一层属性开始写,不要以变量名为起点。

1.2 rules 的字段映射关系

rules通常长这样:

rules: { username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], 'user.phone': [{ required: true, message: '请输入手机号', trigger: 'blur' }] }

注意第二行的'user.phone',它表示校验规则作用于form.user.phone这个路径。这个字符串路径必须和el-form-item上的prop完全对应,一个字符都不能差。

Element UI 内部使用的是异步校验库 async-validator,它支持点路径的规则键名。也就是说,当你把rules和prop都写成user.phone时,校验器会按照这个路径去form对象上寻址取值,再进行规则判断。

清楚了这条线之后,多层级对象校验就变成一个“路径对齐”问题了:

  1. el-form的:model绑定了某个对象。
  2. 每个el-form-item的prop正确指向该对象下的路径。
  3. rules中遵循同样路径写规则。

三点对齐,校验就生效。

2. 多层级对象表单的校验配置实战

下面按不同的嵌套深度,给出完整的配置示例。这些都是我在实际代码里反复用过的模式,可以直接抄。

2.1 两层对象嵌套:字段本身是一个对象

最常见的场景就是“用户信息”整体作为一个对象,比如:

data() { return { form: { user: { name: '', age: '', phone: '' } }, rules: { 'user.name': [{ required: true, message: '姓名必填', trigger: 'blur' }], 'user.age': [{ required: true, message: '年龄必填', trigger: 'blur' }], 'user.phone': [{ required: true, message: '手机号必填', trigger: 'blur' }] } } }

模板部分:

<el-form :model="form" :rules="rules" ref="formRef" label-width="80px"> <el-form-item label="姓名" prop="user.name"> <el-input v-model="form.user.name"></el-input> </el-form-item> <el-form-item label="年龄" prop="user.age"> <el-input v-model="form.user.age"></el-input> </el-form-item> <el-form-item label="手机号" prop="user.phone"> <el-input v-model="form.user.phone"></el-input> </el-form-item> </el-form>

这里最容易犯的错误是el-form-item上的prop写成了name,而v-model绑的是form.user.name。此时页面输入框有值,但校验器去form.name取值,拿到的是undefined,required 规则直接报“请输入姓名”,或者干脆因为取不到值而不触发。

还有一点,trigger字段要按输入类型配置:

  • 输入类组件(el-input、el-select)一般用blur或change。el-input推荐blur,el-select推荐change。
  • 单选/复选类组件(el-radio-group、el-checkbox-group)必须用change,因为它们没有 blur 事件。

2.2 三层及以上嵌套:继续加路径前缀

深层嵌套并不特殊,本质上就是路径更长,比如:

form: { company: { department: { name: '' } } }

对应的prop写法就是company.department.name,rules键名也写成'company.department.name'。在模板中层层嵌套el-form-item或者在同一个el-form下直接写一行都是可以的,关键是保证路径完整。

有人会问,el-form-item到底需不需要跟着数据层级嵌套?比如:

<el-form-item label="部门" prop="company.department"> <el-form-item prop="company.department.name"> <el-input v-model="form.company.department.name"></el-input> </el-form-item> </el-form-item>

这种方式我试过,能跑,但没必要。如果内层el-form-item已经带prop并且v-model是直接深入到最终字段,直接写一层el-form-item就够了。外层el-form-item如果还绑定了prop,反而可能触发额外的路径校验干扰,特别是当外层el-form-item里的值是一个对象时,async-validator 可能提示[object Object]无法校验。

2.3 对象数组:列表形式的校验

多层级对象校验里最麻烦的其实是“对象数组”,也就是数据长这样:

form: { list: [ { name: '', age: '' }, { name: '', age: '' } ] }

这时prop的写法是带索引的路径:

<el-form-item v-for="(item, index) in form.list" :key="index" :prop="'list.' + index + '.name'" > <el-input v-model="item.name"></el-input> </el-form-item>

rules也一样:

rules: { 'list.0.name': [{ required: true, message: '姓名必填', trigger: 'blur' }], 'list.1.name': [{ required: true, message: '姓名必填', trigger: 'blur' }] }

但实际开发中列表长度是动态的,不可能写死 0、1、2 的规则。所以更推荐用:rules动态绑定当前项的独立规则对象:

<el-form-item v-for="(item, index) in form.list" :key="index" :prop="'list.' + index + '.name'" :rules="{ required: true, message: '姓名必填', trigger: 'blur' }" > <el-input v-model="item.name"></el-input> </el-form-item>

或者统一在data里定义一个“单行规则”,在列表渲染时用:rules="rowRules.name"传入。

有一点特别值得注意:v-for循环渲染时,如果使用了同一个prop,比如所有行的prop都写成list.name,那校验器只会校验第一行,或者校验结果互相覆盖。必须用index拼出唯一的prop,否则动态循环时校验全乱套。

3. 动态增删场景下,prop 必须跟着数据一起变

表单往往不是静态的,常见的有“动态增加一行”“根据条件禁用或必填”这类交互。多层级对象在动态场景下,坑比静态场景更多。

3.1 动态增删表单项的 prop 拼接

以动态添加一个联系人列表为例:

addRow() { this.form.list.push({ name: '', age: '' }) }, removeRow(index) { this.form.list.splice(index, 1) }

模板中:

<el-form-item v-for="(item, index) in form.list" :key="index" :prop="'list.' + index + '.name'" > <el-input v-model="item.name"></el-input> </el-form-item>

这里index在插入和删除中间位置时会变化。比如有三行,删掉第一行,原本的第二行变成了第一行,它的prop从list.1.name变成了list.0.name。因为 Element UI 每次渲染都会根据当前的index重新拼接prop,所以只要:prop是动态绑定的,删除后的重新渲染、重新校验基本能自动对齐。

但如果你在某个el-form-item上写死了prop="list.1.name",删掉第一行后,老的“第二行”内容仍然挂在list.1.name上,而渲染出的第一行prop却是list.0.name,这时校验规则能找到的路径就错了位,导致校验结果张冠李戴。

另外,动态删改数组时,不要用index作为:key的唯一标识。因为删掉中间行后,index会被重新分配,容易引发输入框状态串行(比如输入框里出现了上一行的内容残留)。建议给每一项加一个唯一的 id 或者时间戳字段作为key,而prop仍然用index去拼路径。

3.2 动态必填规则:按条件切换 required

多层级对象里,经常有“某个字段在某种情况下才必填”的需求,比如说“当选择了某选项后,对象的子字段才必填”。此时可以分两种方式处理:

第一种,用函数动态生成rules:

computed: { rules() { return { 'user.phone': [ { required: this.form.hasPhone, message: '手机号必填', trigger: 'blur' } ] } } }

这种方式很直观,只要this.form.hasPhone变化,rules就会被重新计算,required属性跟着变化。

第二种,用自定义 validator :

rules: { 'user.phone': [ { validator: (rule, value, callback) => { if (this.form.hasPhone && !value) { callback(new Error('手机号必填')) } else { callback() } }, trigger: 'blur' } ] }

这种方式更灵活,适合需要同时判断多个字段联动的情况。比如“工作单位类型是企业时,单位名称必填”这种跨字段联动条件,用 validator 是最清晰的。

注意:使用自定义 validator 时,函数内部不要用箭头函数之外的方式丢失this指向。建议使用validator: (rule, value, callback) => {...}配合外层保存this,或者直接写在methods里用普通函数并绑定实例。如果this指错了,vue 实例上的form数据就拿不到,判断条件就会失效。

4. 校验的触发时机与手动调用的正确姿势

rules 配好之后,校验不会自动“全表单扫描”,它是在特定事件触发后才对单个字段进行校验。理解这一点,很多“为什么没反应”的问题其实就解开了一大半。

4.1 trigger 到底怎么写才有效

每个规则里的trigger字段对应的是组件触发校验的事件。

  • el-input:默认支持blur和change。blur是失焦时触发,change是输入内容变化后触发。文本输入建议用blur,避免用户还没输入完就提示。
  • el-select、el-radio-group、el-checkbox-group、el-date-picker、el-cascader、el-switch:推荐change。因为它们没有“失焦”概念,选择完成即触发。
  • el-input-number:也是change,因为值变化时事件就是 change。

如果你trigger写错了,比如给el-select写了blur,很可能整个校验都不会触发,因为组件根本没有派发 blur 事件给校验器。常见症状就是“我选了值,但错误提示没有消失”。

4.2 整表单校验、局部校验和清除校验

提交时通常需要整体校验:

this.$refs.formRef.validate((valid) => { if (valid) { // 校验通过,提交 } else { // 校验不通过 } })

更推荐用 Promise 的方式:

this.$refs.formRef.validate() .then(() => { // 校验通过 }) .catch(() => { // 校验失败 })

还有两个非常实用的 API:

// 校验指定 prop 路径的字段 this.$refs.formRef.validateField('user.phone', (errorMessage) => { // errorMessage 为空表示通过 }) // 清除某个字段的校验状态 this.$refs.formRef.clearValidate('user.phone')

在动态表单里,clearValidate这个 API 特别有用。比如“切换某个选项后隐藏了一个字段”,如果不手动清除该校验状态,隐藏字段可能还残留着红色报错提示,再次提交时也会因为残留状态导致整体表单无法通过。正确的做法就是在字段隐藏或清空时主动clearValidate对应的 prop。

提示:clearValidate支持数组,比如this.$refs.formRef.clearValidate(['user.phone', 'user.email'])。如果是动态数组,需要拼出每个 index 对应的路径列表再清除。

4.3 手动触发校验的三个“不起作用”原因

我遇到过不少同事反馈“我明明调用了 validate,为什么没反应”,排查下来基本是以下三种情况:

第一,ref写到了el-form-item上而不是el-form上。this.$refs.formRef.validate会报undefined。

第二,prop写得不匹配。例如规则里有user.phone,但页面上的el-form-item的prop写成了phone,那么validate的时候,校验器找不到和user.phone对应的表单项,自然没有任何提示。

第三,rules没有正确绑定到el-form上,比如写成了静态属性rules而不是:rules。别笑,这个坑我真的见人踩过,绑定错误时 rules 根本不会进到校验器。

5. 常见问题与排查技巧实录

下面汇总一下多层级对象表单校验中最高频的几个问题,附上排查思路,做成一个速查表,方便对照。

5.1 高频问题速查表

问题现象可能原因排查与解决办法
提交时整体 validate 无任何反应el-form上的ref没写或不对检查ref="formRef"是否写在el-form上,并用this.$refs.formRef.validate()
单个字段敲了内容但错误提示不消失trigger写错,或prop路径不对确认输入组件对应的事件类型,把trigger改成blur/change
提示出现[object Object]外层el-form-item的prop指向了对象本身,而不是具体字段把prop改成深层路径,如user.name
动态列表只有第一行校验生效prop没有用index拼接改为:prop="'list.' + index + '.name'"
删除/增加行后校验错乱key用了index且数组中间变动给每项加唯一 id 作为key
条件必填不生效required写死在静态 rules 里改用 computed 生成 rules 或自定义 validator
隐藏字段校验残留字段隐藏时没有清除校验状态调用clearValidate并传入对应 prop 路径
校验规则是异步接口查询,结果抖动缺乏防抖或竞态处理在自定义 validator 中使用防抖并确保只采用最新请求结果

5.2 定位问题的一套组合拳

排查这类问题,我一般按三步走。

第一步,先看控制台有没有报错。Element UI 校验失败时某些版本会在控制台打印警告信息,里面会指出哪条规则存在问题,这是最快的线索。

第二步,在自定义 validator 里加console.log打印value和rule.field。很多问题一打印就明白了,比如value是undefined,说明路径没取到值;rule.field和你预期的 prop 不一致,说明路径拼写有问题。

第三步,直接调用validateField测试指定路径。比如:

this.$refs.formRef.validateField('user.phone', (msg) => { console.log(msg) })

如果这个字段路径正确,输出会明确是'请输入手机号'这类提示;如果输出undefined但页面明显是空的,说明根本没有对应的表单项绑定到这个 prop,检查模板里的el-form-item去。

6. 几个值得注意的细节坑

上面只是把流程走通,但真正上线前还需要注意几个容易被忽视的细节,处理不好会出现“这次校验通过,下次却异常”之类的怪问题。

6.1 异步校验的竞态问题

多层级对象里,有些字段的校验规则需要后端接口确认,比如“校验手机号是否已被注册”这类场景。自定义 validator 里发请求:

validator: (rule, value, callback) => { if (!value) { callback(new Error('请输入手机号')) return } checkPhone(value).then((res) => { if (res.exists) { callback(new Error('手机号已注册')) } else { callback() } }) }

这里有个隐患:用户快速连输几个字符,每次都会发请求,上一次请求可能比下一次更晚返回,结果把旧结果当新结果用,导致提示内容与当前输入不一致。

处理方式很简单,加一个“请求序列号”:

let requestSeq = 0 validator: (rule, value, callback) => { const currentSeq = ++requestSeq checkPhone(value).then((res) => { if (currentSeq !== requestSeq) return // 只有最后一次请求的返回才有效 }) }

这个技巧成本极低,但能避免很多令人困惑的现场问题。

6.2 值为 0 或空字符串时的校验判断

多层级对象里经常有数值字段,比如年龄、数量。required校验对0是放行的(因为 0 不是空值),但如果后端要求“人工必须大于 0”,光靠required拦不住。

这时候可以这么写:

rules: { 'user.age': [ { required: true, message: '请输入年龄', trigger: 'blur' }, { type: 'number', min: 1, message: '年龄必须大于0', trigger: 'blur' } ] }

还有一类坑是:多层级对象中某个字段初始为null或undefined,规则里如果配置了type: 'email'或其他类型,校验器可能直接报类型错误,在你还没输入任何东西时就开始报警。常见的处理是给type规则加上required前置判断,如果一个字段是非必填但填了才校验格式,可以写成:

[ { validator: (rule, value, callback) => { if (!value) { callback() } else { const isValid = /^[\w.-]+@[\w-]+\.[\w.]+$/.test(value) if (!isValid) { callback(new Error('邮箱格式不正确')) } else { callback() } } }, trigger: 'blur' } ]

这种做法确保“不填不报错,填了格式不对才报错”,在多层级对象可选项较多的场景下非常实用。

6.3 不要把 rules 定义在每次渲染都会新生成的函数里

就是不要写成这样:

computed: { dynamicRules() { return { 'user.phone': [ { required: !!this.form.user.phoneRequired, message: '手机号必填', trigger: 'blur' } ] } } }

虽然上面这个写法本身没问题,但要小心:当:rules绑定的是一个每次计算都返回新函数的rules对象时,可能造成校验器重新初始化和校验状态重置。有些版本的 Element UI 对<el-form-item :rules="...">传入新对象时,会重新注册当前表单项的校验规则。

一个更稳妥的方式是:静态规则放在data里定义,动态的必填规则用computed生成,但确保computed只在依赖项变化时才改变返回值的引用。上面computed的写法因为内部依赖了this.form.user.phoneRequired,所以只在它变化时才返回新对象,这个是安全的。如果是完全不依赖任何响应式数据的函数,每次执行都返回新对象,就可能会干扰校验状态。

6.4 多层级对象的默认值与初始化

校验能不能正常触发,还跟初始值有关。如果form.user.phone一开始是undefined,而el-input的v-model绑定的是item.phone,可能出现输入框“不显示输入内容”的怪问题。因为 Vue 无法在响应式系统里追踪到后续新增的phone属性。

初始化时一定要把多层级对象的完整结构定义好:

data() { return { form: { user: { name: '', age: '', phone: '' }, list: [] } } }

尤其是空数组list这一项,不能省掉。否则后面this.form.list.push(...)在 Vue 响应式系统里是可以工作的,但如果你直接this.form.list = [...],只要list属性从一开始就不存在,重新赋值同样能触发视图更新,因为form本身是响应式的,关键是el-form-item里对深层路径取值时要能取到。为了少踩无谓的坑,初始化时把所有层级的字段全部列出来,是最省心的方案。

7. 后续还能怎么改

多层级表单校验本身是个日常需求,但如果不满足于“能校验通过”,有几个方向可以进一步优化。

第一个方向是抽出通用的 “类型校验器”。比如项目里大量存在“手机号、邮箱、身份证号”等格式校验,可以封装成独立模块:

// validators.js const phoneRule = { validator: checkPhone, trigger: 'blur' } const emailRule = { validator: checkEmail, trigger: 'blur' }

然后在各个表单里复用。这样多层级对象里的规则配置会清爽不少。

第二个方向是对于极其复杂的动态表单(比如配置化动态生成表单项),可以把prop路径的拼接逻辑封装成工具函数,统一生成带索引的路径字符串,避免各页面手拼出错。

第三个方向是考虑把表单校验和提交动作解耦,所有validate调用统一放到一个提交基类里。这个就见仁见智了,项目小的话没必要过度设计。

我个人做过一段时间的配置化表单,最深的体会是:多层级对象本身不是问题,问题在于路径不一致。只要遵循“prop 路径 = rules 路径 = model 深层路径”这个铁律,静态、动态、数组、嵌套都能一套逻辑打通。真到了要排查具体 bug 的时候,优先打印rule.field和value,基本两分钟就能定位到问题出在哪一层。

最后再分享一个小习惯:所有多层级对象字段初始化时,我都会在数据里把完整结构写出来,禁止出现“用到了才往对象上挂字段”的写法。这个习惯帮我省掉了至少一半的“表单校验不触发”类问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询