首页
/ Angular Signal Forms 异步校验实战:validateHttp() 与 validateAsync() 完整指南及源码解析

Angular Signal Forms 异步校验实战:validateHttp() 与 validateAsync() 完整指南及源码解析

2026-09-06 16:55:17作者:魏献源Searcher

本篇围绕 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',
        };
      },
    });
  });
}

校验流程如下:

  1. 用户输入一个值;
  2. 同步校验规则先运行;
  3. 若同步校验失败,异步校验不运行;
  4. 若同步校验通过,异步校验启动,pending() 变为 true
  5. 请求完成,pending() 变为 false
  6. 根据响应结果更新 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 接口 看,onSuccessonError 的第二个参数均为字段上下文 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() 需要四个属性:paramsfactoryonSuccessonErrorparams 返回 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 变更扣留指定毫秒后才写入模型,窗口内的新变更会重置计时器。

下面的示例在注册表单的用户名字段上同时应用 debouncevalidateHttp,把可用名校验推迟到用户停顿之后:

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 规则会扣留字段的一切反应,从同步校验到派生信号再到异步校验。但有时恰恰需要相反的效果:让 requiredemail 这类廉价同步校验立即运行以提供即时反馈,只让昂贵的异步调用等待用户停顿。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/coredebounced() 包装再进入工厂。由于只有参数信号被 debounce,模型与其他规则不受影响——这正是“模型照常逐键更新、仅异步调用被节流”的实现原理。

两种方案按作用域选择:

选项 适用场景
debounce() 规则 同步校验、派生状态与提交都应等字段提交后再反应;整个字段不应在输入中途响应。
validateHttp({ debounce })validateAsync({ debounce }) 廉价同步校验需要即时反馈,但昂贵的异步调用应等用户停顿。

两者都接受毫秒时长。它们的自定义计时回调不同:表单级规则接受 Debouncer,校验器级选项接受来自 @angular/coreDebounceTimer。两个签名不能互换。

通过 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() 返回 false
  • invalid() 返回 false
  • errors() 返回空数组
  • 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 机制会中止上一个进行中的请求并发起新的(对应 loadingreloading 状态迁移),最终落到 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 保持互操作。

延伸阅读:

登录后查看全文
热门项目推荐
相关项目推荐