Vue3 表单工程化:动态表单与验证策略
背景与问题界定 表单是 Web 应用中最常见也最容易被低估的组件。一个典型的中后台表单往往包含数十个字段、联动规则、异步校验和复杂嵌套结构。Vue3 生态中虽然有很多表单库(Element Plus、Ant Design Vue、VeeValidate),但在处理动态表单——字段数量、类型和验证规则根据运行时数据动态生成——时,每套方案都暴露出各自的局限性:Element Plus 的动态表单需要手动管理 v-for 和 prop 路径的映射;VeeValidate 的 Field 组件在动态增减场景下容易丢失验证状态;自研方案则在半途陷入模板膨胀和逻辑分散的困境。此外,跨组件表单校验(如多步表单中前一步验证结果影响后一步的展示逻辑)进一步加剧了复杂度。我们需要一个兼顾灵活性和可维护性的表单工程化方案。 目标拆解与工程约束 动态字段的定义驱动:表单项的定义(类型、校验规则、联动条件)必须集中在 JSON Schema 或 TypeScript 配置中,模板中只做渲染,不做逻辑判断。 验证状态与 UI 状态分离:字段的验证结果(valid/invalid/pending)不存储在表单数据对象中,也不与 v-model 绑定的值混淆,通过独立的验证层管理。 异步验证的竞态处理:字段的远程唯一性校验、邮箱验证等异步操作必须处理竞态——后发起的请求结果不应被先发起的过期结果覆盖。 跨组件验证上下文:多步骤表单中,步骤 A 的验证状态应在步骤 B 中可访问,但不能通过 props 透传造成组件耦合。 方案设计 我们从三个维度构建表单工程化方案:表单描述层(Schema Layer)、表单控制层(Controller Layer) 和 表单渲染层(Render Layer)。 表单描述层采用 JSON Schema-Inspired 的配置定义,每个字段的类型、默认值、验证规则和联动条件声明在一个配置对象中: interface FieldSchema { key: string type: 'input' | 'select' | 'checkbox' | 'date' | 'custom-component' label: string defaultValue?: any rules?: RuleItem[] visible?: (formValues: Record<string, any>) => boolean dependencies?: string[] // 依赖字段列表,用于优化联动计算 } 表单控制层是一个组合函数 useForm,接收字段配置数组和表单数据,返回控制方法和状态: function useForm<T extends Record<string, any>>(schema: FieldSchema[]) { const formData = reactive<Partial<T>>({}) as T const validationState = reactive<Record<string, ValidationResult>>({}) const pendingValidations = new Map<string, number>() // 用于竞态控制 // 初始化默认值 schema.forEach(field => { formData[field.key as keyof T] = field.defaultValue }) async function validateField(key: string): Promise<ValidationResult> { pendingValidations.set(key, (pendingValidations.get(key) || 0) + 1) const currentSeq = pendingValidations.get(key)! const result = await runRules(key, formData[key], schema.find(s => s.key === key)?.rules) // 竞态检测:只有当前 seq 与最新一致时才写入 if (pendingValidations.get(key) === currentSeq) { validationState[key] = result } return result } return { formData, validationState, validateField, validateAll: () => Promise.all(schema.map(s => validateField(s.key))) } } 表单渲染层则是纯模板工作——通过 v-for 遍历 schema,根据 field.type 动态选择渲染组件。渲染层只做两件事:读取配置渲染控件,绑定 v-model 到 formData。联动逻辑通过 computed 驱动,当依赖字段变化时自动重新计算 visible 属性,隐藏的字段不参与验证。 ...