Files
nl-admin-view/docs/src/guide/in-depth/zod-v4-form-migration.md
dream-weave 28757fb9c8 refactor(@vben-core/form-ui): migrate to Zod 4 and TanStack Form (#8176)
* build(@vben-core/form-ui): update form validation dependencies

* refactor(@vben-core/form-ui): replace vee-validate with TanStack Form

* test(@vben-core/form-ui): cover TanStack Form migration

* docs(@vben/docs): document Zod 4 form migration

* refactor(project): update form adapters

* refactor(project): migrate application form consumers

* refactor(@vben/playground): migrate form examples

* docs(@vben/docs): fix validate return type and required rule empty check in form docs

* fix(@vben-core/form-ui): resolve oxlint and eslint errors

- rewrite nested ternary expressions as if/else in form-field-array.vue and form-runtime.ts
- sort @tanstack/vue-form before @vben-core/composables in package.json

* chore: fix merge artifacts and formatting

- move dependencies before devDependencies in root package.json
- update preferences snapshot for widget positioning fields
- format form-array demo README

* refactor(@vben-core/form-ui): optimize form runtime and value flow

- add fine-grained field selectors and atomic dependency resolution
- expose raw and formatted value snapshots with focused regression coverage

* fix(@vben/layouts): dispose sortable instance on unmount

* docs(@vben/docs): document form runtime API changes

- list added, changed, removed, and deprecated form APIs
- document atomic dependencies and raw versus formatted values
2026-07-22 19:47:07 +08:00

14 KiB
Raw Blame History

outline
outline
deep

Zod 4 与 TanStack Form 迁移指南

本次迁移将表单校验 schema 从 Zod 3 升级到 Zod 4并将内部表单引擎从 vee-validate 替换为 TanStack Form。迁移目标是保持 Vben 业务 API 稳定,同时移除业务代码对具体表单引擎的耦合。

依赖变化

类型 迁移前 迁移后
Schema zod@^3.25.76 zod@^4.4.3
默认值 zod-defaults@0.1.3 zod-defaults@^0.2.3
表单引擎 vee-validate@^4.15.1 @tanstack/vue-form@^1.33.2
Zod 适配器 @vee-validate/zod@^4.15.1 不再需要TanStack Form 支持 Standard Schema

迁移后源码、package manifest 和锁文件中都不应再依赖 vee-validate@vee-validate/zod

上层兼容范围

以下 Vben API 保持兼容:

  • useVbenForm(options) 仍返回 [Form, formApi]
  • FormApi 的值、校验、提交、重置、schema 更新和组件引用能力
  • FormSchemafieldNamecomponentcomponentPropsrulesdependenciesdefaultValuevalueFormat 和数组字段结构
  • dependencies.triggerFields 与回调参数
  • 组件适配器和 z 重导出路径
  • 自定义 slot 中原有的 componentField 绑定对象

formApi.form 现在是库无关的 FormContextApi。它提供 values、errors、meta、字段读写、验证、提交、重置和数组操作但不再暴露 vee FormContext 或原始 TanStack 实例。

新代码使用 resetsubmitvalidateAndSubmitclearValidation。旧的 resetFormsubmitFormvalidateAndSubmitFormresetValidate 仍会委托给新实现,并通过 @deprecated 与开发环境一次性 warning 提示迁移;生产环境不输出 warning。

本轮 Form UI API 变更

新增 API

API 类型/位置 说明
dependencies.resolve(context) FormItemDependenciesResolve 根据声明的 triggerFields 一次计算完整动态 patch并原子更新字段状态。context 包含只读 valuesactionscontroller 和数组行感知的 schema
useValues() FormContextApi 订阅完整表单值。仅在确实需要整表响应式值时使用。
useFieldValue(fieldName) FormContextApi 订阅单字段值,避免无关字段变化触发组件更新。
useFieldValues(fieldNames) FormContextApi 订阅一组字段值,主要用于声明式依赖计算。
useFieldError(fieldName) FormContextApi 订阅单字段错误,不再依赖全量错误对象。
getRawValues() FormApi 返回未执行字段映射与 valueFormat 的独立原始值快照。
formatValues(rawValues) FormApi 对指定原始值执行统一格式化流水线。
getValueSnapshot() FormApi 同时返回 { rawValues, values },其中 values 为格式化结果。
asyncDebounceMs FormFieldOptions 设置 TanStack Field 异步校验防抖时间。
changeEventFallback FormCommonConfig / adapter config 为只发送 change、不发送 update:* 的旧组件启用事件回退,默认 false

dependencies.resolve 可以返回 ifshowdisabledrequiredrulescomponentPropshelprenderComponentContent。未返回 rules 时继续使用静态规则;显式返回 rules: null 时关闭静态规则。

变更的 API

API 迁移前 迁移后
提交回调 handleSubmit(values) handleSubmit(values, rawValues);首参为格式化值,次参为同一次提交对应的只读原始快照。旧单参数函数仍可直接使用。
值变化回调 handleValuesChange(values, fieldsChanged) handleValuesChange(rawValues, fieldsChanged, getFormattedValues);第三个参数为惰性格式化函数,不调用时不产生深拷贝和转换开销。
字段校验触发 四个 validateOn* 布尔项 validateOn?: readonly ('blur' | 'change')[]submit 始终校验。
change 事件兼容 disabledOnChangeListener: false 表示启用 changeEventFallback: true 表示启用,改为正向语义。
顶层动态渲染回调 componentProps(values, actions, ctx)help(values, actions, ctx)renderComponentContent(values, actions, ctx) 仅接收轻量 FormSchemaContext。依赖表单值的动态逻辑迁移到 dependencies.resolve
validateAndSubmit() 自行调用底层校验并重复实现错误滚动,提交阶段可能再次校验 委托统一 validate() 与共享提交逻辑,无效时不提交,错误滚动只有一个实现。
getValues() 隐式完成所有字段转换 语义保持为“返回格式化值”;需要原始值时显式使用 getRawValues()

删除的 API

已删除 API 替代方式
FormValidationOptions validate()validateField(fieldName) 不再接收 options。
force / silent / validated-only validation mode 这些 vee mode 在 TanStack runtime 中没有对应语义,直接删除。
validateOnBlur / validateOnChange / validateOnInput / validateOnModelUpdate 使用 formFieldProps.validateOninput 与 model update 统一归入 change
disabledOnChangeListener 使用正向语义的 changeEventFallback
disabledOnInputListener 不再自动绑定 input listener确需自定义 input 处理时在 componentProps.onInput 中显式提供。
顶层 schema 渲染函数中的 values/actions 参数 使用 FormSchemaContext;值相关联动使用 dependencies.resolve

已弃用但保留兼容

  • dependencies.if/show/disabled/required/rules/componentProps/trigger 本轮仍完整兼容,但均已标记 @deprecated。开发环境首次使用时警告一次;新旧语法绕过类型同时存在时以 resolve 为准。
  • resetFormsubmitFormresetValidatevalidateAndSubmitForm 继续转发到新方法。
  • FormActions 继续作为 FormContextApi 的弃用类型别名。
  • setupVbenForm({ defineRules }) 继续兼容;与 rules 同名时新 API 优先。
  • z 重导出、componentField slot 和 emptyStateValue 保持不变。

非 API 行为调整

  • 字段组件改用细粒度 value/error selector全量错误聚合退出普通输入热路径。
  • async validator 通过 Vben generation 丢弃过期 Promise不读取 TanStack 私有 AbortController 或 meta 字段。
  • dependencies 新旧语法共用一个原子执行器,异步旧结果不会覆盖新状态。
  • 值格式化按 array-to-string、时间范围映射、schema valueFormat 的固定顺序执行,并且每次格式化只深拷贝一次。

值类型与插槽类型

应用 adapter 保留 UI 组件类型,只把业务值类型作为泛型暴露:

interface AccountFormValues {
  email: string;
  nickname: string;
}

const [Form, formApi] = useVbenForm<AccountFormValues>({
  handleSubmit(values) {
    return addAccount(values);
  },
  schema: [
    { component: 'Input', fieldName: 'email' },
    { component: 'Input', fieldName: 'nickname' },
  ],
});

TValues 会传递给 VbenFormPropsFormSchemaFormApiFormContextApi、值读写 API、提交/变化回调、selector 和 schema 动态回调。返回的 Form 组件同时提供 typed slots已知字段插槽的 field.state.valuecomponentField.modelValue 使用对应字段类型,并额外提供完整 values 与同型 formApi;默认和操作插槽也提供 values/formApi。未声明 TValues 的旧表单仍允许任意字段插槽并回退为宽泛类型。

新旧规则注册 API

新代码使用 rules

setupVbenForm({
  rules: {
    required(value, _params, context) {
      const isEmpty =
        value === undefined ||
        value === null ||
        value === '' ||
        (Array.isArray(value) && value.length === 0);
      return isEmpty ? `${context.label} is required` : true;
    },
  },
});

旧的 defineRules 仍会转发到同一个规则注册表:

setupVbenForm({
  defineRules: {
    required: legacyRequiredRule,
  },
});

使用旧入口时,开发环境针对该弃用项只输出一次警告;生产环境不输出。若同时提供 rulesdefineRules 的同名规则,rules 优先。FormActions 类型保留为 FormContextApi 的弃用别名,类型别名本身无法触发运行时警告,编辑器会通过 @deprecated 提示迁移。

使用迁移工具

建议在干净的 Git 工作树中按项目 tsconfig 执行固定版本工具:

npx --yes zod-v3-to-v4@1.21.3 path/to/tsconfig.json

工具会原地修改 .ts.tsx.vue 文件,没有 dry-run 模式。执行后必须检查 git diff

工具只能可靠识别直接从 zod 导入的调用。通过 @vben/common-ui 或应用 adapter 间接取得 z 的 schema 需要人工审计,尤其是构造器错误参数、字符串格式和动态 refine 参数。

Zod 4 代码变更

错误参数

构造器中的 required_errorinvalid_type_error 合并为 error

const count = z.number({
  error: (issue) =>
    issue.input === undefined ? 'Count is required' : 'Count must be a number',
});

refinement 继续支持字符串或对象参数。需要根据输入动态生成消息时,使用 error(issue),不再传入返回 params 的第二个函数。

字符串格式

优先使用顶层格式 API

z.email('Invalid email');
z.url('Invalid URL');
z.uuid('Invalid UUID');

旧的 z.string().email() 等形式不应继续新增。

错误列表

ZodError 使用 issues

const result = schema.safeParse(value);
if (!result.success) {
  console.log(result.error.issues);
}

不要读取已移除的 .errors

默认值与 optional

Zod 4 的 default 在输入为 undefined 时可以直接返回默认值。.default().optional() 的结果必须按实际 parse 语义复核,而不是通过类型名称猜测。

Vben 表单按以下优先级生成初值:

  1. schema 中显式 defaultValue
  2. Zod schema 中的 .default()
  3. zod-defaults 生成的对象、intersection 和基础空值
  4. Vben 组件约定的空字符串、空数组或空状态值

必填标记以 schema 是否接受 undefined 为准。

包装器、refine 与 transform

不要读取 _def_zod.deftypeName。公共包装器使用 .unwrap()Zod 4 的 transform/pipe 使用公开的输入 schema。intersection 的默认值交给支持 Zod 4 的 zod-defaults 处理。

TanStack Form 使用 Standard Schema 校验时不会自动把 transform/coerce 的输出写回当前表单 state。提交 payload 需要转换时,继续使用 valueFormat;如果必须提交 schema transform 后的结果,应在提交边界显式调用 parseAsync

其他需要复核的 API

  • z.record() 需要明确 key schema 与 value schema
  • z.enum() 已覆盖原 nativeEnum 用法
  • number 的 int、Infinity 和 finite 约束需按 Zod 4 语义复核
  • object 的 strict、merge、unknown keys 行为需要通过测试确认
  • intersection 合并冲突现在可能直接抛出错误
  • coerce schema 的 input 类型默认为 unknown
  • ZodEffectsZodTypeAnyAnyZodObject 等 Zod 3 类型不应继续使用

表单引擎行为

验证触发

formFieldProps.validateOn 接收 blurchange 数组,默认两者都启用;所有字段仍会在 submit 时验证。asyncDebounceMs 映射到 TanStack Field 的异步防抖配置。原 vee 风格的四个 validateOn* 布尔项和 force/silent/validated-only mode 已删除。

错误与可访问性

shadcn form primitive 使用 Vben 自有字段上下文,不再注入 vee 的 FieldContextKeyFormLabelFormControlFormDescriptionFormMessage 继续维护:

  • for 与 control id
  • aria-invalid
  • aria-describedby
  • touched、dirty、valid 和错误消息

clearValidation(fieldNames?) 会递增 Vben validator generation 并清空公开错误状态,不依赖 TanStack 私有 AbortController。异步 Promise 即使随后完成也会因代次过期而被丢弃;省略字段参数时会处理全部已注册或已有错误的字段。

依赖与数组

dependencies.resolve(context) 是推荐语法:一次求值并原子提交完整动态 patch过期异步结果整体丢弃。旧的 if/show/disabled/required/rules/componentProps/trigger 语法仍兼容,但已标记为 @deprecated 并在开发环境首次使用时提示迁移;内部仍归一到同一个执行器。两种语法都只根据 triggerFields 重算,无关字段变化不会执行回调。

handleValuesChange(rawValues, fieldsChanged, getFormattedValues) 接收未格式化的只读当前值,第三个参数仅在调用时执行格式化。getRawValues()getValues() 分别只生成原始或格式化快照;需要同时比较时使用 getValueSnapshot()。提交回调通过 handleSubmit(values, rawValues) 同时取得两种结构。格式化流水线只深拷贝一次,并按 array-to-string、时间范围映射、schema valueFormat 的顺序执行。数组字段继续使用 TanStack push/remove 操作和稳定行身份。

测试与验收

迁移至少需要覆盖以下层级:

  • Zod 4 helperdefault、optional、nullable、intersection、pipe、transform、coerce 与错误参数
  • runtime值读写、selector、reset、字段错误、validate 和异步校验
  • 组件输入绑定、blur/change 触发、错误消息、ARIA、dependencies 和数组增删
  • 兼容:rules/defineRules 结果一致、开发 warning 去重、生产静默、类型别名
  • 集成:useVbenForm 生命周期、提交、handleValuesChange、submit-on-change 和 async race

验收标准:

  1. 受影响 package、应用、playground 和 docs 无 TypeScript 错误
  2. form-ui 与所有应用构建成功
  3. 单元、组件和集成测试全部通过
  4. 浏览器 smoke 流程无 pageerrorconsole.error 或未处理 Promise
  5. 修改文件通过 oxfmt 与 ESLint
  6. 静态搜索中不再出现 vee 依赖、Zod 私有结构或 Zod 3 错误参数

参考资料