Angular Signal Forms 异步校验实战:validateHttp() 与 validateAsync() 完整指南及源码解析
本篇围绕 Angular 信号表单(@angular/forms/signals)的异步校验能力展开,系统讲解 validateHttp() 与 validateAsync() 两个核心 API 的用法、pending() 状态管理、debounce 节流策略与请求取消机制,并结合仓库内 packages/forms/signals 的源码剖析其底层实现原理。读完本篇,你将能够为用户名校验、邮箱查重等场景编写生产级异步表单,并理解“同步校验先行、异步请求可取消”在源码层面是如何落地的。
何时使用异步校验
某些校验逻辑需要依赖外部数据源,例如后端 API 或第三方服务。Signal Forms 为此提供了两个函数:validateHttp() 用于基于 HTTP 的校验,validateAsync() 用于基于自定义 resource 的校验。
典型适用场景包括:
- 唯一性检查 —— 确认用户名或邮箱尚未被占用;
- 数据库查询 —— 将字段值与服务端数据比对;
- 外部 API 校验 —— 借助第三方服务校验地址、税号等数据;
- 服务端业务规则 —— 应用只有服务端才能判定的校验规则。
注意:对于可以在客户端同步完成的检查,不应使用异步校验。格式校验与静态规则请使用同步规则,如 pattern()、email() 或 validate()。
异步校验的工作机制
异步校验只会在所有同步校验通过之后才运行。校验执行期间,字段的 pending() 信号返回 true。校验结果可以定向到特定字段,且当字段值发生变化时,进行中的请求会被自动取消。
以下示例演示了用户名校验:
import {Component, signal} from '@angular/core';
import {form, validateHttp, FormField} from '@angular/forms/signals';
@Component({
selector: 'app-registration',
imports: [FormField],
template: `
<form>
<label>
Username:
<input [formField]="registrationForm.username" />
</label>
@if (registrationForm.username().pending()) {
<span class="checking">Checking availability...</span>
}
@if (registrationForm.username().invalid()) {
@for (error of registrationForm.username().errors(); track $index) {
<span class="error">{{ error.message }}</span>
}
}
</form>
`,
})
export class Registration {
registrationModel = signal({username: ''});
registrationForm = form(this.registrationModel, (schemaPath) => {
validateHttp(schemaPath.username, {
request: ({value}) => {
const username = value();
return username ? `/api/users/check?username=${username}` : undefined;
},
onSuccess: (response: {available: boolean}) => {
return response.available
? null
: {
kind: 'usernameTaken',
message: 'Username is already taken',
};
},
onError: (error) => {
console.error('Validation request failed:', error);
return {
kind: 'serverError',
message: 'Could not verify username availability',
};
},
});
});
}
校验流程如下:
- 用户输入一个值;
- 同步校验规则先运行;
- 若同步校验失败,异步校验不运行;
- 若同步校验通过,异步校验启动,
pending()变为true; - 请求完成,
pending()变为false; - 根据响应结果更新
errors()。
源码印证:同步校验先行是如何实现的
从 validateAsync 源码 可以看到,这一执行顺序是通过一个受管元数据回调实现的:
metadata(path, RESOURCE, (ctx) => {
const node = ctx.stateOf(path) as FieldNode;
const validationState = node.validationState;
if (validationState.shouldSkipValidation() || !validationState.syncValid()) {
return undefined;
}
if (opts.when && !opts.when(ctx)) {
return undefined;
}
return opts.params(ctx);
});
当同步校验未通过(syncValid() 为 false)时,回调直接返回 undefined,即 resource 的参数为 undefined,底层 httpResource 不会发出任何请求。这从源码层面印证了文档所述的“同步校验失败则异步校验不运行”,也解释了为什么返回 undefined 能跳过请求:参数为 undefined 时 resource 处于 idle 状态。
此外,源码接口中还暴露了文档示例未展示的 when 选项(一个 LogicFn),返回 false 时同样会跳过本次异步校验,可用于基于跨字段逻辑的条件触发。
另一个源码级细节在 异步错误规则:
pathNode.builder.addAsyncErrorRule((ctx) => {
const res = ctx.state.metadata(RESOURCE)!;
switch (res.status()) {
case 'idle':
return undefined;
case 'loading':
case 'reloading':
return 'pending';
case 'resolved':
case 'local':
...
字段之所以“pending”,正是因为异步错误规则在 resource 处于 loading/reloading 状态时返回 'pending' 标记;资源解析完成后,onSuccess 的返回值会经过 addDefaultField(errors, ctx.fieldTree) 处理——错误可以指定目标字段,未指定时默认落在被校验字段上,这就是文档中“校验可以定向到特定字段”的实现机制。
使用 validateHttp() 进行 HTTP 校验
validateHttp() 是最常见的异步校验形式,适用于 REST API 或任意 HTTP 端点的校验。
从 validateHttp 源码 可以看出,它本质上是 validateAsync() 的语法糖:
validateAsync(path, {
params: opts.request as (ctx) => string | HttpResourceRequest | undefined,
debounce: opts.debounce,
factory: (request: Signal<any>) => httpResource(request, opts.options),
onSuccess: opts.onSuccess,
onError: opts.onError,
when: opts.when,
});
request 被直接映射为 resource 的参数,工厂函数固定为“根据请求信号创建 httpResource”,而 options 则透传给 httpResource。理解这一点后,validateHttp 的所有行为都可以用 validateAsync + httpResource 的语义推导出来。
request 函数
request 函数返回一个 URL 字符串或 HttpResourceRequest 对象;返回 undefined 则跳过本次校验:
import {Component, signal} from '@angular/core';
import {form, validateHttp, FormField} from '@angular/forms/signals';
@Component({
selector: 'app-registration',
imports: [FormField],
template: `...`,
})
export class Registration {
registrationModel = signal({username: ''});
// 缓存已通过校验的用户名
private validatedUsernames = new Set<string>();
registrationForm = form(this.registrationModel, (schemaPath) => {
validateHttp(schemaPath.username, {
request: ({value}) => {
const username = value();
// 已校验过的用户名不再发请求
if (this.validatedUsernames.has(username)) return undefined;
return `/api/users/check?username=${username}`;
},
onSuccess: (response: {available: boolean}, {value}) => {
if (response.available) {
// 缓存成功校验
this.validatedUsernames.add(value());
return null;
}
return {
kind: 'usernameTaken',
message: 'Username is already taken',
};
},
onError: () => ({
kind: 'serverError',
message: 'Could not verify username',
}),
});
});
}
对于 POST 请求或自定义 header,返回 HttpResourceRequest 对象:
request: ({value}) => ({
url: '/api/validate',
method: 'POST',
body: {username: value()},
}) // prettier-ignore
onSuccess 与 onError 处理器
onSuccess 接收 HTTP 响应,返回校验错误;值合法时返回 undefined(或 null):
onSuccess: (response: { valid: boolean; message?: string }) => {
if (response.valid) return undefined;
return {
kind: 'invalid',
message: response.message || 'Validation failed',
};
} // prettier-ignore
必要时可以返回多个错误:
onSuccess: (response: { usernameTaken: boolean; profanity: boolean }) => {
const errors = [];
if (response.usernameTaken) {
errors.push({
kind: 'usernameTaken',
message: 'Username taken',
});
}
if (response.profanity) {
errors.push({
kind: 'profanity',
message: 'Username contains inappropriate content',
});
}
return errors.length > 0 ? errors : undefined;
} // prettier-ignore
onSuccess 的响应类型可以直接写在参数注解中,也可以通过 validateHttp 选项中的 parse 属性指定:
onSuccess: (response: { usernameTaken: boolean; profanity: boolean }) => {
// ...
} // prettier-ignore
// 或者
options: {
parse: (response) => response as {usernameTaken: boolean; profanity: boolean};
}
onSuccess: (response) => {
// ...
} // prettier-ignore
onError 负责处理请求失败(网络错误、HTTP 错误等):
onError: (error) => {
console.error('Validation request failed:', error);
return {
kind: 'serverError',
message: 'Could not verify. Please try again later.',
};
} // prettier-ignore
从 HttpValidatorOptions 接口 看,onSuccess 与 onError 的第二个参数均为字段上下文 ctx(即 FieldContext),可用于读取其他字段值来做跨字段判断,文档示例中未用到,但源码签名已支持。
HTTP options
通过 options 参数定制 HTTP 请求:
import {HttpHeaders} from '@angular/common/http';
validateHttp(schemaPath.field, {
request: ({value}) => `/api/validate?value=${value()}`,
options: {
headers: new HttpHeaders({
Authorization: 'Bearer token',
}),
timeout: 5000,
},
onSuccess: (response: {valid: boolean}) =>
response.valid
? null
: {
kind: 'invalid',
message: 'Invalid value',
},
onError: () => ({
kind: 'requestFailed',
message: 'Unable to reach server to validate.',
}),
});
options 的类型是 HttpResourceOptions,完整的可用选项请参考 @angular/common/http 中的 httpResource API。
使用 validateAsync() 进行自定义异步校验
大多数应用应优先使用 validateHttp():它以最少的配置覆盖 HTTP 请求场景,满足绝大多数用例。
validateAsync() 是更底层的 API,直接暴露 Angular 的 resource 原语。它提供了完全的控制权,但需要更多代码和对 resource API 的熟悉。只有当 validateHttp() 无法满足需求时才考虑它,例如:
- 非 HTTP 校验 —— WebSocket 连接、IndexedDB 查询或 Web Worker 计算;
- 自定义缓存策略 —— 超出简单记忆化的应用级缓存;
- 复杂的重试逻辑 —— 自定义退避策略或条件重试;
- 直接访问 resource —— 需要完整 resource 生命周期时。
创建自定义校验规则
validateAsync() 需要四个属性:params、factory、onSuccess 和 onError。params 返回 resource 的参数,factory 创建 resource:
import {Component, inject, signal, resource, Signal} from '@angular/core';
import {form, validateAsync, FormField} from '@angular/forms/signals';
import {UsernameValidator} from './username-validator';
@Component({
selector: 'app-registration',
imports: [FormField],
template: `...`,
})
export class Registration {
registrationModel = signal({username: ''});
private usernameValidator = inject(UsernameValidator);
private cache = new Map<string, {available: boolean}>();
// 带缓存的自定义 resource 工厂
createUsernameResource = (usernameSignal: Signal<string | undefined>) => {
return resource({
params: () => usernameSignal(),
loader: async ({params: username}) => {
if (!username) return undefined;
// 先查缓存
const cached = this.cache.get(username);
if (cached !== undefined) return cached;
// 使用注入的服务进行校验
const result = await this.usernameValidator.checkAvailability(username);
// 写入缓存
this.cache.set(username, result);
return result;
},
});
};
registrationForm = form(this.registrationModel, (schemaPath) => {
validateAsync(schemaPath.username, {
params: ({value}) => {
const username = value();
return username.length >= 3 ? username : undefined!;
},
factory: this.createUsernameResource,
onSuccess: (result) => {
return result?.available
? null
: {
kind: 'usernameTaken',
message: 'Username taken',
};
},
onError: (error) => {
console.error('Validation failed:', error);
return {
kind: 'serverError',
message: 'Could not verify username',
};
},
});
});
}
params 函数在每次值变化时运行,返回 undefined 可跳过校验;factory 只在初始化时运行一次,并接收参数信号,resource 会在参数变化时自动更新。
源码注释(AsyncValidatorOptions.factory)明确了这一契约:框架在“本次无需运行校验”时会把参数报告为 undefined,因此工厂创建的 resource 必须能正确处理参数为 undefined 的情况(例如 loader 中直接返回 undefined)。
使用基于 Observable 的服务
如果应用中存在返回 Observable 的既有服务,可以使用 @angular/core/rxjs-interop 中的 rxResource:
import {Component, inject, signal, Signal} from '@angular/core';
import {rxResource} from '@angular/core/rxjs-interop';
import {form, validateAsync, FormField} from '@angular/forms/signals';
import {UsernameService} from './username-service';
@Component({
selector: 'app-registration',
imports: [FormField],
template: `...`,
})
export class Registration {
registrationModel = signal({username: ''});
private usernameService = inject(UsernameService);
private createUsernameResource = (usernameSignal: Signal<string | undefined>) => {
return rxResource({
params: () => usernameSignal(),
stream: ({params: username}) => this.usernameService.checkUsername(username),
});
};
registrationForm = form(this.registrationModel, (schemaPath) => {
validateAsync(schemaPath.username, {
params: ({value}) => value(),
factory: this.createUsernameResource,
onSuccess: (result) =>
result?.available ? null : {kind: 'usernameTaken', message: 'Username taken'},
onError: () => ({
kind: 'serverError',
message: 'Could not verify username',
}),
});
});
}
rxResource 直接与 Observable 协作,并在字段值变化时自动处理订阅清理。该互操作能力对应仓库中的 rxjs-interop 模块。
Debounce 策略
debounce 规则延迟把用户输入提交到表单模型的时机,可以理解为规则把值“扣住”,直到用户暂停输入。当下游行为不应响应每个按键时特别有用,例如昂贵的派生计算、输入中途就闪烁报错的校验、或逐字符重新应用的搜索过滤。
在 schema 中添加 debounce 规则,即可延迟表单字段的 UI 变更到达模型的时间。最简形式 debounce(path, ms) 会将每次 UI 变更扣留指定毫秒后才写入模型,窗口内的新变更会重置计时器。
下面的示例在注册表单的用户名字段上同时应用 debounce 与 validateHttp,把可用名校验推迟到用户停顿之后:
import {Component, signal} from '@angular/core';
import {form, debounce, validateHttp, FormField} from '@angular/forms/signals';
@Component({
selector: 'app-registration',
imports: [FormField],
template: `
<label>
Username:
<input [formField]="registrationForm.username" />
</label>
@if (registrationForm.username().pending()) {
<span class="checking">Checking availability...</span>
}
`,
})
export class Registration {
registrationModel = signal({username: ''});
registrationForm = form(this.registrationModel, (schemaPath) => {
// UI 更新先扣留 300ms,再写入模型
debounce(schemaPath.username, 300);
// 针对 debounce 后的模型值运行,而非每个按键
validateHttp(schemaPath.username, {
request: ({value}) => {
const username = value();
// 空值跳过请求
return username ? `/api/users/check?username=${username}` : undefined;
},
onSuccess: (response: {available: boolean}) =>
response.available ? null : {kind: 'usernameTaken', message: 'Username is already taken'},
onError: () => ({
kind: 'serverError',
message: 'Could not verify username availability',
}),
});
});
}
配置 300ms debounce 后,只有当用户停顿超过设定时长,模型才会更新并触发校验。例如快速连打 “signal forms”,只会发出一次校验请求,而不是十二次。
Touch 会立即刷新模型
无论 debounce 时长多少,当字段变为 touched 时,框架会立即把字段的 controlValue() 写入模型。原生输入在失焦时变为 touched,因此用户打完字按 Tab 离开时无需等待 debounce 计时结束。自定义控件可以在任意选定的事件上标记字段为 touched。
这在实际中最重要的一点是表单提交:当用户点击提交按钮时,聚焦的输入框失焦,该字段被 touched,其待处理的 debounce 会在提交处理器运行之前被刷新。
仅在 blur 时提交
有些字段不应在输入中途更新,而应等用户输入完毕后再更新。例如每次变更都会重新应用的搜索过滤器,或会触发昂贵派生状态的表单——这类场景下让模型等到用户完成输入再更新往往更好。
此时传入 'blur' 而不是时长,把所有更新推迟到字段 touched:
form(this.registrationModel, (schemaPath) => {
debounce(schemaPath.username, 'blur');
});
使用 'blur' 时,用户输入过程中模型保持旧值。同步与异步校验、派生信号以及任何读取该字段值的响应式规则,在字段 touched 之前看到的都是旧值。这通常发生在原生输入失焦,或自定义控件自行发出 touch 信号时。
自定义计时逻辑
对于时长或 'blur' 无法表达的计时逻辑,可以传入一个 Debouncer 函数。该函数接收字段上下文和一个 AbortSignal,返回一个在模型应当更新时 resolve 的 Promise<void>:
import {debounce, type Debouncer} from '@angular/forms/signals';
const shorterWhenLonger: Debouncer<string> = ({value}, abortSignal) => {
// 短查询更可能还在输入,给予更长延迟
const ms = value().length < 3 ? 500 : 200;
return new Promise((resolve) => {
const timeoutId = setTimeout(resolve, ms);
// 字段被 touch 或其值变化时触发 abort,清除待处理的计时器
abortSignal.addEventListener(
'abort',
() => {
clearTimeout(timeoutId);
resolve();
},
{once: true},
);
});
};
const registrationForm = form(registrationModel, (schemaPath) => {
debounce(schemaPath.username, shorterWhenLonger);
});
abortSignal 会在字段被 touched、或其值在 debounce resolve 前发生变化时触发。务必在 abort 时 resolve promise,让 debounce 器释放挂起的计时器。框架在 touch 时会把待处理值写入模型,在更新的值到来时则丢弃旧值。Debouncer 的完整签名见 debounce 规则源码。
只对单个异步校验器做 debounce
debounce 规则会扣留字段的一切反应,从同步校验到派生信号再到异步校验。但有时恰恰需要相反的效果:让 required、email 这类廉价同步校验立即运行以提供即时反馈,只让昂贵的异步调用等待用户停顿。validateHttp() 与 validateAsync() 都接受各自的 debounce 选项,只对该校验器节流:
form(this.registrationModel, (schemaPath) => {
validateHttp(schemaPath.username, {
// 只对该 HTTP 调用节流
debounce: 300,
request: ({value}) => {
const username = value();
// 空值跳过请求
return username ? `/api/users/check?username=${username}` : undefined;
},
onSuccess: (response: {available: boolean}) =>
response.available ? null : {kind: 'usernameTaken', message: 'Username is already taken'},
onError: () => ({
kind: 'serverError',
message: 'Could not verify username availability',
}),
});
});
此时模型仍在每次按键时更新,字段上挂的其他规则也照常即时反应。只有 HTTP 请求被 debounce:每次变化等待 300ms 的静默期才发出,确保请求只在用户停顿输入后发出。
这一机制在 validateAsync 源码 中一目了然:
const RESOURCE = createManagedMetadataKey<...>(
(_state, params) => {
if (opts.debounce !== undefined) {
const debouncedResource = debounced(() => params(), opts.debounce);
const wrappedParams = computed(() => ɵchain(debouncedResource));
return opts.factory(wrappedParams);
}
return opts.factory(params);
},
);
即:当设置了校验级 debounce 时,传给工厂的参数信号先经过 @angular/core 的 debounced() 包装再进入工厂。由于只有参数信号被 debounce,模型与其他规则不受影响——这正是“模型照常逐键更新、仅异步调用被节流”的实现原理。
两种方案按作用域选择:
| 选项 | 适用场景 |
|---|---|
debounce() 规则 |
同步校验、派生状态与提交都应等字段提交后再反应;整个字段不应在输入中途响应。 |
validateHttp({ debounce }) 或 validateAsync({ debounce }) |
廉价同步校验需要即时反馈,但昂贵的异步调用应等用户停顿。 |
两者都接受毫秒时长。它们的自定义计时回调不同:表单级规则接受 Debouncer,校验器级选项接受来自 @angular/core 的 DebounceTimer。两个签名不能互换。
通过 factory 组合 resource
内置的 debounce 选项覆盖了节流需求,但 validateAsync() 暴露了更深的组合点:factory 函数。工厂接收参数信号并返回一个 resource,在这两点之间可以自由组合所需的一切。
最简形式下,工厂包裹单个 resource。用户名校验可以作为组件类上的方法,再通过引用接入 validateAsync:
export class Registration {
registrationModel = signal({username: ''});
private usernameValidator = inject(UsernameValidator);
// 工厂函数
checkUsernameAvailable = (username: Signal<string | undefined>) =>
resource({
params: () => username(),
loader: async ({params: name}) => this.usernameValidator.checkAvailability(name),
});
registrationForm = form(this.registrationModel, (schemaPath) => {
validateAsync(schemaPath.username, {
params: ({value}) => {
const username = value();
// 短用户名跳过校验
return username.length >= 3 ? username : undefined!;
},
debounce: 300,
// 引用上面定义的工厂
factory: this.checkUsernameAvailable,
onSuccess: (result) =>
result?.available ? null : {kind: 'usernameTaken', message: 'Username taken'},
onError: () => ({kind: 'serverError', message: 'Could not verify'}),
});
});
}
params 回调对短用户名返回 undefined,表示应跳过校验。应用 debounce: 300 后,resource 会等用户停顿输入 300ms 才响应每次变更;随后才对合法用户名运行 loader,当 debounce 后的值稳定在 undefined 时保持空闲。
将 debounce 与额外逻辑组合
当需要超出普通时长 debounce 的逻辑时,可以用自定义工厂把 debounce 与该逻辑组合起来。常见场景是缓存已校验的响应。例如,服务器确认过某个用户名后,用户再次键入相同值时就不必重复询问。
export class Registration {
registrationModel = signal({username: ''});
private usernameValidator = inject(UsernameValidator);
registrationForm = form(this.registrationModel, (schemaPath) => {
validateAsync(schemaPath.username, {
params: ({value}) => {
const username = value();
return username.length >= 3 ? username : undefined;
},
factory: (username) => {
// 核心原语:源停止变化 300ms 后稳定
const debouncedUsername = debounced(username, 300);
// 缓存位于工厂闭包内,生命周期与字段一致
const cache = new Map<string, {available: boolean}>();
return resource({
// 从 debounce 后的信号读取,而非原始信号
params: () => debouncedUsername.value(),
loader: async ({params: name}) => {
const cached = cache.get(name);
if (cached) return cached;
const result = await this.usernameValidator.checkAvailability(name);
cache.set(name, result);
return result;
},
});
},
onSuccess: (result) =>
result?.available ? null : {kind: 'usernameTaken', message: 'Username taken'},
onError: () => ({
kind: 'serverError',
message: 'Could not verify username',
}),
});
});
}
cache 位于工厂闭包内,生命周期与字段一致。当用户再次键入服务器已确认过的用户名时,loader 直接从缓存读取,而不发起新的网络请求。此处的 debounced() 原语可参考 debounced API 指南。
理解 pending 状态
异步校验运行时,字段的 pending() 信号返回 true。在此期间:
valid()返回falseinvalid()返回falseerrors()返回空数组submit()会等待校验完成
在模板中展示 pending 状态以提供反馈:
<input [formField]="loginForm.username" />
@if (loginForm.username().pending()) {
<span class="loading">Checking availability...</span>
}
@if (loginForm.username().touched() && loginForm.username().invalid()) {
@for (error of loginForm.username().errors(); track $index) {
<span class="error">{{ error.message }}</span>
}
}
在 pending 期间禁用表单提交:
<button type="submit" [disabled]="loginForm().pending()">
@if (loginForm().pending()) {
Validating...
} @else {
Submit
}
</button>
更多基于 pending()、valid()、invalid() 信号的状态模式,参见 Field State Management 指南。
校验执行顺序
异步校验只在同步校验通过后运行,从而避免为无效输入发出无谓的服务端请求:
import {form, required, minLength, validateHttp} from '@angular/forms/signals';
form(model, (schemaPath) => {
// 1. 这些同步校验规则先运行
required(schemaPath.username);
minLength(schemaPath.username, 3);
// 2. 该异步校验规则仅在同步校验通过时运行
validateHttp(schemaPath.username, {
request: ({value}) => `/api/check?username=${value()}`,
onSuccess: (result: {valid: boolean}) =>
result.valid
? null
: {
kind: 'usernameTaken',
message: 'Username taken',
},
onError: () => ({
kind: 'serverError',
message: 'Validation failed',
}),
});
});
这种执行顺序降低了服务端负载并即时捕获格式错误,从而提升性能。
请求取消
当字段值变化时,Signal Forms 会自动取消该字段所有进行中的异步校验请求。这避免了竞态条件,并保证校验始终反映当前值。你无需自己实现取消逻辑。
从源码结构看,这一能力由 resource 的参数驱动机制提供:request/params 返回 undefined 时 resource 停止加载,参数从旧值切换到新值时,Angular 的 resource 机制会中止上一个进行中的请求并发起新的(对应 loading → reloading 状态迁移),最终落到 idle/resolved 等稳定状态。这与前面 validateAsync 中的状态映射 相互印证。
最佳实践
与同步校验组合
始终在发起异步请求前先做格式校验,这样能即时捕获错误并避免不必要的服务端请求:
import {form, required, email, validateHttp} from '@angular/forms/signals';
form(model, (schemaPath) => {
// 先校验格式
required(schemaPath.email);
email(schemaPath.email);
// 再检查可用性
validateHttp(schemaPath.email, {
request: ({value}) => `/api/emails/check?email=${value()}`,
onSuccess: (result: {available: boolean}) =>
result.available
? null
: {
kind: 'emailInUse',
message: 'Email already in use',
},
onError: () => ({
kind: 'serverError',
message: 'Could not verify email',
}),
});
});
适时跳过校验
从 request 函数返回 undefined 即可跳过校验。用它来避免校验空字段或不满足最低要求的值:
import {validateHttp} from '@angular/forms/signals';
validateHttp(schemaPath.username, {
request: ({value}) => {
const username = value();
// 跳过空值或过短的用户名
if (!username || username.length < 3) return undefined;
return `/api/users/check?username=${username}`;
},
onSuccess: (result: {valid: boolean}) =>
result.valid
? null
: {
kind: 'usernameTaken',
message: 'Username taken',
},
onError: () => ({
kind: 'serverError',
message: 'Validation failed',
}),
});
优雅地处理错误
提供清晰、面向用户的错误消息。技术性细节用于日志排查,向用户展示简单信息:
import {validateHttp} from '@angular/forms/signals';
validateHttp(schemaPath.field, {
request: ({value}) => `/api/validate?field=${value()}`,
onSuccess: (result: {valid: boolean; message?: string}) => {
if (result.valid) return null;
// 优先使用服务端消息
return {
kind: 'serverError',
message: result.message || 'Validation failed',
};
},
onError: (error) => {
// 记录用于排查
console.error('Validation request failed:', error);
// 向用户展示友好消息
return {
kind: 'serverError',
message: 'Unable to validate. Please try again later.',
};
},
});
展示清晰的反馈
使用 pending() 信号展示校验正在进行,帮助用户理解延迟,提升感知性能:
@if (field().pending()) {
<span class="checking">
<span class="spinner"></span>
Checking...
</span>
}
@if (field().valid() && !field().pending()) {
<span class="success">Available</span>
}
@if (field().invalid()) {
<span class="error">{{ field().errors()[0]?.message }}</span>
}
版本前提与相关资源
本文涉及的 Signal Forms API 在源码中标注为 @publicApi 22.0(见 validateHttp 源码注释),因此请以 Angular 22 及以后版本为前提理解本文内容。
信号表单的定位见 Signal-based forms API 包说明:它是构建在 signals 之上的替代性表单 API,与既有 @angular/forms API 保持互操作。
延伸阅读:
- Signal Forms 校验指南 —— 同步与异步校验的完整规则体系;
- Field State Management 指南 ——
pending()、valid()、invalid()等字段状态信号; - resource() 原语指南 ——
validateAsync()工厂所基于的底层 API; - validateHttp 源码 与 validateAsync 源码 —— 本文所有源码级结论的出处;
- debounce 规则源码 —— 表单级
Debouncer签名与实现。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00