后续:使用 validateStandardSchema 简化 Angular Signal Forms 中的 Zod 验证
Source: Dev.to
我最近发布了一个关于在 Angular Signal Forms 中使用 Zod 验证的教程,效果非常好。
但一位 Reddit 评论者礼貌地指出,我把整个过程弄得过于复杂!
在那个视频里,我手动接线了模式验证、映射错误、处理成功状态,并翻译了字段名称——结果发现 Angular 已经内置了一个专门为 Zod 这类模式验证器设计的帮助函数。
是的,我完全错过了它。
所以今天我们来改正这个错误。我们将删除大量代码,切换到正确的 API,让 Angular Signal Forms 中的 Zod 验证 变得几乎尴尬地简单。
请继续观看——最终的解决方案出奇地简洁。
本文展示了在 Angular Signal Forms 中使用内置的validateStandardSchema() API 进行 Zod 验证的推荐方式。
Zod 验证在 Angular Signal Forms 中的工作原理(重构前)
让我们先看看当前应用的表现。
这是一个已经更新为使用全新 Signal Forms API 的简单注册表单:

如果我尝试提交表单,两个字段会立刻出现验证错误:

这些错误来自当前接入我们 Signal Form 的 Zod schema。
现在我输入一个有效的用户名和电子邮件:

可以看到错误会自动消失——这是个好兆头。
再次提交表单时,它真的会提交数据,我们可以通过下面的控制台日志确认:

所以从功能上来看,一切都正常。
这也正是问题隐蔽的原因——实现本可以 更加简洁。
Source: …
为 Angular Signal 表单定义 Zod 验证模式
让我们看看当前使这一切工作起来的代码。
我们先从 schema 文件 开始:
import { z } from 'zod';
export const signupSchema = z.object({
username: z
.string()
.min(3, 'Username must be at least 3 characters long')
.regex(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores are allowed'),
email: z.string().email('Please enter a valid email address'),
});
这部分非常出色——它是声明式的、框架无关的、易于测试,正是应该定义模式的方式。
问题
向下滚动一点,我们会看到我在前一个版本中添加的自定义 validateSignup() 函数:
export function validateSignup(value: SignupModel) {
const result = signupSchema.safeParse(value);
if (result.success) {
return {
success: true as const,
data: result.data,
errors: {} as ZodErrorMap,
};
}
const errors = result.error.issues.reduce((acc, issue) => {
const field = issue.path[0]?.toString() ?? '_form';
(acc[field] ??= []).push(issue.message);
return acc;
}, {});
return {
success: false as const,
data: null,
errors,
};
}
这个函数:
- 调用
safeParse。 - 检查验证是否成功。
- 将 Zod 的问题归约为自定义错误映射。
- 将所有内容重新塑形为 Angular 能够理解的格式。
它可以工作,但该文件现在做的事情远不止定义验证规则——这就是我们的第一个真正的问题。
使用 validateTree() 手动进行 Zod 验证(为何这是一种过度做法)
基于 Signal 的表单模型
import { signal } from '@angular/core';
import { SignupModel } from './form.schema';
protected readonly model = signal({
username: '',
email: '',
});
这个 signal 是表单状态的唯一真实来源。
Signal Forms 会观察该 signal 并在变化时自动响应。
使用 form() 创建表单
import { form, validateTree } from '@angular/forms/signals';
import { ValidationError } from '@angular/forms/signals';
import { validateSignup, ZodErrorMap } from './form.schema';
protected readonly form = form(this.model, s => {
validateTree(s, ctx => {
const result = validateSignup(ctx.value());
if (result.success) {
return undefined;
}
const zodErrors: ZodErrorMap = result.errors;
const errors: ValidationError.WithOptionalField[] = [];
const getFieldRef = (key: string) => {
switch (key) {
case 'username':
return ctx.field.username;
case 'email':
return ctx.field.email;
default:
return null;
}
};
for (const [fieldKey, messages] of Object.entries(zodErrors)) {
const fieldRef = getFieldRef(fieldKey);
if (fieldRef) {
errors.push(
...messages.map(message => ({
kind: `zod.${fieldKey}` as const,
message,
field: fieldRef,
}))
);
}
}
return errors.length ? errors : undefined;
});
});
我们使用 validateTree()——Angular 的逃生舱验证 API,它为我们提供了:
- 完整的表单值访问
- 单个字段的引用
- 对验证行为的完整控制
虽然功能强大,但这种做法会迫使我们编写大量样板代码。
样板代码的问题
自定义验证器运行后,我们处理成功情况:
if (result.success) {
return undefined;
}
随后我们手动将 Zod 错误映射为 Angular 的 ValidationError 对象:
for (const [fieldKey, messages] of Object.entries(zodErrors)) {
const fieldRef = getFieldRef(fieldKey);
if (fieldRef) {
errors.push(
...messages.map(message => ({
kind: `zod.${fieldKey}` as const,
message,
field: fieldRef,
}))
);
}
}
这类繁琐的工作难以扩展,正是 Reddit 评论者所指出的问题。
内置模式验证:validateStandardSchema()
Angular Signal Forms 提供了一个用于像 Zod 这样的模式验证器的辅助函数:validateStandardSchema()。
它能够:
- 运行模式
- 解释错误
- 将错误映射到正确的字段
- 当值变为有效时清除错误
简而言之,Angular 已经知道如何与 Zod 对话——我们只需让它这么做。
将 validateTree() 替换为 validateStandardSchema()
删除所有与 validateTree() 相关的代码,并替换为:
import { form, validateStandardSchema } from '@angular/forms/signals';
import { signupSchema } from './form.schema';
protected readonly form = form(this.model, s => {
validateStandardSchema(s, signupSchema);
});
- 第一个参数 (
s) 是表单作用域。 - 第二个参数是 Zod 模式 (
signupSchema)。
就这么简单!Angular 现在会自动运行模式,将错误映射到相应字段,并在值变为有效时立即清除它们。不再需要手动接线、错误翻译或字段查找逻辑。
简化 Schema 文件
由于自定义验证代码已移除,我们可以删除 form.schema.ts 中的 validateSignup() 函数和 ZodErrorMap 类型。现在该文件仅定义验证规则:
import { z } from 'zod';
export type SignupModel = z.infer<typeof signupSchema>;
export const signupSchema = z.object({
username: z
.string()
.min(3, 'Username must be at least 3 characters long')
.regex(
/^[a-zA-Z0-9_]+$/,
'Only letters, numbers, and underscores are allowed'
),
email: z
.string()
.email('Please enter a valid email address'),
});
该 schema 文件现在只做一件事:定义验证规则。完美!
最终结果:使用 Signal Forms 实现干净的 Zod 验证
在应用这些更改并提交后,项目:
- 使用单一的真相来源信号来管理表单状态。
- 利用 Angular 内置的
validateStandardSchema()进行 Zod 集成。 - 消除所有手动错误映射的样板代码。
- 让 schema 文件仅专注于验证规则。
代码库现在更加简洁,易于维护,并且充分利用了 Angular Signal Forms 的原生能力。
验证演示
当我们再次提交表单时,验证错误仍会自动出现:
现在我们输入一些有效数据:
完美。 错误消失了。
当我们再次提交表单时:
太好了! 它如预期般成功提交。
所以现在我们用更少的代码实现了相同的行为!
何时使用 validateStandardSchema() 与 validateTree()
在以下情况下使用 validateStandardSchema():
- 您正在使用标准的模式验证库(Zod、Yup、Joi 等)。
- 您的模式遵循标准契约(可解析,错误以标准格式返回)。
- 您希望实现尽可能最简的集成。
在以下情况下使用 validateTree():
- 您需要自定义验证逻辑,且不符合标准模式的模式。
- 您正在与不遵循标准契约的验证库集成。
- 您需要对错误映射或验证时机进行细粒度控制。
对于大多数使用 Zod 的 Angular 开发者来说,validateStandardSchema() 是正确的选择。
该 API 的存在是为了让您在每次集成验证库时无需重新发明模式适配器。
Source: …
Angular Signal Forms 中 Zod 验证的最佳实践
这是一个很容易被忽视的 Angular API,但一旦了解,你就不想再回头。
如果你在 Signal Forms 中使用 Zod,不要像我那样手动接线验证。请使用 validateStandardSchema()。
好处
- 更少的代码 – 无需手动错误映射或字段查找。
- 更少的 bug – Angular 正确处理集成。
- 更易维护 – Schema 文件专注于验证规则。
- 更好的可扩展性 – 随着表单复杂度提升仍能无缝工作。
- 类型安全 – 全程提供 TypeScript 支持。
特别感谢 pkgmain 的指正!
是的,下次我会在发布前更仔细地阅读文档。


