十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Element UI 迁移 Ant Design Vue 表单验证避坑指南

Element UI 迁移 Ant Design Vue 表单验证避坑指南 如果你的项目最近在做组件库切换或者团队里 React 端用 Ant Design、Vue 端用 Element UI 两套并存那表单验证这块你迟早要经历一次“搬家”。我最近就在给一个中后台项目做迁移把 Element UI 的表单挪到新版 ant design vueVue3 的 FormElement UI 里辛辛苦苦调通的 rules 几乎原样搬过去结果验证不触发、错误清不掉、甚至空值直接提交成功光这些问题就排查了一个下午。说实话表单验证只是换组件库时最容易被低估的一环。旁边还有 icon 体系要换Element UI 里el-icon-setting切到 Ant Design 系要么用SettingOutlined /这种组件要么在旧版 antdv 用a-icon typesetting /但这都属于“看得见的改动”。真正麻烦的是验证逻辑这种东西它藏在你写的 rules、ref、v-model、initialValues 里一个细节没对齐行为就完全不一样。下面这篇东西不是我抄文档整理出来的是我这段时间实际踩坑、逐行对比两个库得到的经验适合正在做 Element UI 与 Ant Design / ant design vue 迁移或者需要在两套体系里维护表单的同学参考。1. 迁移前先搞清楚两套表单验证的底层思路差在哪1.1 Element UI 的验证链路Element UI 的表单验证依赖 async-validator但外壳决定了它很多行为。el-form上要绑定:model和:rulesel-form-item通过prop告诉表单“这个字段叫 username”提交时调用$refs.form.validate()内部遍历有prop的 form-item再交给 async-validator 跑规则。el-form refform :modelform :rulesrules el-form-item label用户名 propusername el-input v-modelform.username / /el-form-item /el-formexport default { data() { return { form: { username: }, rules: { username: [ { required: true, message: 请输入用户名, trigger: blur } ] } }; }, methods: { submit() { this.$refs.form.validate((valid, fields) { if (valid) { // 走提交 } else { // fields 里是各个字段的错误信息 } }); } } };Element UI 最大的特点是表单的数据本质上还是你在外面用v-model维护的那份form组件库只是“读你的数据 根据 rules 提醒你”。这带来的好处是调试直观this.form.username永远是你实际要提交的值坏处是字段有没有参与验证完全取决于 model 里的 prop 是否初始存在、el-form-item是否真的渲染出来了。1.2 Ant Design 系React antd / ant design vue 新版的验证链路React 的 antd 用的是 rc-field-form整个字段值由 Form 内部 store 管理。Form.Item上写name对应 store 里的 keyrules 写在 Form.Item 上而不是统一写在一个外层对象里。提交时不是“调一个 validate 方法看看对不对”而是表单本身知道每个字段的当前值、校验状态、错误信息所以经常看到“自动校验”这种说法。const [form] Form.useForm(); Form form{form} namebasic initialValues{{ username: }} Form.Item label用户名 nameusername rules{[{ required: true, message: 请输入用户名 }]} Input / /Form.Item Button htmlTypesubmit提交/Button /Formant design vue 的新版3.x/4.x基本沿用了 React antd 的形态a-form refformRef :modelformState :rulesrules a-form-item label用户名 nameusername a-input v-model:valueformState.username / /a-form-item /a-form如果你当年用的是 ant design vue 1.x那里还有一套a-form-model长得像 Element UI而旧版a-form又是装饰器写法这本身又是一个容易混的坑。所以迁移前先确认你项目里是哪种 Ant Design 写法再去对照 Element UI 的规则否则代码长得很像行为却对不上。1.3 两套表单直接对照速查表我整理了一份实际项目里最常用到的对照关系建议直接贴到团队文档里功能Element UIAnt Design React / antdv 新版字段定位el-form-item 的 propForm.Item 的 name初始值model 里自己定义Form 的 initialValues提交校验this.$refs.form.validate(callback)await form.validateFields() 或 Form 的 onFinish清空校验状态clearValidateform.setFields([{ name, value, errors: [] }])重置表单resetFieldsform.resetFields()滚动到第一个错误el-form 的 scroll-to-errorForm 的 scrollToFirstError校验库async-validatorasync-validator数据归谁管外部 modelForm store / 外部 model 可配合注意最后一行Element UI 和 Ant Design 用的校验库其实都是 async-validator所以很多基础 rules 的语义是通用的。但你遇到的坑几乎都来自“同一个规则在不同外壳下的触发时机和数据流不同”也就是下面要展开的部分。2. 最容易踩的三个坑trigger、required 和空值判断2.1 trigger 写不对验证要么不触发要么疯狂触发Element UI 的 rules 里那个trigger是真的要命。最常见的写法是rules: { username: [{ required: true, message: 请输入用户名, trigger: blur }] }如果输入框是普通el-inputblur 触发没问题。但如果你把el-input包在自定义弹层组件里或者用了一个不向外 emit blur 的第三方输入框那这条 rule 在提交前可能一次都不会触发。反过来如果你图省事写成trigger: change用户输入第一个字符就开始报错体验非常糟糕尤其密码框或者用户名这种需要输完才能判断的字段。Ant Design 系里的逻辑是另一套React antd 的 Form.Item 默认在字段onChange之后触发校验如果不希望在输入过程中立刻报错要通过validateTrigger控制Form.Item nameusername validateTriggeronBlur rules{[{ required: true, message: 请输入用户名 }]} Input / /Form.Item这里有个细节很多人踩过validateTrigger是定义在 Form.Item 上的但表单控件本身是否在 blur 时把值同步给 Form store取决于 Input 是否触发 onBlur。也就是说你把 validateTrigger 改成 onBlur以为一定会 blur 才校验但如果控件根本没有正常触发 onBlur结果就是校验不跑。经验做法是区分“交互校验”和“提交兜底”。交互校验按你想要的触发时机配置但提交时一律再走一次整体 validate / validateFields不要依赖交互校验帮你拦截最终提交。两个框架都支持单独调用整体校验这才是真正的防线。2.2 required 和自定义 validator 写在一起必填提示会莫名消失我以前见过很多同事在 Element UI 里写自定义验证喜欢这样rules: { username: [ { required: true, validator: (rule, value, callback) { if (value admin) { callback(new Error(该用户名不可用)); } else { callback(); } }, trigger: blur } ] }看起来没毛病空值靠 required 拦非法值靠 validator 拦。但实际测试时会发现输入 admin 后报“该用户名不可用”然而把输入框清空再 blur必填提示根本不出现。原因是自定义 validator 里已经对空值走了callback()等于空值这条路径被你的 validator 直接放行了required 并没有想象中那么“优先执行”。正确做法是把必填和自定义校验拆成两条规则空值判定不要交给自定义 validator 自己决定rules: { username: [ { required: true, message: 请输入用户名, trigger: blur }, { validator: (rule, value, callback) { if (value admin) { callback(new Error(该用户名不可用)); } else { callback(); } }, trigger: blur } ] }Ant Design React 里同样的语义建议 required 单独规则自定义规则用 async validator。而且自定义 validator 里如果发现空值直接 return 或者 resolve不要把空值当成错误抛出去也不要把空值当成“校验成功但不处理”否则会再次绕开必填提示。2.3 数字 0、空字符串、null 在规则里的表现不一样这可能是全项目最隐蔽的坑。Element UI 里如果字段值是0required 默认不会认为它是空值这点是好的但现实里很多人提交前会把值做了一层转换比如把后端返回的0转成了于是 required 就拦住了。排查时以为校验规则写错其实是自己的数据处理把 0 变成了空串。Ant Design 系里如果 Form.Item 被销毁或者从未渲染但 name 对应的值是 undefinedrequired 有时候“看起来没生效”。说到底两个库的 required 都依赖 async-validator 对空值的判断标准undefined、null、空字符串算空数字 0 不算空。所以真正要注意的是不要让业务数据在传给表单字段时发生“0 变成 ”这种隐形类型转换。我自己常用的摸底手段是在提交前打印JSON.parse(JSON.stringify(this.form))或者在 antd 里console.log(form.getFieldsValue(true))把所有字段值和类型列出来大部分“为什么空值判不准”的问题当场就能看出来。排查表单验证问题第一步永远不是改规则而是看当前字段的真实值和类型。3. 动态校验、异步校验和跨字段校验别照着抄就完事3.1 场景切换后 rules 变了旧的错误提示还赖着不走中后台表单很常见新增和编辑共用一个页面某个字段在编辑模式下必填新增模式下选填。Element UI 里通常用 computed 根据 mode 生成 rulescomputed: { rules() { const codeRules []; if (this.mode edit) { codeRules.push({ required: true, message: 请输入编码, trigger: blur }); } return { code: codeRules }; } }规则本身会切换但你会遇到一个问题从编辑模式切到新增模式后大概率那行红字还挂在页面上因为校验错误状态不会因为 rules 变化自动清掉。正确操作是在 mode 切换后主动清一下this.$nextTick(() { this.$refs.form.clearValidate(code); });Ant Design React 这边动态规则的坑更多。如果某个 Form.Item 因为条件渲染被销毁它内部的错误状态也会消失但校验逻辑可能没有重新跑导致用户看到“表单已经是可提交状态”的假象。新版 antd 通过shouldUpdate和dependencies可以解决联动刷新问题但这些都是需要显式声明的不会因为 rules 变了就自动全量重校验。ant design vue 新版基本复刻了 React antd 的这套心智所以如果你是从 Element UI 过来动态规则这块建议主动多做一步凡是和“新增/编辑”“审核/退回”这类模式切换相关的表单切完模式后手动重置或清一次校验状态不要指望框架自动帮你同步。3.2 异步校验里最容易出现“校验永远不结束”Element UI 时代的异步校验是 callback 风格。我见过最经典的问题const validateName (rule, value, callback) { if (!value) { callback(); return; } checkNameUnique(value).then(() { callback(); }); };如果checkNameUnique接口异常promise 走了 catchcallback 永远不会被调用整个表单 validate 就卡在那里点击提交没有任何反应。更隐蔽的是某些分支里以为 return 就行实际 Element UI 的校验函数必须调用 callback纯粹 return 出去等于告诉 async-validator“这个字段校验没完成”。所以我的 Element UI 异步校验底线写法是这样const validateName (rule, value, callback) { if (!value) { callback(); return; } checkNameUnique(value) .then(() callback()) .catch(() callback(new Error(校验服务暂时不可用))); };注意 catch 里要 callback 一个具体错误不要静默吞掉否则用户端表现为点提交没反应比报错还难查。Ant Design React 的 validator 支持 Promise看起来更现代化rules{[ { validator: async (_, value) { if (!value) return; const exists await checkNameUnique(value); if (exists) { throw new Error(用户名已存在); } } } ]}这里有个容易忽略的点validator 内部如果 await 的接口抛异常错误信息会直接变成一行红字可能是“Network Error”这种不友好的内容。建议在 validator 内部捕获接口异常并转成业务上能看懂的提示或者当接口不可用时放行交给后端在提交时统一提示。校验的职责是拦住明确不合法或已存在的输入而不是承担接口异常兜底的职责。3.3 跨字段校验两个库的解法完全不是一个思路以“开始日期不能晚于结束日期”为例。Element UI 里常用的做法是在结束日期的校验规则里读取this.form.startDateconst validateEndDate (rule, value, callback) { if (!value || !this.form.startDate) { callback(); } else if (value this.form.startDate) { callback(new Error(结束日期不能早于开始日期)); } else { callback(); } };这个写法能跑但问题是当用户修改开始日期时结束日期的错误不会自动重新校验因为 ant 的组件只在你改结束日期本身的时候才触发。所以你必须 watch 开始日期的变化然后手动刷新结束日期的验证watch: { form.startDate() { this.$nextTick(() { this.$refs.form.validateField(endDate); }); } }React antd 提供了更合适的dependencies机制Form.Item namepassword dependencies{[confirmPassword]} /它能让某个字段的校验在依赖字段变化时自动重新执行避免“改了 AB 不刷新”的问题。ant design vue 新版也支持 dependencies。这也是两个库心智差异最明显的地方Element UI 倾向“你自己在外部管理数据流”Ant Design 系倾向“表单自己管理字段间的联系”。迁移时最容易出的问题就是把 Element UI 里 watch 手动刷的方式搬到 antd 后发现一方面依赖没声明另一方面手动刷的方式和 Ant Design 的内部状态管理互相打架。建议到了 Ant Design 体系就按它的 dependencies / getFieldValue validator 重新组织跨字段校验不要保留“watch 外部 form 手动 validate”这种老写法。4. resetFields 不是万能的它有一堆前置条件4.1 为什么有时候 resetFields 清不掉表单Element UI 的resetFields会把字段值重置为“初始值”这个初始值不是表单 model 当前的值而是 el-form-item 首次渲染时记录下来的值。这个机制带来一个经典问题弹窗里做编辑打开弹窗后用接口数据给 form 赋值handleEdit(row) { this.form { id: row.id, username: row.username }; this.dialogVisible true; }关掉弹窗时执行this.$refs.form.resetFields()结果下一次打开弹窗表单值不是空的而是上一行 row 的数据。原因就在于 form-item 首次渲染时初始值可能已经被记录成第一行数据了。Element UI 官方也提醒过resetFields 清的是“初始值”不是你后手动赋进去的值。处理方式有两种一种是弹窗内容用 v-if每次打开都重新渲染 el-form这样每次渲染都能以当前最新数据为初始值另一种是关弹窗时手动把 model 重置成默认对象再调 clearValidate别依赖 resetFields 一步到位。4.2 Ant Design 里 resetFields 和 setFieldsValue 的顺序坑React antd 里initialValues是 Form 的初始状态form.setFieldsValue()修改的是当前值。resetFields()的行为是回到 initialValues不是回到“上一次 setFieldsValue 的值”。所以你在数据请求回来后调用 setFieldsValue 填值再点取消或重置时调用 resetFields表单会直接回到 initialValues那个初始值可能是空对象也可能你还传了其他默认值。如果你希望回到的是“请求回来的值”就不能用 resetFields得手动再 setFieldsValue 一次。ant design vue 新版也踩同样的问题。我在团队里的约定是查询表单用 resetFields 重置到约定初始值编辑弹窗在打开时统一通过initialValues或“打开前 setFieldsValue 打开后 setFieldsValue”两个阶段去控制不在关闭时糊涂调用 resetFields。另外 Ant Design React 还有一个容易忽略的点如果 Form.Item 从未被挂载也就是字段从来没出现在页面上resetFields不会清掉 store 里可能残留的值字段销毁时默认会保留值除非设置preserve{false}。这个在你做 Tab 切换、步骤条场景时会突然冒出来表现为明明表单已经卸载了重新挂载后值还在。4.3 清错误状态不等于清值别把两者混在一起很多人以为重置表单等于调一下 resetFields 就万事大吉。实际上“清空值”和“清除红色错误提示”是两个维度。Element UI 里 clearValidate 只管错误提示不管值resetFields 虽然值回到初始值但如果你手动 set 过 Form model 里的某些字段错误状态不一定会跟着清除干净。Ant Design 这边如果只是想去掉某条错误而不重置值可以form.setFields([ { name: username, errors: [] } ]);这个 API 的用法很容易被忽略但实际排查时非常有用。有些场景你确定值已经合法只是想强行让某个字段的校验状态恢复正常setFields 就是最直接的出口。我把日常表单重置标准流程定成需要回到初始值先确定组件是否首次渲染、是否有接口请求值的影响再选择 resetFields 或手动赋值。只需要去掉错误提示用 clearValidateElement UI或 setFields 清空 errorsAnt Design。弹窗/抽屉关闭时在所有状态更新完成后放到 nextTick 里统一重置和清除避免组件还没销毁就操作表单。这套流程能挡住绝大多数“这个表单怎么清不干净”的灵异事件。5. 常见问题速查与最终建议5.1 八个高频问题的定位表为了让你在遇到问题时能快速找到方向我把这段时间踩过还有和同事排查过的问题整理成一张速查表表面现象可能原因优先排查方向required 没拦住空值自定义 validator 把空值回调放行了检查规则是否拆分validator 分支是否空值 return输入过程中疯狂报红trigger 配了 change且没有宽松校验改 validateTrigger / trigger 为 blur 或首次提交后再校验提交后点按钮没反应异步 validator 有分支没 callback / 没 resolve逐行检查所有分支是否结束校验弹窗第二次打开值不对resetFields 回的 initialValue 不是预期值用 v-if 重建或手动重置 model只改 A 字段B 字段联动校验不刷新没有声明字段依赖关系watch 没有触发 B 的校验用 dependenciesantd或在 watch 里 validateField错误提示位置错乱el-form-item prop 或 antd Form.Item name 和 v-model 字段不一致打印表单字段名逐一核对校验规则改了但界面没变化computed 缓存或 rules 引用没更新确认 rules 是否真正返回新数组必要时加一个版本号强制刷新0 被当成空值或空串被放行字段类型在提交前被转换提交前打印全量字段类型统一类型5.2 迁移与日常维护表单的几条硬经验第一校验规则尽量收敛到“纯函数”。Element UI 的 callback 风格和 antd 的 promise 风格虽然不一样但规则描述可以写成统一的 rule 对象只把两端最外层的执行形式做适配。这样未来如果再换组件库你只需替换外壳不用重写规则。第二别依赖表单框架去理解你的业务。就比如日期的开始和结束应该是业务层先算清楚再把值交给表单去展示和校验。很多跨字段校验问题本质上是业务约束没有提前收敛而是被迫散落在表单规则里才会出现“这里不触发、那里不刷新”的循环。第三提交前一定要有一次不依赖交互校验的“干净校验”。你在交互阶段怎么玩都行但真正的提交动作必须调用一次整体 validateFields / validate并且捕获所有失败字段的错误信息这样就算 trigger 或者 dependencies 配置有漏洞也不会导致脏数据被提交到后端。表单验证这个领域element-ui 和 ant design 底层都是 async-validator 这个事实容易让人产生“规则直接搬”的错觉实际两组壳子对状态的管理方式完全不同。我给团队做迁移时最常说的一句话就是不要复制代码要复制校验意图然后按当前组件库的机制重新实现它。你在 Element UI 里用 watch 手动刷新 B 字段的做法并没有错但把它原样塞进 antd 的表单体系里反而会破坏 antd 自己管理状态的节奏。先搞清楚手上的组件库把字段值、初始值和校验状态分别存在哪里再去调整规则这类问题基本都能快速收敛。
返回列表