资讯动态

Vue 3 + Vuelidate 自定义必传校验:从原理到实战

发布时间:2026/10/15 4:54:09 来源:尧图企业网站定制
1. 表单校验这件事为什么值得单独拎出来聊做前端表单校验最怕的不是写规则而是规则写完之后发现“这个字段的校验逻辑跟别的不一样”。比如一个提交按钮页面上有十几个字段其中大部分是选填但偏偏有几个是“必须传”的——不是“必填”而是“必须传值到后端”哪怕这个值是空字符串、是0、是false只要这个字段出现在请求体里后端就认。这种需求在配置类、审批类、数据同步类的页面里特别常见。我最近在做一个内部管理系统的配置模块时就碰到了这个场景。前端用的是Vue 3 Vuelidate这套组合Vuelidate本身很轻量基于Composition API写校验规则非常顺手但它的默认必填校验required只认“非空”对于“必须传”这种语义原生规则是不够用的。你需要自己写自定义校验器而且这个校验器还要跟Vuelidate的响应式体系无缝配合不能破坏原有的校验流程。这篇文章就是围绕“使用Vuelidate自定义必传项完成校验”这个主题把我在实际项目里踩过的坑、试过的方案、最终落地的代码结构完整拆一遍。如果你正在用Vuelidate做表单或者准备在Vue项目里引入表单校验方案这篇内容应该能帮你省掉不少查文档和试错的时间。文章会从方案选型讲到代码实现再到常见问题的排查尽量做到你照着就能用。2. 为什么选Vuelidate以及“必传项”到底特殊在哪2.1 Vuelidate在Vue表单校验里的定位Vuelidate的设计哲学跟Element Plus、Ant Design那种“表单组件自带校验”的方案不太一样。它不绑定任何UI库纯粹是一个校验逻辑层。你定义好数据模型和规则它帮你算出每个字段的校验状态至于错误信息怎么展示、什么时候触发校验完全由你自己控制。这种解耦带来的好处是灵活坏处是你得自己搭一套展示逻辑。在Vue 3的Composition API模式下Vuelidate提供了useVuelidate这个函数接收一个响应式对象和规则对象返回一个v$实例。这个实例上挂着$errors、$invalid、$touch等方法用起来很直观。我选它的原因很简单项目里已经有了一套自定义的表单布局组件不想因为引入UI库的校验体系而重构整个表单结构。Vuelidate可以只做校验不碰渲染正好合适。2.2 “必传”和“必填”的语义差异很多人会把“必传”和“必填”混为一谈但在接口层面这两个词代表完全不同的约束。必填指的是用户必须输入一个有效值不能为空必传指的是这个字段必须出现在请求体里至于值是什么后端可能不关心或者后端有自己的默认值处理逻辑。举个例子一个数据同步配置页面有一个字段叫syncMode它的可选值是full和incremental。如果用户不选前端可以不传这个字段后端会用默认的full。但如果用户选了incremental那么lastSyncTime这个字段就必须传哪怕用户没有手动填前端也要传一个空字符串或者null过去让后端知道“这次是增量同步但起始时间没指定”。这种情况下lastSyncTime的校验规则就不是“非空”而是“当syncMode为incremental时该字段必须存在于提交数据中”。Vuelidate的required规则只能判断值是否为空判断不了“字段是否存在”。所以我们需要自定义一个校验器它的逻辑是检查当前字段的key是否出现在待提交的对象里或者检查一个特定的“已传”标记。2.3 自定义校验器需要满足的条件在动手写之前我列了一下这个自定义校验器必须满足的几个条件。第一它要能跟Vuelidate的响应式系统配合也就是说当依赖的字段变化时校验结果要自动更新。第二它不能影响其他规则的执行比如一个字段同时有required和自定义的mustPass规则两个规则要能独立工作。第三错误信息的展示要能区分“未填写”和“未传值”这两种情况方便用户理解。第四它要能处理嵌套对象和数组场景因为实际项目里的表单数据结构往往不是扁平的。这四个条件看起来简单但真正写起来尤其是第三和第四条需要你对Vuelidate的$errors结构和响应式依赖收集有比较清楚的认识。下面我会逐个拆解。3. 自定义必传校验器的完整实现路径3.1 基础环境搭建与依赖安装假设你已经有一个Vue 3的项目用的是Vite或者Vue CLI都行。首先安装Vuelidate的核心包和内置规则包npm install vuelidate/core vuelidate/validators如果你用的是Vue 2那对应的包名和用法会不一样这里只讨论Vue 3的场景。安装完之后在你的组件里引入useVuelidate和需要的规则。我一般会把校验逻辑抽到一个单独的useFormValidation组合式函数里这样多个表单可以复用同一套校验基础设施。import { useVuelidate } from vuelidate/core import { required, helpers } from vuelidate/validatorshelpers这个对象里有一个withMessage方法用来给规则包裹自定义错误信息。后面自定义校验器也会用到它。3.2 定义数据模型与规则对象假设我们有一个配置表单数据模型是这样的const formData reactive({ syncMode: full, lastSyncTime: , dataSource: , targetTable: })规则对象需要跟数据模型的key一一对应const rules { syncMode: { required }, lastSyncTime: { mustPass: helpers.withMessage(增量同步时必须传此字段, mustPassValidator) }, dataSource: { required }, targetTable: { required } }注意这里的mustPassValidator就是我们即将自定义的校验器。它需要接收当前字段的值以及整个formData的引用才能判断“syncMode是否为incremental”这个条件。Vuelidate的自定义校验器默认只接收当前值要访问其他字段需要用helpers里的withParams或者直接在闭包里引用外部的响应式对象。3.3 编写mustPass自定义校验器最直接的写法是在组件内部定义一个函数利用闭包访问formDataconst mustPassValidator (value) { if (formData.syncMode ! incremental) { return true } return value ! undefined value ! null }这个逻辑的意思是如果不是增量模式这个字段随便校验通过如果是增量模式值不能是undefined或null但可以是空字符串。因为空字符串在接口层面是“传了但为空”符合“必传”的语义。但这里有一个坑Vuelidate的校验器在初始渲染时就会执行一次如果formData.syncMode的初始值是full那mustPassValidator返回true没问题。但当用户把syncMode改成incremental时lastSyncTime的校验状态不会自动更新因为Vuelidate没有追踪formData.syncMode这个依赖。你需要手动触发lastSyncTime的重新校验或者用Vuelidate提供的$validate方法在syncMode变化时调用。更优雅的做法是用helpers.withParams把依赖显式声明出来但Vuelidate的withParams主要是用来传参数的不是用来做依赖追踪的。真正靠谱的方案是在syncMode的watch里调用v$.lastSyncTime.$touch()或者v$.$validate()。我试过几种方式下面这个组合最稳定watch(() formData.syncMode, () { v$.value.lastSyncTime.$touch() })注意v$是useVuelidate返回的ref在Vue 3的setup里需要.value访问。3.4 处理嵌套对象与数组的必传校验实际项目里的表单很少是扁平的。比如一个订单表单里面有一个items数组每个item有productId和quantity其中productId是必传的。Vuelidate对嵌套结构的支持是通过规则对象的嵌套来实现的const rules { items: { $each: { productId: { required }, quantity: { required } } } }但如果productId的必传逻辑依赖于item里的另一个字段比如itemType那就需要在$each的规则里写自定义校验器。这时候闭包访问的是当前item而不是整个formData。你需要用helpers.withParams把item的索引或者整个item对象传进去const mustPassForItem (value, item) { if (item.itemType ! special) return true return value ! undefined value ! null } const rules { items: { $each: { productId: { mustPass: helpers.withParams( { item: (value, parent) parent }, mustPassForItem ) } } } }这段代码我调试了挺久才跑通withParams的第二个参数是一个函数接收当前值和父级对象返回你要传给校验器的参数。Vuelidate的文档里对这个用法的说明比较简略实际用的时候建议多打日志确认参数是否正确传入。4. 校验触发时机与错误展示的细节处理4.1 什么时候触发必传校验Vuelidate默认不会自动校验你需要手动调用$touch或者$validate。常见的触发时机有三个字段失焦、表单提交、依赖字段变化。对于必传校验来说依赖字段变化这个时机最关键。比如syncMode从full变成incrementallastSyncTime的必传状态就变了这时候应该立即触发校验并展示错误信息而不是等用户点到那个字段才提示。我的做法是在watch里同时做两件事调用$touch让字段变成“已触碰”状态然后调用$validate强制重新计算校验结果。$touch只改变$dirty状态不重新计算$validate会重新跑一遍所有规则。两个一起用才能保证UI上的错误提示及时更新。watch(() formData.syncMode, async () { v$.value.lastSyncTime.$touch() await v$.value.lastSyncTime.$validate() })注意$validate返回的是Promise在Vue 3的响应式系统里有时候需要await一下才能拿到最新的$invalid状态。4.2 错误信息的区分展示前面提到“未填写”和“未传值”是两种不同的错误。用户看到“此字段必填”和“增量同步时必须传此字段”感受是不一样的。前者是“你忘了填”后者是“这个场景下这个字段不能少”。在Vuelidate里每个规则可以有自己的错误信息通过helpers.withMessage包裹即可。但如果你在同一个字段上同时用了required和mustPass两个规则都失败时$errors数组里会有两条错误信息。展示的时候需要根据当前场景决定显示哪一条。我的做法是在模板里做一个简单的优先级判断div v-ifv$.lastSyncTime.$error span v-ifformData.syncMode incremental {{ v$.lastSyncTime.mustPass.$message }} /span span v-else {{ v$.lastSyncTime.required.$message }} /span /div这样用户看到的错误信息始终跟当前场景匹配不会出现“明明不是增量模式却提示必须传”的困惑。4.3 提交时的整体校验与拦截表单提交时需要调用v$.$validate()来触发所有字段的校验。如果返回false说明有字段没通过这时候要阻止提交并且把第一个出错的字段滚动到可视区域。Vuelidate的$errors数组里包含了所有出错的字段信息但它的结构是嵌套的扁平化处理需要写一个递归函数。我一般会写一个collectErrors工具函数const collectErrors (v$, path ) { const errors [] if (v$.$errors v$.$errors.length) { v$.$errors.forEach(err { errors.push({ path: path err.$property, message: err.$message }) }) } Object.keys(v$).forEach(key { if (key.startsWith($)) return if (v$[key] typeof v$[key] object) { errors.push(...collectErrors(v$[key], path key .)) } }) return errors }这个函数会遍历整个v$树把所有错误信息收集到一个扁平数组里。提交时如果errors.length 0就取第一个错误的path用document.querySelector找到对应的DOM元素并滚动过去。5. 常见问题与排查技巧实录5.1 自定义校验器不生效的几种原因我遇到过好几次“写了自定义校验器但校验结果永远是true”的情况。排查下来原因主要有三类。第一类是校验器函数没有正确注册到规则对象里比如把mustPass写成了mustPassValidator但规则对象里的key是mustPassVuelidate找不到对应的函数。第二类是校验器的返回值不是布尔值比如返回了undefined或者一个对象Vuelidate会把它当成false处理但不会报错导致你以为校验通过了。第三类是响应式依赖丢失校验器内部访问了外部的响应式变量但Vuelidate没有追踪到这个依赖导致变量变化时校验器没有重新执行。排查方法很简单在校验器函数的第一行加一个console.log看看它有没有被调用以及调用时的参数是什么。如果压根没被调用那就是注册问题如果调用了但结果不对那就是返回值或依赖问题。5.2 嵌套数组校验的性能问题当items数组的长度超过50时Vuelidate的$each校验会变得很慢因为每个item的每个规则都会独立执行一遍。如果每个item里还有自定义校验器性能下降会更明显。我的优化方案是给$each加一个$trackBy让Vuelidate知道用哪个字段来追踪item的身份避免不必要的重新校验const rules { items: { $each: { productId: { required }, quantity: { required } }, $trackBy: id } }另外如果某些校验规则只在提交时才需要执行可以用$lazy或者把规则拆成两组一组实时校验一组提交时校验。Vuelidate本身没有内置的lazy机制但你可以通过控制$touch的调用来实现类似效果。5.3 与UI库表单组件的兼容问题如果你的项目里用了Element Plus或者Ant Design Vue的表单组件它们的el-form-item或者a-form-item有自己的校验状态展示逻辑。把Vuelidate的校验结果接进去需要手动把$error和$message映射到组件的props上。我一般会写一个useFormItem的小hook把Vuelidate的字段状态转换成UI库需要的格式const useFormItem (v$, field) { return computed(() ({ validateStatus: v$.value[field].$error ? error : success, help: v$.value[field].$error ? v$.value[field].$errors[0].$message : })) }这样模板里就可以直接绑定v-binduseFormItem(v$, lastSyncTime)不用在每个字段上重复写判断逻辑。5.4 常见问题速查表问题现象可能原因排查方法解决方案自定义校验器永远返回true校验器未注册或返回值非布尔加console.log确认调用检查规则对象key与函数名一致依赖字段变化后校验不更新响应式依赖未追踪在watch里手动$touch用watch监听依赖字段并调用$validate嵌套数组校验卡顿$each逐项校验开销大减少数组长度测试加$trackBy拆分实时/提交校验错误信息不显示$error为false或$message为空打印v$对象查看状态确认withMessage包裹正确提交时校验通过但接口报错必传字段值为undefined检查提交数据序列化用JSON.stringify过滤undefined6. 一些实操心得与扩展思路自定义必传校验这件事表面上看只是加一个规则但实际做下来涉及响应式依赖管理、错误信息分层、性能优化、UI库适配等多个层面。我最大的体会是不要试图用一个校验器解决所有问题。把“必填”和“必传”拆成两个独立的规则各自负责各自的语义然后在展示层做优先级判断这样代码的可维护性会好很多。另外Vuelidate的$errors结构在嵌套场景下比较深建议在项目初期就封装一个统一的错误收集和展示组件避免每个表单页面都写一遍重复的模板代码。我现在的做法是写一个FormField组件接收v$实例和字段路径内部自动处理错误信息的展示和滚动定位业务组件只需要传数据和规则就行。这个方案后续还可以扩展的方向包括把校验规则配置化用JSON描述规则而不是写JavaScript函数接入异步校验比如检查某个编码是否已存在以及把校验状态持久化到本地避免用户刷新页面后丢失已填数据。这些等后面有实际需求了再逐步迭代。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑