资讯动态

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

发布时间:2026/10/14 20:01:53 来源:尧图企业网站定制
1. 为什么表单校验值得单独拎出来聊做前端表单校验这件事说简单也简单说麻烦是真麻烦。简单在于一个required属性就能挡住大部分空值提交麻烦在于真实业务里的必填往往不是非空两个字能概括的。比如一个上传组件用户选了文件但没点上传字段值是个空数组required判定为通过可业务上这就是没填再比如一个多选标签输入用户输入了内容但没回车确认值还是空的界面上看着填了提交时却报错用户一脸懵。我最近在一个后台管理系统里就踩了这类坑。项目用的是 Vue 3 Vuelidate 这套组合Vuelidate 本身是个很轻量的校验库基于组合式 API 写起来很顺手但它的内置校验器required、minLength、email这些只覆盖了最基础的场景。一旦遇到自定义必传项——也就是那些需要根据业务规则判断是否算已填写的字段——内置校验器就不够用了必须自己写自定义校验函数。这篇内容就是把我这段时间用 Vuelidate 做自定义必传校验的完整思路、代码实现、踩过的坑和排查技巧整理出来。适合正在用 Vue 3 Vuelidate 做表单的开发者也适合那些被看起来填了其实没填这类问题折磨过的朋友。我会从设计思路讲到具体实现再到常见问题的排查尽量把每个为什么这么写都讲清楚让你看完能直接抄作业也能理解背后的逻辑遇到变体场景自己能改。核心关键词会围绕Vuelidate 自定义校验、必传项校验、Vue 3 表单验证、自定义校验函数、表单校验避坑这几个方向展开这些都是实际开发中高频出现的搜索需求。2. Vuelidate 的校验机制与自定义校验的切入点2.1 Vuelidate 的核心工作方式要写好自定义校验得先搞清楚 Vuelidate 是怎么运转的。它和那些绑定在组件上自动校验的库不太一样Vuelidate 的核心思路是你把校验规则定义成一个对象这个对象的结构和你表单数据的结构一一对应然后 Vuelidate 帮你追踪每个字段的校验状态。用组合式 API 的写法大概是这样import { useVuelidate } from vuelidate/core import { required, minLength } from vuelidate/validators import { reactive } from vue const formData reactive({ username: , tags: [] }) const rules { username: { required, minLength: minLength(3) }, tags: { required } } const v$ useVuelidate(rules, formData)这里rules对象的 key 必须和formData的 key 对应Vuelidate 会遍历这些规则对每个字段执行校验函数。校验函数返回true表示通过返回false表示失败。这是最基础的认知但很多人卡在自定义校验上恰恰是因为没意识到自定义校验函数本质上就是一个接收当前值、返回布尔值或 Promise的普通函数没什么神秘的。2.2 内置校验器为什么不够用Vuelidate 内置的required校验器源码逻辑大致是判断值是否为null、undefined、空字符串或者空数组。听起来挺全的但实际业务里必填的定义千差万别文件上传场景字段值是文件对象数组用户选了文件但没触发上传数组里可能有临时对象required判定通过但业务上文件还没真正上传成功不算填了。标签输入场景用户输入了文字但没按回车确认绑定值还是空数组界面上看着有内容实际值是空的。级联选择场景用户选了省但没选市字段值是个不完整的对象required只看对象非空判定通过但业务要求必须选到最末级。富文本场景编辑器里只有空标签pbr/p字符串非空required通过但实际没有任何有效内容。这些场景的共同点是是否已填写需要业务逻辑来判断而不是简单的非空判断。这就是自定义校验函数要解决的问题。2.3 自定义校验函数的两种写法Vuelidate 支持两种自定义校验的写法各有适用场景。第一种是内联箭头函数适合逻辑简单的场景const rules { tags: { required: (value) Array.isArray(value) value.length 0 } }第二种是独立函数适合逻辑复杂、需要复用或需要传参的场景const mustHaveAtLeastOne (minCount 1) { return (value) { if (!Array.isArray(value)) return false return value.length minCount } } const rules { tags: { required: mustHaveAtLeastOne(1) } }我个人的习惯是只要校验逻辑超过两行或者需要传参数就抽成独立函数。这样代码可读性好测试也方便而且多个表单可以复用同一套校验逻辑。内联写法只适合那种一眼能看懂的简单判断。注意自定义校验函数里不要用thisVuelidate 的校验函数是纯函数式的this指向不确定容易出问题。需要依赖外部状态时通过闭包或者参数传入。3. 自定义必传校验的完整实现方案3.1 场景定义与数据结构设计我拿一个实际做过的场景来演示一个项目发布表单包含三个必填字段——项目名称普通文本、技术标签多选标签输入、附件文件上传。其中技术标签和附件就是典型的自定义必传场景。先定义数据结构import { reactive } from vue const formData reactive({ projectName: , techTags: [], // 标签数组每项是字符串 attachments: [] // 附件数组每项是 { name, url, status } })这里techTags和attachments都是数组但必填的含义不同techTags要求至少有一个标签attachments要求至少有一个上传成功的附件status success。这就是为什么内置required不够用——它只看数组长度不看内容状态。3.2 自定义校验函数的编写针对这两个字段我写了两个自定义校验函数// 校验标签至少一个非空字符串标签 const requiredTags (value) { if (!Array.isArray(value)) return false return value.some(tag typeof tag string tag.trim() ! ) } // 校验附件至少一个上传成功的附件 const requiredAttachments (value) { if (!Array.isArray(value)) return false return value.some(item item item.status success item.url) }注意requiredTags里用了some而不是length 0因为标签数组里可能存在空字符串用户误操作留下的用some加trim判断能过滤掉这种脏数据。requiredAttachments则明确要求status success且有url确保附件是真的上传成功了而不是还在上传中或者上传失败。把这两个函数接入规则import { useVuelidate } from vuelidate/core import { required, minLength } from vuelidate/validators const rules { projectName: { required, minLength: minLength(2) }, techTags: { required: requiredTags }, attachments: { required: requiredAttachments } } const v$ useVuelidate(rules, formData)这里有个细节techTags和attachments的规则 key 都叫required但值是自定义函数。Vuelidate 不关心 key 叫什么它只关心值是不是函数。所以你可以把自定义校验挂在required这个 key 下也可以叫customRequired效果一样。我习惯挂在required下因为语义清晰而且模板里用v$.techTags.required.$invalid判断时更直观。3.3 校验触发时机的控制Vuelidate 默认的校验触发时机是值变化时校验但实际体验中用户刚打开表单什么都没填所有字段就飘红报错体验很差。所以需要控制触发时机。Vuelidate 提供了$touch()方法配合$invalid和$dirty使用// 提交时统一触发校验 const handleSubmit async () { const isValid await v$.value.$validate() if (!isValid) { // 校验失败滚动到第一个错误字段 return } // 校验通过提交数据 }模板里判断错误显示div classform-item label技术标签/label TagInput v-modelformData.techTags blurv$.techTags.$touch() / span v-ifv$.techTags.required.$invalid v$.techTags.$dirty classerror 请至少添加一个技术标签 /span /div关键点是v$.techTags.$dirty——只有字段被触碰过用户操作过才显示错误避免初始状态就报错。$touch()在blur事件里调用用户离开字段时才标记为已触碰。实操心得对于自定义组件比如标签输入、文件上传blur事件不一定能正常冒泡需要在组件内部emit(blur)父组件再调用$touch()。我踩过这个坑标签组件里输入框的 blur 被组件根元素拦截了导致错误提示一直不显示排查了半天。3.4 异步校验的处理有些必传校验需要异步判断比如附件必须上传完成这个状态可能需要调接口确认。Vuelidate 支持异步校验函数返回 Promise 即可const requiredAttachmentsAsync async (value) { if (!Array.isArray(value) || value.length 0) return false // 模拟调接口确认附件状态 const results await Promise.all( value.map(item checkAttachmentStatus(item.id)) ) return results.some(status status success) }异步校验要注意两点一是$validate()返回的是 Promise提交时要await二是异步校验期间要有 loading 状态避免用户重复提交。我一般会在提交按钮上加:loadingv$.$pendingVuelidate 的$pending属性会反映是否有校验正在进行。4. 实操过程中的关键细节与避坑4.1 自定义校验函数的返回值陷阱Vuelidate 对校验函数的返回值有明确要求返回false表示校验失败返回true表示通过返回其他值包括undefined会被当作失败处理。这个规则听起来简单但实际写的时候很容易踩坑。比如下面这个函数// 错误写法 const requiredTags (value) { if (!Array.isArray(value)) return // 返回 undefined被当作失败 value.length 0 // 没有 return返回 undefined }这个函数有两个问题一是!Array.isArray(value)时直接return返回undefinedVuelidate 会判定失败虽然结果碰巧对了但逻辑不清晰二是最后一行没有return函数返回undefined永远判定失败。正确写法必须显式返回布尔值// 正确写法 const requiredTags (value) { if (!Array.isArray(value)) return false return value.length 0 }注意如果你的校验函数里有if分支确保每个分支都有明确的return true或return false不要依赖隐式的undefined。我见过有人写if (condition) return true然后忘了写return false导致校验永远通过线上出了数据问题。4.2 数组类型字段的响应式问题Vue 3 的响应式系统对数组的处理有个细节直接修改数组的length或者通过索引赋值在某些情况下不会触发 Vuelidate 的重新校验。比如// 可能不触发校验 formData.techTags[0] Vue formData.techTags.length 0 // 推荐写法确保触发 formData.techTags [Vue] formData.techTags []Vuelidate 内部依赖 Vue 的响应式追踪数组的变异方法push、splice、pop等能正常触发但直接赋值索引或修改length在 Vue 3 里虽然也是响应式的但和 Vuelidate 的追踪机制配合时偶尔会有延迟。我的经验是对数组字段做整体替换而不是局部修改这样最稳妥。4.3 多个自定义校验的优先级一个字段可能挂多个校验规则比如techTags既要必填又要限制最多 5 个const rules { techTags: { required: requiredTags, maxTags: (value) !Array.isArray(value) || value.length 5 } }Vuelidate 会依次执行所有规则只要有一个失败$invalid就是true。但错误提示的显示顺序需要注意模板里如果同时判断v$.techTags.required.$invalid和v$.techTags.maxTags.$invalid两个错误可能同时显示用户会困惑。我的做法是按优先级显示只显示第一个错误span v-ifv$.techTags.required.$invalid v$.techTags.$dirty classerror 请至少添加一个技术标签 /span span v-else-ifv$.techTags.maxTags.$invalid v$.techTags.$dirty classerror 技术标签最多 5 个 /span用v-else-if确保同一时间只显示一个错误用户体验更清晰。4.4 表单重置时的校验状态清理表单提交成功后通常要重置但 Vuelidate 的校验状态不会自动清理需要手动调$reset()const handleReset () { formData.projectName formData.techTags [] formData.attachments [] v$.value.$reset() }$reset()会把所有字段的$dirty、$touched、$invalid等状态重置为初始值。如果不调用重置后字段可能还显示着之前的错误提示用户会以为没重置成功。这个坑我在项目里踩过一次测试同学提了个重置按钮无效的 bug排查后发现是校验状态没清。5. 常见问题排查与速查表5.1 校验不生效的排查思路自定义校验不生效通常有这几个原因按排查顺序列一下排查项检查方法常见原因规则 key 是否匹配检查rules的 key 和formData的 key 是否完全一致拼写错误、大小写不一致校验函数返回值在函数里console.log返回值返回了undefined或非布尔值响应式是否丢失检查formData是否用reactive或ref包裹直接用了普通对象触发时机检查是否调用了$touch()或$validate()只依赖自动校验但字段没触发异步校验检查是否await了$validate()异步函数没等待直接读结果我遇到最多的是第一条和第二条。规则 key 拼写错误这种低级问题在字段多的时候特别容易发生建议用 TypeScript 或者把 key 抽成常量减少手误。5.2 自定义组件校验的通信问题自定义组件标签输入、文件上传、富文本做校验时最大的问题是组件内部的值变化没有正确同步到formData。比如标签组件用户输入后按回车组件内部维护了一个数组但没有emit更新到父组件的formData.techTags导致 Vuelidate 拿到的还是旧值。解决方案是确保组件用v-model正确双向绑定TagInput v-modelformData.techTags /组件内部const props defineProps([modelValue]) const emit defineEmits([update:modelValue]) const addTag (tag) { const newTags [...props.modelValue, tag] emit(update:modelValue, newTags) // 必须 emit否则父组件值不更新 }关键点是每次修改都 emit 新数组而不是直接修改props.modelValue。直接修改 props 是 Vue 的反模式而且不会触发父组件的响应式更新Vuelidate 自然也不会重新校验。5.3 性能问题的排查表单字段多、校验逻辑复杂时可能会遇到输入卡顿。Vuelidate 默认每次值变化都校验如果校验函数里有复杂计算或异步请求就会拖慢输入响应。优化手段有两个一是防抖二是延迟校验。防抖可以用 lodash 的debounce包裹校验函数但更推荐用 Vuelidate 的$lazy或者手动控制触发时机// 只在 blur 时校验而不是每次输入 const rules { projectName: { required, minLength: minLength(2) } } // 模板里不绑定 input 事件只绑定 blurVuelidate 本身没有内置防抖但可以通过$touch()的调用时机来控制。我的做法是普通文本字段在blur时校验数组类字段在值变化时校验因为数组变化频率低这样兼顾体验和性能。5.4 错误提示的国际化与动态文案自定义校验的错误提示往往需要动态生成比如至少添加 N 个标签N 是变量。Vuelidate 本身不管理错误文案文案由你在模板里写。如果项目有多语言需求建议把文案抽成配置const errorMessages { techTags: { required: 请至少添加一个技术标签, maxTags: (max) 技术标签最多 ${max} 个 } }模板里引用span v-ifv$.techTags.required.$invalid v$.techTags.$dirty {{ errorMessages.techTags.required }} /span这样文案集中管理改起来方便也便于接入国际化方案。6. 几个提升开发体验的实用技巧6.1 把校验规则抽成独立模块表单字段多了之后rules对象会变得很长混在组件里可读性差。我的做法是把校验规则抽到单独的文件// validators/projectForm.js import { required, minLength } from vuelidate/validators export const requiredTags (value) { if (!Array.isArray(value)) return false return value.some(tag typeof tag string tag.trim() ! ) } export const requiredAttachments (value) { if (!Array.isArray(value)) return false return value.some(item item item.status success item.url) } export const projectFormRules { projectName: { required, minLength: minLength(2) }, techTags: { required: requiredTags }, attachments: { required: requiredAttachments } }组件里直接引入import { projectFormRules } from /validators/projectForm const v$ useVuelidate(projectFormRules, formData)这样校验逻辑和组件解耦方便单元测试也方便其他表单复用。6.2 用 TypeScript 约束校验函数类型如果项目用 TypeScript可以给校验函数加类型约束避免返回值类型错误type Validator (value: unknown) boolean | Promiseboolean export const requiredTags: Validator (value) { if (!Array.isArray(value)) return false return value.some(tag typeof tag string tag.trim() ! ) }这样写的时候如果忘了return或者返回了非布尔值TypeScript 会直接报错把问题挡在编译期。6.3 校验函数的单元测试自定义校验函数是纯函数非常适合单元测试。用 Vitest 或 Jest 写几个用例import { describe, it, expect } from vitest import { requiredTags, requiredAttachments } from /validators/projectForm describe(requiredTags, () { it(空数组返回 false, () { expect(requiredTags([])).toBe(false) }) it(有非空标签返回 true, () { expect(requiredTags([Vue])).toBe(true) }) it(只有空字符串标签返回 false, () { expect(requiredTags([, ])).toBe(false) }) it(非数组返回 false, () { expect(requiredTags(null)).toBe(false) }) })这几个用例覆盖了正常、边界、异常三种情况跑一遍就能确认校验逻辑没问题。我一般在写完校验函数后立刻补测试比在页面上手动点来点去高效得多。6.4 调试 Vuelidate 状态的小技巧Vuelidate 的状态对象v$结构比较深调试时不容易看清。可以在开发环境把它挂到window上if (import.meta.env.DEV) { window.__v$ v$ }然后在浏览器控制台里直接__v$.techTags.required.$invalid查看状态比在 Vue DevTools 里一层层点开快得多。这个技巧在排查为什么校验没触发时特别有用能快速定位是值没更新还是规则没匹配。7. 关于这套方案的一些个人体会用 Vuelidate 做自定义必传校验核心其实就三件事写对校验函数、接对响应式数据、控对触发时机。校验函数要显式返回布尔值别依赖隐式undefined响应式数据要用reactive或ref包裹数组操作尽量整体替换触发时机用$touch()和$dirty配合避免初始状态就报错。我踩过最深的坑是自定义组件的值同步问题。标签组件内部维护了数组但忘了emit更新导致 Vuelidate 拿到的永远是空数组校验一直失败但界面上标签明明显示着。排查了半天才意识到是组件通信的问题不是校验逻辑的问题。所以如果你用自定义组件做表单字段第一件事就是确认v-model双向绑定是通的值能正确同步到formData。另外校验规则抽成独立模块这个习惯强烈建议养成。一开始可能觉得麻烦但字段一多、表单一变你就会感谢自己当初抽出来了。校验逻辑和组件解耦后改规则不用动组件测试也好写复用也方便。最后分享一个小技巧如果某个字段的校验逻辑特别复杂与其硬写成一个函数不如拆成多个小函数组合。比如附件必传可以拆成数组非空和至少一个上传成功两个判断分别测试最后组合。这样每个函数都简单出问题也好定位。校验这东西越简单越可靠别为了炫技把逻辑写得太绕。

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

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

免费获取报价 →
↑