news 2026/9/30 1:39:05

拆透 el-form 校验链路:model、prop 与 rules 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
拆透 el-form 校验链路:model、prop 与 rules 实战

1. 先把 el-form 这套设计逻辑捋清楚

刚接手后台管理系统那会儿,我写过不少表单,也踩过不少坑。最常见的场景是:页面上有十几个字段,用户点了提交,接口返回一句"登录失败:表单提交校验失败,请刷新后重试",然后我盯着代码发懵——明明每一项都写了rules,为什么前端一个红字都不弹?后来才慢慢明白,el-form的model、prop、rules这三样东西不是各写各的,它们之间是一条必须严丝合缝的链路。任何一个环节断了,表单校验就会静默失效,不报错、不提示,比报错还难受。

这篇文章我就把这条链路从头到尾拆一遍。el-form的model属性和prop属性是校验寻址的两个坐标,rules是规则本身,三者配合才能让表单校验真正跑起来。适合谁看?写过一点 Vue、用过 Element UI 或者 Element Plus、但对其中的机制还是一知半解的同学;也适合那些"表单能用但不敢改"的朋友,改一个字段就怕牵连出三个 bug。

我不会只贴 API 文档,那玩意儿官网都有。我打算讲清楚每个设计背后的原因,讲清楚我实际项目中踩过的坑,以及一套可以拿去直接抄的落地写法。

1.1 原生表单到组件化表单,中间差了什么东西

原生 HTML 表单的校验非常朴素:<input required>、<input type="email">、pattern属性,浏览器自己帮你校验,提交时弹个原生气泡。这套东西的问题在于样式没法改、错误信息没法定制、跨字段联动基本做不了。你在做一个中后台系统的时候,客户要求"手机号输入框下面显示一行红色小字",原生方案直接歇菜。

组件化表单要解决的是三件事。第一是数据同步——输入框的值怎么进到我的 JS 对象里;第二是规则声明——校验规则写在哪里、怎么写才不散落一地;第三是状态管理——哪一项校验失败了、失败信息怎么拿到、什么时候清掉。

Element 的设计思路是:数据同步交给v-model,规则声明交给rules,状态管理交给el-form-item内部维护。但问题是,el-form-item怎么知道"我这个输入框对应的规则是rules里的哪一条"?它没法靠猜,所以必须有一个人告诉它——这就是prop存在的全部理由。而prop是model上的一个路径,所以又必须有model。三个东西环环相扣,缺一不可。

理解了这个因果链,很多"为什么我这么写不行"的问题就自然有答案了。

1.2 model 负责"数据在哪",prop 负责"字段是谁"

打个比方。el-form的model就像一张登记表,prop是表格里某一栏的编号,rules是这一栏的填写要求。用户填完表交上去,审核员拿着编号去表格里找对应内容,再对照要求判断是否合规。

关键点在于:审核员只看编号找内容,不看你的输入框放在页面哪个位置。所以如果你的prop写成了username,但model里根本没有username这个字段,审核员就会找不到东西,直接放行——校验静默通过。这就是"规则写了但没生效"的第一大元凶。

另一个容易混淆的点是el-form-item上还有个label属性,它只负责显示文字,跟校验一点关系都没有。我见过有人把prop和label搞混,写了个prop="用户名",然后纳闷为什么校验不了。label是给人看的,prop是给程序看的,这两者身份完全不同。

1.3 校验链是如何一步步串起来的

完整链路我用文字描述一遍,你可以对着自己的代码逐项核对:

用户输入触发事件(blur 或 change)→ 事件冒泡到el-form-item→el-form-item拿着自己的prop去el-form的rules里找规则(如果自己身上也有rules,按框架版本决定是覆盖还是合并)→ 拿着prop去el-form的model里按路径取值 → 把值和规则一起交给底层的校验库(async-validator)→ 拿到校验结果,写入el-form-item的validateState,渲染红色错误文字。

这里有两个隐含前提:el-form-item必须能通过组件层级关系找到祖先el-form(靠provide/inject),以及prop必须在model里能取到值。任意一条不满足,链路就断。

我个人的经验是:遇到校验问题,永远先查 prop 和 model 的对应关系,再看 rules 写得对不对,最后才怀疑框架。按这个顺序排查,九成问题三分钟能定位。

2. el-form 的 model 属性:整个校验体系的地基

model是整个表单的数据源,写法很简单,:model="form"。但它真正的作用不只是"绑值",而是给校验提供一个可寻址的数据容器。如果没有它,prop就失去了寻址的起点,规则也就没有对象可校验。很多人把它当成一个普通的绑定,随便写写,结果就在各种边界场景里翻车。

2.1 model 里到底该放什么

最直接的建议是:model里的字段要和表单里所有带prop的项一一对应,哪怕这个字段暂时不用,也要先声明出来。原因有两个。

一个原因是响应式。在 Vue 2 里,对象新增属性不会触发视图更新,也就是常说的"响应式丢失"。如果你在提交前才动态给this.form加一个字段,然后指望校验能读到它,那是读不到的。Vue 3 用 Proxy 解决了这个问题,但声明齐全仍然是更稳妥的写法。

另一个原因是resetFields。这个方法的逻辑是把字段恢复到初始值,而初始值是在el-form-item挂载那一刻记下来的。如果字段在挂载时压根不存在于model上,重置后的效果可能是删掉这个属性,而不是恢复成空字符串,后续校验的行为就很诡异。

所以我在项目里养成的习惯是,先把数据结构写全:

data() { return { form: { username: '', password: '', mobile: '', agree: false, tags: [], ext: { company: '', position: '' } }, rules: { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' } ], agree: [ { required: true, message: '请先同意相关条款', trigger: 'change' } ], tags: [ { type: 'array', required: true, message: '至少选择一个标签', trigger: 'change' } ] } } }

注意tags我给的是type: 'array'。这里有个小陷阱:数组的空值判断和字符串不一样。async-validator 在处理type: 'array'时,把length === 0视为空,所以[]会触发必填错误,这一点符合直觉。但如果你不写type: 'array'而写默认的string,[]会被当成"有值",必填直接通过,你就白写了。

2.2 响应式陷阱:整体替换 model 会发生什么

我在一个项目里遇到过这么个需求:编辑页和新增页共用同一个组件,编辑时拉详情接口回填数据,新增时用空对象。当时同事图省事,直接在接口回调里写了this.form = res.data。

结果就是,编辑页的表单能显示数据,但校验状态乱套了。原因在于el-form-item在挂载时缓存了初始值,你整体替换model之后,组件内部记的还是旧对象,某些重置行为就会出现"改了没反应"或者"重置成了上一个页面的值"。

正确的做法是逐字段赋值,或者用Object.assign把新数据合并到原对象上,保持对象的引用不变:

// 不推荐:整体替换,引用变了 this.form = res.data // 推荐:保持引用,逐字段更新 Object.assign(this.form, res.data) // 如果是嵌套对象,建议用展开赋值 this.form.ext = { ...this.form.ext, ...res.data.ext }

另外还有个高频坑:同一个组件实例被复用于不同记录时,别忘了先resetFields或手动清空。我有次做列表页的"编辑"弹窗,连续点两行记录,第二行的表单里还残留着第一行的校验红字,就是因为没清。这一点在后文的排查章节我还会再展开。

2.3 嵌套对象与动态字段的处理方式

嵌套对象在model里是天然支持的,只要你的prop写对了路径就行。比如model里有ext: { company: '', position: '' },那么el-form-item的prop就写ext.company。

对于动态生成的字段,一般会有两种方案。一种是用数组结构,比如model里存在contacts: [{ name: '', phone: '' }],这样增删行就是操作数组。这种方案最推荐,因为结构清晰、校验路径也好拼。

另一种是用对象结构,键是动态的,比如model.certMap[typeId]。这种方案的麻烦在于prop要拼成certMap.${typeId},而且model里必须提前把这个 key 初始化好,否则开头提到的响应式问题就会出现。所以除非业务上确实需要按类型做键,我一般优先选数组方案。

还有一个细节值得提:不要把model直接绑成Object.create(null)或者某些特殊对象,老老实实用字面量对象,能省掉很多说不清楚的怪问题。

3. prop 属性详解:校验寻址的唯一凭据

prop是整个校验体系里最容易被低估的属性。它看起来只是一个字符串,实际上承担着"从model里精确取到我要校验的那个值"的职责。理解了它的寻址规则,很多玄学问题都会消失。

3.1 prop 的字符串路径语义

prop支持两种写法:字符串路径和数组路径。字符串写法用点号分隔,比如user.name、list.0.title;数组写法是['user', 'name']。日常开发里字符串写法更常见,可读性也更好。

它内部实现的逻辑大致是:把字符串按.拆开,一层一层往model里钻。所以ext.company会先取model.ext,再取model.ext.company。这意味着中间任何一层如果是undefined,取到的最外层就是空的。比如model.ext没初始化,prop="ext.company"就直接取不到值,校验自然不会有反应。

这里顺带说一个容易忽略的点:prop只影响校验和重置这两个行为,跟输入框实际绑定的v-model是完全独立的两条线。也就是说,理论上你可以让el-input绑一个字段,而el-form-item的prop指向另一个字段,代码能跑但逻辑是错的。我见过因为复制粘贴导致的这种错位,排查了半天。养成习惯:写完一个el-form-item,眼睛扫一遍v-model和prop是不是同一个路径。

3.2 数组、嵌套、动态索引的写法

普通数组项用数字索引,比如list.0.name。动态列表就必须拼字符串了,模板里长这样:

<el-form-item v-for="(item, index) in form.contacts" :key="item.key" :prop="'contacts.' + index + '.phone'" :rules="[ { required: true, message: '请填写联系电话', trigger: 'blur' }, { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' } ]" > <el-input v-model="item.phone" placeholder="请输入手机号" /> </el-form-item>

这里有三个细节值得单独说。

第一个是:key。千万不要用index当 key,要用数据本身的唯一标识。原因很直接:你删掉第一行之后,原来第二行的index从 1 变成了 0,Vue 复用组件时会把el-form-item的实例错配,校验状态、初始值全都跟着串位,表现就是"删了一行之后,剩下的行莫名其妙报错"。用item.key或者后端返回的id才稳。

第二个是:prop前面的冒号。它是动态绑定,不是静态字符串。如果你漏了冒号写成prop="contacts.index.phone",那程序就会真的去找一个叫contacts.index.phone的路径,找不到,校验失效。

第三个是定位方式。prop用索引寻址,意味着数组顺序一旦变化,规则和数据的对应关系就依赖索引保持一致。这也是为什么我不建议在列表里做太激进的排序操作,尤其是编辑态的实时排序。

3.3 prop 写错的几种典型表现

我把实际遇到过的表现整理成一张表,对照着看能省不少时间:

现象大概率原因定位方式
填了内容也提示必填prop路径找不到,取到undefined断点看model上路径是否真的存在
完全不校验,直接提交成功prop没写,或者el-form没有:model检查el-form上是否绑了 model
校验报错但红字不显示el-form-item内没有绑定对应字段,或 CSS 被覆盖看 DOM 里有没有is-error类
校验报错位置错乱动态列表 key 用了 index,实例复用串位换成唯一 id 做 key
规则完全没走prop和rules的键名不一致两处键名逐字比对

提示:prop必须与rules对象的键名逐字一致,包括大小写。userName和username在程序眼里是两个完全不同的字段。

4. 表单校验规则 rules 全解析

规则本身是async-validator这套库的能力,el-form只是把它包了一层。搞清楚规则怎么写、挂在哪里、什么时候触发,基本就能覆盖日常开发的全部场景。

4.1 rules 的三种挂载位置与优先级

rules可以写在三个地方。

第一是el-form上,用:rules="rules"声明一个完整规则表,键名对应各个prop。这是最推荐的写法,规则集中、便于维护,也方便做整体的规则复用。

第二是el-form-item上,用:rules="[...]"单独声明。适合"这个字段的规则非常特殊、只有它一个人用"的场景,比如动态行里的手机号格式。

第三是两者都写。这时候要特别注意:不同版本对"合并还是覆盖"的处理不一样。有的实现里el-form-item自己的规则会覆盖el-form上同名的规则,有的则是拼接。为了避开这个不确定性,我的做法是——同一个字段的规则永远只写在一个地方。要么全放el-form,要么全放el-form-item,别混着来。

顺带提一句validate-on-rule-change这个属性,它默认是true,意思是规则变化后立刻重新校验一遍。这在动态规则场景下会带来一个副作用:你刚提交完锁定某个字段,规则一改,页面立刻飘红字。遇到这种情况,把它设为false会更符合预期。

4.2 内置校验类型与常用参数

async-validator内置了一批类型,常用的有这些:

type 值适用场景需要注意
string文本输入,默认值数组不加 type 时按字符串校验,空数组会误判
number数字输入值是字符串会校验失败,需要 transform
array多选、标签、列表空数组会触发 required
email邮箱只做格式判断,不做可达性验证
url链接对中文域名支持一般,谨慎使用
date日期需要传入 Date 对象
integer整数小数点会直接失败

几个高频参数也顺带说一下。required控制必填,注意它在type: 'number'下,值为0是合法的,不会误判为空,这点和某些手写判断不一样。min/max对字符串是长度,对数字是范围,所以配合type使用才不会搞混。pattern是正则,记得把正则写成字面量,别写成字符串。

数字类型的坑我单独说一个。如果你给一个el-input绑定了字符串值,又写了type: 'number',用户输入123也会报错,因为值是"123"不是123。这时候要用transform转一下:

{ type: 'number', required: true, message: '请输入数量', trigger: 'blur', transform: (value) => Number(value) }

transform会在校验前对值做一次处理,且不会影响原始数据,这个特性非常实用。

4.3 自定义 validator 的写法与坑

内置规则覆盖不了的情况,就得自己写validator。它接收三个参数:rule、value、callback。核心纪律只有一条:无论成功还是失败,都必须调用一次callback,而且只能调用一次。

const checkPasswordAgain = (rule, value, callback) => { if (!value) { callback(new Error('请再次输入密码')) } else if (value !== this.form.password) { callback(new Error('两次输入的密码不一致')) } else { callback() } }

不调callback会导致什么?校验一直挂在那里,validate的 Promise 既不 resolve 也不 reject,你会看到提交按钮转圈转到天荒地老。这个坑我踩过,排查的时候完全不知道问题在哪,因为控制台一点报错都没有。

另一个坑是在validator里写了return却忘了调回调。比如:

// 错误写法:提前 return,callback 永远不会执行 const badValidator = (rule, value, callback) => { if (!value) return callback() }

正确写法是return callback()或者return callback(new Error('...'))。

还有一点,自定义validator里的this需要注意。如果你用箭头函数定义在data()里,this指向组件实例没问题;但如果定义在methods里再引用,就要保证引用方式正确。我一般把validator写在data里配合箭头函数,或者直接在methods里定义然后this.checkXxx引用。

4.4 trigger 触发时机怎么选

trigger决定校验什么时候跑,常见取值是'blur'和'change'。

blur在输入框失去焦点时触发,体验最好,因为用户输入过程中不会一直被红字打扰。change在值变化时触发,适合下拉框、单选、多选、开关这类"选完即定"的控件。

有些字段需要两个都监听,写成数组trigger: ['blur', 'change']。典型场景是"必填且格式有要求"的输入框——失焦时检查格式,值变化时清掉旧的错误提示。

这里必须提醒一个高频陷阱:自定义组件默认不触发校验。原因在于trigger依赖组件往外派发特定事件,普通el-input内部做了适配,你自己封装的组件如果只emit('update:modelValue')而不派发对应事件,失焦、值变化都不会触发校验。解决办法有两个,一个是让组件对外派发匹配的事件,另一个是关闭自动校验、手动触发:

<el-form-item prop="customField"> <MyCustomInput v-model="form.customField" :validate-event="false" @change="handleCustomChange" /> </el-form-item>
handleCustomChange() { this.$refs.formRef.validateField('customField') }

这种"手动触发"的思路在复杂场景下反而更可控,我现在的项目里凡是自定义控件,基本都走这条路。

5. 从零落地一个可复用的表单校验方案

前面讲的都是零件,这一节我把它装成一台能跑的车。我会按一个"员工信息登记表单"的完整场景来写,包含普通字段、嵌套字段、动态列表、提交和重置的全部逻辑。

5.1 数据结构与规则表设计

先定数据。我的习惯是先写form,再对着form逐字段写rules,这样不会漏。

data() { return { formRef: null, submitting: false, form: { name: '', mobile: '', department: '', enterDate: '', skills: [], emergency: { contactName: '', contactPhone: '' }, projects: [] }, rules: { name: [ { required: true, message: '请输入姓名', trigger: 'blur' }, { min: 2, max: 20, message: '姓名长度为 2 到 20 个字符', trigger: 'blur' } ], mobile: [ { required: true, message: '请输入手机号', trigger: 'blur' }, { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' } ], department: [ { required: true, message: '请选择部门', trigger: 'change' } ], enterDate: [ { required: true, message: '请选择入职日期', trigger: 'change' } ], skills: [ { type: 'array', required: true, message: '请至少选择一项技能', trigger: 'change' } ], 'emergency.contactName': [ { required: true, message: '请输入紧急联系人', trigger: 'blur' } ], 'emergency.contactPhone': [ { required: true, message: '请输入紧急联系人电话', trigger: 'blur' }, { pattern: /^1[3-9]\d{9}$/, message: '电话格式不正确', trigger: 'blur' } ] } } }

注意rules里的键名用了emergency.contactName这种带点的形式。这是支持的标准写法,只要prop和它完全一致就行。我当初第一次看到也怀疑过,实际测下来是没问题的。

5.2 模板层编写要点

模板部分有几个位置容易出问题,我标出来。

<el-form ref="formRef" :model="form" :rules="rules" label-width="110px" status-icon :scroll-to-error="true" > <el-form-item label="姓名" prop="name"> <el-input v-model="form.name" placeholder="请输入姓名" clearable /> </el-form-item> <el-form-item label="手机号" prop="mobile"> <el-input v-model="form.mobile" placeholder="请输入手机号" maxlength="11" /> </el-form-item> <el-form-item label="部门" prop="department"> <el-select v-model="form.department" placeholder="请选择部门" clearable> <el-option label="研发部" value="rd" /> <el-option label="产品部" value="pd" /> </el-select> </el-form-item> <el-form-item label="技能" prop="skills"> <el-checkbox-group v-model="form.skills"> <el-checkbox label="前端" /> <el-checkbox label="后端" /> <el-checkbox label="测试" /> </el-checkbox-group> </el-form-item> <el-form-item label="紧急联系人" prop="emergency.contactName"> <el-input v-model="form.emergency.contactName" placeholder="请输入姓名" /> </el-form-item> <el-form-item label="联系人电话" prop="emergency.contactPhone"> <el-input v-model="form.emergency.contactPhone" placeholder="请输入电话" /> </el-form-item> <el-form-item v-for="(item, index) in form.projects" :key="item.key" :label="'项目' + (index + 1)" :prop="'projects.' + index + '.projectName'" :rules="[ { required: true, message: '请输入项目名称', trigger: 'blur' } ]" > <el-input v-model="item.projectName" placeholder="请输入项目名称" /> <el-button type="danger" link @click="removeProject(index)">删除</el-button> </el-form-item> <el-form-item> <el-button type="primary" :loading="submitting" @click="handleSubmit">提交</el-button> <el-button @click="handleReset">重置</el-button> <el-button @click="addProject">添加项目</el-button> </el-form-item> </el-form>

有几点值得说明。

ref="formRef"是必须的,后面所有主动调用的校验方法都靠它。status-icon会在校验通过时给输入框加个绿色对勾,体验上会好一些。:scroll-to-error="true"在长表单里非常实用,提交失败时自动滚到第一个出错的位置,省得用户从上往下翻。

动态行的:rules我写在了el-form-item上,因为这些规则与具体行绑定,放在el-form的rules里反而不方便。但要注意,这样一来,这个字段的规则来源就只有el-form-item一处,符合前面说的"规则不分散"原则。

5.3 提交、重置、局部校验的完整实现

逻辑层是整个方案的核心,我逐块写。

methods: { addProject() { this.form.projects.push({ key: Date.now(), projectName: '' }) }, removeProject(index) { this.form.projects.splice(index, 1) }, async handleSubmit() { if (this.submitting) return this.submitting = true try { await this.$refs.formRef.validate() await this.submitToServer(this.form) this.$message.success('提交成功') } catch (err) { if (err && err.fields) { this.$message.warning('请检查表单填写是否完整') } else { this.$message.error('提交失败,请稍后重试') } } finally { this.submitting = false } }, handleReset() { this.$refs.formRef.resetFields() this.form.projects = [] } }

这段代码里藏着几个我吃过亏的点,逐个说。

第一,validate()不传回调时返回 Promise,并且校验失败会 reject。这就意味着你必须try/catch,否则控制台会飘一个未捕获的 Promise 错误。更麻烦的是,这个 reject 的错误对象和接口请求失败的错误对象长得不一样,如果你不区分,用户明明只是没填字段,你却弹了"网络异常",体验很差。所以我用err.fields是否存在来判断是不是校验错误。

第二,submitting这个锁非常必要。表单校验是异步的,用户手快点两下,可能发出两个请求,后端就多了一条脏数据。加个锁成本极低,收益很高。

第三,resetFields管不到动态列表。因为它只能重置那些有prop的静态字段,form.projects是数组,resetFields不会去清空整个数组。所以我在后面补了一行手动清空。这个坑我印象很深,当时测的时候发现"重置之后动态行还在",一度以为是框架 bug。

如果只想校验某几个字段,用validateField:

// 校验单个字段 this.$refs.formRef.validateField('mobile') // 校验多个字段,传数组 this.$refs.formRef.validateField(['mobile', 'name'], (errorMessage) => { if (errorMessage) { console.log('还有字段未通过', errorMessage) } else { console.log('这几个字段都通过了') } })

validateField在实时联动校验里特别有用,比如"手机号填完立刻去查是否被注册"。这时候你只想校验这一个字段,用validate会带着整个表单一起报红,视觉上很吵。

5.4 动态增删行的校验处理

动态行的校验有两个额外的坑,单独拎出来说。

第一个是删除行之后残留校验状态。你删掉一行,Vue 复用组件实例,原来那行的错误状态可能被带到新的一行上。解决办法是在删除操作后主动清一下校验:

removeProject(index) { this.form.projects.splice(index, 1) this.$nextTick(() => { this.$refs.formRef.clearValidate( this.form.projects.map((_, i) => `projects.${i}.projectName`) ) }) }

clearValidate只清校验状态,不动数据,正好符合这个场景。包一层$nextTick是因为 DOM 更新是异步的,你需要等列表渲染完再清。如果不传参数,它会清掉全部字段的校验状态,也可以直接用它做"全部清红"。

第二个是提交后的回填。编辑已有数据时,projects数组是接口给的,每一条都要手动补上key,否则:key会取到undefined,Vue 会警告并且可能复用错位。我在回填时统一写了这么一行:

this.form.projects = (res.data.projects || []).map((item, index) => ({ ...item, key: item.id || `${Date.now()}_${index}` }))

这个key不需要稳定跨页面,只要在当前渲染周期内唯一就行,所以用时间戳加索引兜底是够用的。

6. 常见问题与排查实录

这一节是我这些年攒下来的问题清单,基本覆盖了你会遇到的大部分情况。

6.1 校验完全不生效的排查顺序

我整理了一个固定的排查顺序,按这个来基本不会走冤枉路。

第一步,检查el-form上有没有:model。这是最常见也最隐蔽的问题。很多人从别的项目复制代码,把:model那一行漏掉了,模板里照样能显示、能输入,但校验就是不触发,而且控制台一个字都不报。

第二步,核对prop和rules的键名。逐字比对,包括大小写、点号、是否有多余空格。我见过prop="user Name"中间多了个空格的,肉眼扫过去完全看不出来。

第三步,确认prop路径在model上真的取得到值。在提交的方法里console.log(this.form)看一眼,特别是嵌套字段,中间那一层有没有初始化。

第四步,确认el-form-item没有被v-if干掉。被移除的组件不会参与校验,但如果你期望它校验,那就会漏。这种情况应该用v-show或者给字段加非空兜底。

第五步,检查trigger是否和实际交互匹配。比如el-select写了trigger: 'blur',在选择过程中可能根本不触发,用户选完了也没校验。这种情况改成change就好了。

注意:el-form的model如果绑的是undefined或null,校验会直接抛错,而且错误信息不太友好。初始化时给它一个空对象{}是最保险的。

6.2 重置与清除校验的坑

resetFields和clearValidate是两个特别容易搞混的方法,我列个表对比一下。

方法作用是否改动数据重置成什么
resetFields()重置字段值并清除校验状态是组件挂载时记录的初始值
clearValidate()只清除校验状态否无
resetField(prop)重置单个字段的值和状态是初始值

关于resetFields,有三个高频误区。

误区一,以为它会把字段清成''。实际上它恢复的是初始值,如果你的表单初始值是'默认部门',重置后就是'默认部门',不是空串。这在"从接口拉初始值"的场景下会导致一个奇怪的现象:用户改了值,点重置,回来了,但他说"我要清空"。这种情况你就得手动赋值。

误区二,以为它万能的。前面说过,动态数组、没有prop的字段,它都管不了。

误区三,在弹窗场景下忘记先重置再赋值。正确的顺序是:打开弹窗 → 先resetFields清掉上一次的残留 → 再回填本次的数据。顺序反了,resetFields会把刚回填的数据又还原回去,表现就是"赋值了但显示还是空的"。

6.3 异步校验、防抖与提交竞态

带接口请求的校验,也就是异步校验,需要写asyncValidator:

{ asyncValidator: (rule, value) => { if (!value) return Promise.resolve() return new Promise((resolve, reject) => { checkNameUnique(value).then((res) => { res.data.unique ? resolve() : reject(new Error('该名称已被使用')) }).catch(() => { reject(new Error('校验服务暂时不可用')) }) }) }, trigger: 'blur' }

这里有两个要注意的地方。

一个是asyncValidator必须返回 Promise,且在校验通过时resolve(),失败时reject(new Error(...))。如果 reject 一个字符串或者普通对象,错误提示可能显示不出来。

另一个是异步校验天然会带来竞态。用户快速在输入框里输入、失焦、再输入、再失焦,两次请求的返回顺序可能颠倒,导致最后显示的是旧结果。解决办法有两个:一是加防抖,二是给每次请求打标记,只认最新一次的返回。我在项目里通常是两者都上:

let seq = 0 asyncValidator: (rule, value) => { const current = ++seq return checkNameUnique(value).then((res) => { if (current !== seq) return Promise.resolve() return res.data.unique ? Promise.resolve() : Promise.reject(new Error('该名称已被使用')) }) }

至于提交竞态,前面提到的submitting锁已经能解决大部分情况。另外如果你的表单支持"边输入边自动保存",那还要考虑保存请求和提交请求的顺序问题,这就属于更复杂的场景了,一般中后台系统不太需要。

6.4 表单校验与其他场景的联动

最后补充两个实战里经常遇到的联动场景。

一个是条件必填。比如"是否需要发票"选是,才校验"发票抬头"。实现方式是在el-form的规则表里做动态判断,或者用validate方法内联判断:

const dynamicRequired = (rule, value, callback) => { if (this.form.needInvoice && !value) { callback(new Error('请填写发票抬头')) } else { callback() } }

记得在"是否需要发票"的change事件里,主动触发一次发票抬头字段的校验,否则用户切换选项后不会立刻看到变化。

另一个是表单校验对提交按钮的控制。有人喜欢用validate的实时结果去控制按钮禁用,这个做法我不太推荐,因为用户没填完的时候按钮是灰的,他不知道哪里出了问题,体验反而差。更好的做法是按钮一直可点,点了之后走校验、滚到第一个错误位置、再给出统一提示。这样用户至少知道"我点了,系统告诉我哪里要改"。

7. 我在实际项目里的一些体会

写到这里,关于el-form的model、prop和表单校验,该说的基本说完了。最后分享几个我个人的判断,不算规范,但都是真实用过的。

规则和字段的对应关系,越集中越好维护,越分散越容易出问题。我现在做表单,第一件事就是把form和rules两个对象并排写出来,字段名一一对齐,写完再动手写模板。这样能避免大部分"规则写了但没生效"的问题,因为对齐的过程本身就是一次检查。

不要迷信框架的自动化,主动控制往往更可靠。比如自定义控件的校验,与其去研究要派发哪个事件、什么时机派发,不如直接关掉自动校验,在合适的时机手动调validateField。代码多写两行,但调试成本几乎为零。

异步校验是表单里最容易被低估的部分。同步校验出错,页面上立刻有反馈;异步校验出错,可能是一次请求失败、可能是竞态导致的结果错乱,排查起来麻烦得多。如果你的表单里有任何走接口的校验,一定要把超时、失败兜底、竞态标记这三件事想清楚。

最后一个小技巧。如果你发现某个字段的校验怎么调都不对,最快的验证方式是把prop改成model上最外层的一个简单字段,比如临时改成username,看校验是不是立刻正常了。如果能正常,说明规则和链路都没问题,问题就在路径上;如果还是不行,那问题就在el-form的model或者rules的挂载位置。用这个二分法定位,比一行行翻代码快得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 1:39:00

FPGA实战:CORDIC算法实现三角函数计算与EGo1上板验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:38:57

大模型全生命周期开源评测工具链生态与实操全景横评

大模型全生命周期开源评测工具链生态与实操全景横评在 2026 年的大模型&#xff08;LLM&#xff09;研发与生产化落地中&#xff0c;“如何科学、客观、无偏地评估一个大模型究竟有多聪明、有多安全、有多可信”&#xff0c;已经成为决定企业技术路线与投资决策的核心胜负手。 …

作者头像 李华
网站建设 2026/9/30 1:37:01

QT+Halcon机器视觉实战:产线级测量系统搭建与调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:36:19

Linux 中文 man 手册安装指南:从查找机制到实战配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:35:40

YOLO11cls农作物病虫害分类实战:数据集构建、训练调参与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华