@angular/forms 源码解析之 Validators

1,306 阅读5分钟

我们知道,@angular/forms 包主要用来解决表单问题的,而表单问题非常重要的一个功能就是表单校验功能。数据校验非常重要,不仅仅前端在发请求给后端前需要校验数据,后端对前端发来的数据也需要校验其有效性和逻辑性,尤其在存入数据库前还得校验数据的有效性。 @angular/forms 定义了一个 Validator 接口,并内置了 RequiredValidatorCheckboxRequiredValidatorEmailValidatorMinLengthValidatorMaxLengthValidatorPatternValidator 六个常用的校验指令,每一个 validator 都实现了 Validator 接口。这些校验指令的使用很简单,比如使用 EmailValidatorRequiredValidator 指令来校验输入的数据得是 email 且不能为空:

<input type="email" name="email" ngModel email required>

这样输入的如果不是 email 格式,EmailValidator 指令就会校验错误,会给 host(这里也就是 input 元素)添加 'ng-invalid' class,这样开发者可以给这个 class 添加一些 css 效果,提高用户体验。那么,其内部运行过程是怎样的呢?

实际上,上面 demo 中不仅仅绑定了 NgModel 指令,还绑定了 EmailValidatorRequiredValidator 两个 validators 指令。指令在实例化时是按照声明顺序依次进行的,有依赖的指令则置后,FormsModule 先是声明了 RequiredValidator 指令,然后是 EmailValidator 指令,最后才是 NgModel,所以实例化顺序是 RequiredValidator -> EmailValidator -> NgModel,同时由于 NgModel 依赖于 NG_VALIDATORS,所以就算 NgModel 声明在前也会被置后实例化。RequiredValidatorEmailValidator 在实例化过程中都会提供 REQUIRED_VALIDATOREMAIL_VALIDATOR 两个服务,并且 StaticProvider 的 multi 属性设置为 true,这样可以容许有多个依赖服务(这里是 RequiredValidator 和 EmailValidator 对象)公用一个令牌(这里是 NG_VALIDATORS),multi 属性作用可以查看源码中说明。当 NgModel 实例化时,其构造依赖于 @Self() NG_VALIDATORS@Self() 表示从 NgModel 指令挂载的宿主元素中去查找这个令牌拥有的服务,NgModel 没有提供 NG_VALIDATORS,但是挂载在 input 宿主元素上的 REQUIRED_VALIDATOREMAIL_VALIDATOR 却提供了这个服务,所以 NgModel 的依赖 validators 就是这两个指令组成的对象数组。

NgModel 在实例化时,由于没有父控件容器,所以会调用 _setUpStandalone(),从而调用 setUpControl() 方法设置 FormControl 对象的 同步 validator 依赖(如果有异步 validator 依赖,也同理),这个依赖是调用 Validators.compose() 返回的一个 ValidatorFn 函数。而 Validators.compose() 参数调用的是 NgModel.validator,也就是调用 composeValidators 获得 ValidatorFn,内部会调用 normalizeValidator() 函数转换为为 (AbstractControl) => Validator.validate()。所以,和 input 控件绑定的 FormControl 对象就有了同步 validator 数据校验器。那在 input 输入框内输入数据时,校验器是在何时被运行的呢?

NgModel 实例化时,还安装了一个 视图数据更新回调,这样当 input 视图内的数据更新时,就会运行这个回调,该回调会更新 FormControl 的 value 值,即 FormControl.setValue() 函数,内部会调用 updateValueAndValidity,从而开始 运行数据校验器,上文说到 FormControl 的 validator 依赖实际上是 Validators.compose() 返回的函数,所以此时会运行 这个回调函数,而这个 presentValidators 是 (AbstractControl) => RequiredValidator.validate() 和 (AbstractControl) => EmailValidator.validate() 组成的数组,然后依次 运行 这两个 Validator 的 validate() 函数。如果校验错误,就返回 ValidationErrors,比如 email 校验器返回的是 {'email': true}。这里还需注意的是,Validator 指令里的 validate() 函数实际上调用的还是 Validator 类 的对应的静态函数,这样验证器指令可以直接在模板里使用,而 Validator 类的静态函数可以在 响应式表单 中使用。校验器运行完成后,会设置 FormControl.errors 属性,从而计算 FormControl 的 status 属性,假设校验错误,则 status 属性值为 INVALID。那如果校验错误,input 的 class 为何会添加 'ng-invalid' 呢?因为实际上还有一个 NgControlStatus 指令 也在绑定这个 input 元素,该指令的依赖会从当前挂载的宿主元素查找 NgControl,本 demo 中就是 NgModel 指令,NgControlStatus 指令 的 host 属性中的 '[class.ng-invalid]': 'ngClassInvalid',会运行 ngClassInvalid() 函数判断是否会有 'ng-invalid' class,而校验错误时,该函数运行结果是 true,因为它读取的是 FormControl.invalid 属性,则 'ng-invalid' class 就会被添加到 input 元素上。同理,其他 class 如 pending、dirty 等也同样道理。这样就理解了校验器的整个运行过程,也包括为何校验错误时会自动添加描述控件状态的 'ng-invalid' class

我们已经理解了 Validators 的内部运行流程,这样写一个自定义的 Validator 就很简单了(当然,写一个自定义的 Validator 不需要去了解 Validator 内部运行原理)。比如,写一个自定义校验器 ForbiddenValidator,input 输入内容不能还有某些字符串,那可以模仿 @angular/forms 中的内置校验器 MinLengthValidator 写法:

import {Validators as FormValidators} from '@angular/forms';

export class Validators extends FormValidators {
  static forbidden(forbidden: string): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      return (new RegExp(forbidden)).test(control.value) ? {forbidden: true} : null;
    }
  }
}

export const FORBIDDEN_VALIDATOR: StaticProvider = {
  provide: NG_VALIDATORS,
  useExisting: forwardRef(() => ForbiddenValidator),
  multi: true
};

@Directive({
  selector:
    ':not([type=checkbox])[forbidden][formControlName],:not([type=checkbox])[forbidden][formControl],:not([type=checkbox])[forbidden][ngModel]',
  providers: [FORBIDDEN_VALIDATOR],
})
export class ForbiddenValidator implements Validator{
  private _onChange: () => void;
  private _validator: ValidatorFn;
  
  @Input() forbidden: string;
  
  ngOnChanges(changes: SimpleChanges) {
    if ('forbidden' in changes) {
      this._createValidator();
      if (this._onChange) this._onChange();
    }
  }
  
  registerOnValidatorChange(fn: () => void): void {
    this._onChange = fn;
  }
  
  validate(c: AbstractControl): ValidationErrors | null {
    return this.forbidden ? this._validator(c) : null;
  }
  
  private _createValidator(): void {
    this._validator = Validators.forbidden(this.forbidden);
  }
}

这样就可以在组件模板中使用了:

@Component(
{
    template: `
        <h2>Template-Driven Form</h2>
        <input type="email" name="email" [ngModel]="email" email required [forbidden]="forbiddenText">
        <h2>Reactive-Driven Form</h2>
        <input type="email" name="email" [formControl]="emailFormControl" email required [forbidden]="forbiddenText">
        <h2>Update Forbidden Text</h2>
        <input [(ngModel)]="forbiddenText">
    `
})
export class AppComponent {
    // custom validator
      forbiddenText = 'test';
      email = 'test@test.com';
      emailFormControl = new FormControl('test@test.com', [Validators.forbidden(this.forbiddenText)]);
}

完整代码可参见 stackblitz demo

所以,在理解了 Validator 内部运行原理后,不仅仅可以写自定义的 Validator,该 Validator 可以用于模板驱动表单也可以用于响应式表单,还能明白为啥需要那么写,这个很重要!

也可阅读 @angular/forms 相关文章了解 NgModel 双向绑定内部原理:@angular/forms 源码解析之双向绑定