本文系统介绍了使用 vee-validate 结合 zod 进行表单状态管理的解决方案,通过对比传统声明式表单验证的局限性(需手动声明响应式变量/校验规则),重点展示了该方案通过高阶组件与组合式 API 实现表单初始化的灵活性、利用 zod 强大的类型校验体系(支持复杂场景如文件名唯一性校验)、提供两种错误反馈机制(提交时全局校验与实时字段级校验),以及多步骤表单的字段值保留策略,最终实现更简洁高效的表单开发范式。
vee-validate 表单状态管理
为什么要使用 vee-validate?
我们先来看看常规情况下的表单,参考 NaiveUI,使用的是声明式规则和 kv 绑定
1 2 3 4 5 6 7 8 9 10 11
| <n-form ref="formRef" :model="model" :rules="rules"> <n-form-item path="age" label="年龄"> <n-input v-model:value="model.age" @keydown.enter.prevent /> </n-form-item> </n-form> <!-- 声明式规则 --> const rules: FormRules = { age: [ { required: true, validator(rule: FormItemRule, value: string) { if (!value) { return new Error('需要年龄') } else if (!/^\d*$/.test(value)) { return new Error('年龄应该为整数') } else if (Number(value) < 18) { return new Error('年龄应该超过十八岁') } return true }, trigger: ['input', 'blur'] } ], }
|
声明式规则的好处是易拓展,可以随时在对应的组件上修改,但是坏处是写起来非常不方便:
- 首先得声明响应式变量作为表单值 v-model 的绑定,有些时候还要声明未定义变量
1 2
| const formData = ref({ name: "", age: 18 }); const formData = ref({ description: undefined });
|
- 其次,对于校验,即使是判断是否最小,也得手写一部分规则
1 2 3 4 5
| validator(rule: FormItemRule, value: string) { if (!value) { return new Error('需要年龄') } }
|
那有没有什么轻度抽象,能解决上述问题的存在呢?
答案是有的,那就是 vee-validate + zod 的双重方案,不依赖组件原生的实现
vee-validate + zod
多种方式初始化表单和提供响应式值
- vee-validate 有多种方式初始化表单和提供响应式值,不再需要声明未定义情况
1 2 3 4 5 6 7 8 9 10 11 12
| const { values, defineField } = useForm({ validationSchema: toTypedSchema( z.object({ name: z.string().min(1).desribe("名称"), }) ), });
const [name] = defineField("name");
const { value: name } = useField<string>("name");
|
zod 自带校验规则
1 2 3
| const schema = z.object({ name: z.string().min(1).desribe("名称"), });
|
zod 有更强大的规则
- zod 有更强大的规则,比如对所有文件名称进行校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const schema = z.object({ fieldInfos:z.array({ file:z.instanceof(File), name:z.string() }) }).superRefine(({fieldInfos},context)=>({ const fileNames = fieldInfos.map(i=>i.name); fieldInfos.forEach((fieldInfo,index)=>{ const hasExist = fileNames.include(fieldInfo.name); if(hasExist){ context.addIssue({ code:z.ZodIssueCode.custom, message: '文件名已存在', path: ['fileInfos', index, 'name'], }) } })
}))
|
错误的 UI 响应
- 提交时报错,利用 handleSubmit 的第二个参数注入错误回调处理
示例:
1 2 3 4 5 6 7 8 9
| const submit = handleSubmit((values)=>{ yourApi.post({...values}) },({errors})=>{ console.log(errors) })
<form @submit={submit}>
|
- 提交前报错,借助 defineField 的第二个 props,或者借助 useField 的 error
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const [name, nameProps] = defineField("name", (state) => ({ props: { validationStatus: state.errors[0] ? ("error" as const) : undefined, feedback: state.errors[0], }, }));
const { value, errorMessage } = useField(() => props.name); <NFormItem :label="props.label" :showLabel="!!props.label" :required="props.required" :validationStatus="errorMessage ? 'error' : 'success'" :feedback="errorMessage" :showFeedback="!!errorMessage" > // 下面绑定value </NFormItem>
|
多步骤表单
如果组件是多步骤表单,可能涉及到多个状态的组件切换,这个时候需要设置 keepValuesOnUnmount 控制单个字段卸载时的保留
1 2 3 4 5 6 7 8 9 10 11 12
| <script setup>
const { handleSubmit } = useForm({ keepValuesOnUnmount: true, }); </script>
<template> <Step1 v-if="step === 1" /> <Step2 v-if="step === 2" /> </template>
|