MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular表单验证的异步验证器应用

2023-05-184.5k 阅读

一、Angular 表单验证概述

在前端开发中,表单验证是确保用户输入数据有效性的关键环节。Angular 提供了强大且灵活的表单验证机制,包括同步验证器和异步验证器。同步验证器能立即对用户输入进行检查,例如检查输入是否为空、是否符合特定格式等。而异步验证器则适用于需要与外部服务(如后端 API)进行交互来验证数据的场景,比如验证用户名是否已存在,邮箱是否已被注册等。

(一)同步验证器回顾

在深入了解异步验证器之前,先简单回顾一下同步验证器。Angular 内置了一些常用的同步验证器,像 required(必填项验证)、minlength(最小长度验证)、maxlength(最大长度验证)以及 pattern(正则表达式匹配验证)等。

例如,在模板驱动表单中使用 required 验证器:

<input type="text" [(ngModel)]="userName" name="userName" required>

在响应式表单中,以 FormControl 为例使用多个同步验证器:

import { FormControl, Validators } from '@angular/forms';

const emailControl = new FormControl('', [
  Validators.required,
  Validators.pattern(/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/)
]);

同步验证器的优点在于其及时性,用户输入的同时就能得到反馈。但对于一些依赖外部资源的验证,它就显得力不从心,这时候就需要异步验证器登场。

二、异步验证器基础

(一)异步验证器的概念

异步验证器在 Angular 中是一种特殊类型的验证器,它返回一个 PromiseObservable。Angular 会等待这个 Promise 被解决或 Observable 发出值,以此来确定验证结果。这种特性使得异步验证器非常适合处理需要时间来完成的验证任务,比如与后端服务器通信验证数据的唯一性。

(二)创建异步验证器

  1. 使用 ValidatorFn 定义异步验证器 要创建一个异步验证器,首先需要理解 ValidatorFn 接口。对于异步验证器,ValidatorFn 的返回类型是 Promise<ValidationErrors | null>Observable<ValidationErrors | null>。 以下是一个简单的异步验证器示例,模拟检查用户名是否已存在(实际应用中会与后端通信):

    import { AbstractControl, ValidationErrors, AsyncValidatorFn } from '@angular/forms';
    import { Observable, of } from 'rxjs';
    import { delay } from 'rxjs/operators';
    
    export function asyncUserNameValidator(): AsyncValidatorFn {
      return (control: AbstractControl): Observable<ValidationErrors | null> => {
        const mockExistingUsernames = ['admin', 'testuser'];
        const enteredUserName = control.value;
        return of(mockExistingUsernames.includes(enteredUserName))
         .pipe(
            delay(1000) // 模拟网络延迟
          )
         .map(isExists => isExists? { userNameExists: true } : null);
      };
    }
    

    在上述代码中,asyncUserNameValidator 函数返回一个 AsyncValidatorFn。它接受一个 AbstractControl 参数,该参数包含用户输入的值。通过模拟数据检查用户名是否存在,of 操作符创建一个 Observabledelay 操作符模拟网络延迟,最后 map 操作符根据检查结果返回验证错误对象或 null

  2. 在响应式表单中使用异步验证器 创建好异步验证器后,就可以在响应式表单中使用它。

    import { Component } from '@angular/core';
    import { FormControl, FormGroup, Validators } from '@angular/forms';
    import { asyncUserNameValidator } from './async - user - name.validator';
    
    @Component({
      selector: 'app - user - form',
      templateUrl: './user - form.component.html',
      styleUrls: ['./user - form.component.css']
    })
    export class UserFormComponent {
      userForm: FormGroup;
    
      constructor() {
        this.userForm = new FormGroup({
          userName: new FormControl('', [Validators.required], [asyncUserNameValidator()])
        });
      }
    }
    

    在上面的 UserFormComponent 中,userNameFormControl 不仅使用了同步的 required 验证器,还使用了我们自定义的异步验证器 asyncUserNameValidator。注意,异步验证器是作为第三个参数传递给 FormControl 构造函数的。

  3. 在模板驱动表单中使用异步验证器 在模板驱动表单中使用异步验证器稍微复杂一些,需要借助 ngFormFormControlName 指令。 首先,在组件类中定义验证器:

    import { Component } from '@angular/core';
    import { AbstractControl, ValidationErrors, AsyncValidatorFn } from '@angular/forms';
    import { Observable, of } from 'rxjs';
    import { delay } from 'rxjs/operators';
    
    export function asyncEmailValidator(): AsyncValidatorFn {
      return (control: AbstractControl): Observable<ValidationErrors | null> => {
        const mockExistingEmails = ['test@example.com', 'admin@example.com'];
        const enteredEmail = control.value;
        return of(mockExistingEmails.includes(enteredEmail))
         .pipe(
            delay(1000)
          )
         .map(isExists => isExists? { emailExists: true } : null);
      };
    }
    
    @Component({
      selector: 'app - template - driven - form',
      templateUrl: './template - driven - form.component.html',
      styleUrls: ['./template - driven - form.component.css']
    })
    export class TemplateDrivenFormComponent {
      asyncEmailValidator = asyncEmailValidator();
    }
    

    然后,在模板中:

    <form #userForm="ngForm">
      <div class="form - group">
        <label for="email">Email</label>
        <input type="email" id="email" name="email" [(ngModel)]="userEmail"
               required
               [asyncValidator]="asyncEmailValidator"
               #email="ngModel">
        <div *ngIf="email.hasError('required') && (email.touched || email.dirty)">
          Email is required.
        </div>
        <div *ngIf="email.hasError('emailExists') && (email.touched || email.dirty)">
          This email already exists.
        </div>
      </div>
      <button type="submit" class="btn btn - primary">Submit</button>
    </form>
    

    在模板中,通过 [asyncValidator] 绑定自定义的异步验证器 asyncEmailValidator,并根据验证结果在相应的 div 中显示错误信息。

三、异步验证器与后端交互

(一)使用 HttpClient 进行后端验证

在实际应用中,异步验证器通常需要与后端服务器进行通信来验证数据。Angular 的 HttpClient 模块提供了强大的 HTTP 客户端功能,方便与后端进行交互。

假设后端有一个 API 用于验证用户名是否存在,API 地址为 /api/check - username,请求方式为 POST,请求体包含用户名。

  1. 创建服务 首先创建一个服务来处理与后端的通信:

    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Observable } from 'rxjs';
    
    @Injectable({
      providedIn: 'root'
    })
    export class UserService {
      constructor(private http: HttpClient) {}
    
      checkUserNameExists(userName: string): Observable<{ exists: boolean }> {
        const url = '/api/check - username';
        return this.http.post<{ exists: boolean }>(url, { userName });
      }
    }
    

    UserService 中,checkUserNameExists 方法接受一个用户名作为参数,通过 HttpClientpost 方法向后端发送请求,并返回一个 Observable,该 Observable 发出的对象包含一个 exists 属性,用于表示用户名是否已存在。

  2. 更新异步验证器 然后更新异步验证器,使用这个服务来进行实际的验证:

    import { AbstractControl, ValidationErrors, AsyncValidatorFn } from '@angular/forms';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    import { UserService } from './user.service';
    
    export function asyncUserNameValidator(userService: UserService): AsyncValidatorFn {
      return (control: AbstractControl): Observable<ValidationErrors | null> => {
        const enteredUserName = control.value;
        return userService.checkUserNameExists(enteredUserName)
         .pipe(
            map(response => response.exists? { userNameExists: true } : null)
          );
      };
    }
    

    在新的 asyncUserNameValidator 中,它接受 UserService 作为参数,并在验证逻辑中调用 UserServicecheckUserNameExists 方法,根据后端返回的结果确定验证是否通过。

  3. 在组件中使用 最后在组件中使用这个更新后的异步验证器:

    import { Component } from '@angular/core';
    import { FormControl, FormGroup, Validators } from '@angular/forms';
    import { UserService } from './user.service';
    import { asyncUserNameValidator } from './async - user - name.validator';
    
    @Component({
      selector: 'app - user - form',
      templateUrl: './user - form.component.html',
      styleUrls: ['./user - form.component.css']
    })
    export class UserFormComponent {
      userForm: FormGroup;
    
      constructor(private userService: UserService) {
        this.userForm = new FormGroup({
          userName: new FormControl('', [Validators.required], [asyncUserNameValidator(userService)])
        });
      }
    }
    

    UserFormComponent 的构造函数中注入 UserService,并将其传递给 asyncUserNameValidator,从而实现与后端的异步验证交互。

(二)处理后端响应错误

在与后端交互过程中,可能会遇到各种错误,比如网络故障、后端服务异常等。需要在异步验证器中妥善处理这些错误情况。

  1. 使用 catchError 操作符 继续以上面的例子为例,在异步验证器中添加错误处理:

    import { AbstractControl, ValidationErrors, AsyncValidatorFn } from '@angular/forms';
    import { Observable } from 'rxjs';
    import { map, catchError } from 'rxjs/operators';
    import { UserService } from './user.service';
    
    export function asyncUserNameValidator(userService: UserService): AsyncValidatorFn {
      return (control: AbstractControl): Observable<ValidationErrors | null> => {
        const enteredUserName = control.value;
        return userService.checkUserNameExists(enteredUserName)
         .pipe(
            map(response => response.exists? { userNameExists: true } : null),
            catchError(error => {
              console.error('Error checking username:', error);
              return of({ backendError: true });
            })
          );
      };
    }
    

    在上述代码中,通过 catchError 操作符捕获 Observable 中的错误。当发生错误时,在控制台打印错误信息,并返回一个包含 backendError 错误标识的验证错误对象,这样在表单中可以根据这个错误标识显示相应的错误信息。

  2. 在模板中显示错误信息 在模板中添加错误信息显示:

    <form [formGroup]="userForm">
      <div class="form - group">
        <label for="userName">User Name</label>
        <input type="text" id="userName" formControlName="userName">
        <div *ngIf="userForm.get('userName').hasError('required') && (userForm.get('userName').touched || userForm.get('userName').dirty)">
          User name is required.
        </div>
        <div *ngIf="userForm.get('userName').hasError('userNameExists') && (userForm.get('userName').touched || userForm.get('userName').dirty)">
          This user name already exists.
        </div>
        <div *ngIf="userForm.get('userName').hasError('backendError') && (userForm.get('userName').touched || userForm.get('userName').dirty)">
          There was an error checking the user name. Please try again later.
        </div>
      </div>
      <button type="submit" class="btn btn - primary">Submit</button>
    </form>
    

    通过这种方式,用户在遇到后端验证错误时能得到友好的提示信息。

四、异步验证器的性能优化

(一)防抖(Debounce)

当用户在输入框中快速输入时,频繁触发异步验证可能会导致性能问题,特别是与后端交互时。可以使用防抖技术来解决这个问题。

  1. 使用 rxjsdebounceTime 操作符 在异步验证器中应用 debounceTime
    import { AbstractControl, ValidationErrors, AsyncValidatorFn } from '@angular/forms';
    import { Observable } from 'rxjs';
    import { map, catchError, debounceTime } from 'rxjs/operators';
    import { UserService } from './user.service';
    
    export function asyncUserNameValidator(userService: UserService): AsyncValidatorFn {
      return (control: AbstractControl): Observable<ValidationErrors | null> => {
        return new Observable(observer => {
          const subscription = control.valueChanges
           .pipe(
              debounceTime(500),
              map(() => control.value),
              map(enteredUserName => userService.checkUserNameExists(enteredUserName)),
              map(response => response.exists? { userNameExists: true } : null),
              catchError(error => {
                console.error('Error checking username:', error);
                return of({ backendError: true });
              })
            )
           .subscribe(observer);
          return () => subscription.unsubscribe();
        });
      };
    }
    
    在上述代码中,control.valueChanges 监听输入框的值变化,debounceTime(500) 表示在用户停止输入 500 毫秒后才触发后续的验证逻辑,这样可以有效减少不必要的验证请求。

(二)节流(Throttle)

节流也是一种优化性能的方式,它限制异步验证在一定时间间隔内只能执行一次。

  1. 自定义节流函数 首先创建一个自定义的节流函数:
    function throttle<T extends (...args: any[]) => any>(func: T, delay: number): T {
      let timer: NodeJS.Timeout | null = null;
      return ((...args: any[]): any => {
        if (!timer) {
          func.apply(this, args);
          timer = setTimeout(() => {
            timer = null;
          }, delay);
        }
      }) as T;
    }
    
    然后在异步验证器中使用这个节流函数:
    import { AbstractControl, ValidationErrors, AsyncValidatorFn } from '@angular/forms';
    import { Observable } from 'rxjs';
    import { map, catchError } from 'rxjs/operators';
    import { UserService } from './user.service';
    
    const throttledCheckUserName = throttle((userService: UserService, enteredUserName: string) => {
      return userService.checkUserNameExists(enteredUserName);
    }, 500);
    
    export function asyncUserNameValidator(userService: UserService): AsyncValidatorFn {
      return (control: AbstractControl): Observable<ValidationErrors | null> => {
        return new Observable(observer => {
          const subscription = control.valueChanges
           .map(() => control.value)
           .map(enteredUserName => throttledCheckUserName(userService, enteredUserName))
           .map(response => response.exists? { userNameExists: true } : null)
           .catchError(error => {
              console.error('Error checking username:', error);
              return of({ backendError: true });
            })
           .subscribe(observer);
          return () => subscription.unsubscribe();
        });
      };
    }
    
    在这个例子中,throttledCheckUserName 函数被节流,每 500 毫秒最多执行一次 userService.checkUserNameExists 方法,从而避免频繁请求后端。

五、多个异步验证器的组合使用

在实际项目中,可能会遇到需要多个异步验证器同时工作的情况。例如,在注册表单中,可能需要同时验证用户名和邮箱的唯一性。

(一)在响应式表单中组合异步验证器

  1. 创建多个异步验证器 假设已经有两个异步验证器,asyncUserNameValidatorasyncEmailValidator,分别用于验证用户名和邮箱的唯一性。
    import { AbstractControl, ValidationErrors, AsyncValidatorFn } from '@angular/forms';
    import { Observable } from 'rxjs';
    import { map, catchError } from 'rxjs/operators';
    import { UserService } from './user.service';
    
    export function asyncUserNameValidator(userService: UserService): AsyncValidatorFn {
      return (control: AbstractControl): Observable<ValidationErrors | null> => {
        const enteredUserName = control.value;
        return userService.checkUserNameExists(enteredUserName)
         .pipe(
            map(response => response.exists? { userNameExists: true } : null),
            catchError(error => {
              console.error('Error checking username:', error);
              return of({ backendError: true });
            })
          );
      };
    }
    
    export function asyncEmailValidator(userService: UserService): AsyncValidatorFn {
      return (control: AbstractControl): Observable<ValidationErrors | null> => {
        const enteredEmail = control.value;
        return userService.checkEmailExists(enteredEmail)
         .pipe(
            map(response => response.exists? { emailExists: true } : null),
            catchError(error => {
              console.error('Error checking email:', error);
              return of({ backendError: true });
            })
          );
      };
    }
    
  2. 在响应式表单中使用多个异步验证器 在响应式表单组件中:
    import { Component } from '@angular/core';
    import { FormControl, FormGroup, Validators } from '@angular/forms';
    import { UserService } from './user.service';
    import { asyncUserNameValidator, asyncEmailValidator } from './async - validators';
    
    @Component({
      selector: 'app - registration - form',
      templateUrl: './registration - form.component.html',
      styleUrls: ['./registration - form.component.css']
    })
    export class RegistrationFormComponent {
      registrationForm: FormGroup;
    
      constructor(private userService: UserService) {
        this.registrationForm = new FormGroup({
          userName: new FormControl('', [Validators.required], [asyncUserNameValidator(userService)]),
          email: new FormControl('', [Validators.required, Validators.email], [asyncEmailValidator(userService)])
        });
      }
    }
    
    registrationForm 中,userNameemailFormControl 分别使用了对应的异步验证器。

(二)在模板驱动表单中组合异步验证器

  1. 在组件类中准备验证器 在模板驱动表单的组件类中:
    import { Component } from '@angular/core';
    import { AbstractControl, ValidationErrors, AsyncValidatorFn } from '@angular/forms';
    import { Observable } from 'rxjs';
    import { map, catchError } from 'rxjs/operators';
    import { UserService } from './user.service';
    
    export function asyncUserNameValidator(userService: UserService): AsyncValidatorFn {
      return (control: AbstractControl): Observable<ValidationErrors | null> => {
        const enteredUserName = control.value;
        return userService.checkUserNameExists(enteredUserName)
         .pipe(
            map(response => response.exists? { userNameExists: true } : null),
            catchError(error => {
              console.error('Error checking username:', error);
              return of({ backendError: true });
            })
          );
      };
    }
    
    export function asyncEmailValidator(userService: UserService): AsyncValidatorFn {
      return (control: AbstractControl): Observable<ValidationErrors | null> => {
        const enteredEmail = control.value;
        return userService.checkEmailExists(enteredEmail)
         .pipe(
            map(response => response.exists? { emailExists: true } : null),
            catchError(error => {
              console.error('Error checking email:', error);
              return of({ backendError: true });
            })
          );
      };
    }
    
    @Component({
      selector: 'app - template - registration - form',
      templateUrl: './template - registration - form.component.html',
      styleUrls: ['./template - registration - form.component.css']
    })
    export class TemplateRegistrationFormComponent {
      asyncUserNameValidator: AsyncValidatorFn;
      asyncEmailValidator: AsyncValidatorFn;
    
      constructor(private userService: UserService) {
        this.asyncUserNameValidator = asyncUserNameValidator(userService);
        this.asyncEmailValidator = asyncEmailValidator(userService);
      }
    }
    
  2. 在模板中使用多个异步验证器 在模板中:
    <form #registrationForm="ngForm">
      <div class="form - group">
        <label for="userName">User Name</label>
        <input type="text" id="userName" name="userName" [(ngModel)]="userName"
               required
               [asyncValidator]="asyncUserNameValidator"
               #userNameInput="ngModel">
        <div *ngIf="userNameInput.hasError('required') && (userNameInput.touched || userNameInput.dirty)">
          User name is required.
        </div>
        <div *ngIf="userNameInput.hasError('userNameExists') && (userNameInput.touched || userNameInput.dirty)">
          This user name already exists.
        </div>
        <div *ngIf="userNameInput.hasError('backendError') && (userNameInput.touched || userNameInput.dirty)">
          There was an error checking the user name. Please try again later.
        </div>
      </div>
      <div class="form - group">
        <label for="email">Email</label>
        <input type="email" id="email" name="email" [(ngModel)]="email"
               required
               [asyncValidator]="asyncEmailValidator"
               #emailInput="ngModel">
        <div *ngIf="emailInput.hasError('required') && (emailInput.touched || emailInput.dirty)">
          Email is required.
        </div>
        <div *ngIf="emailInput.hasError('emailExists') && (emailInput.touched || emailInput.dirty)">
          This email already exists.
        </div>
        <div *ngIf="emailInput.hasError('backendError') && (emailInput.touched || emailInput.dirty)">
          There was an error checking the email. Please try again later.
        </div>
      </div>
      <button type="submit" class="btn btn - primary">Submit</button>
    </form>
    
    通过这种方式,在模板驱动表单中也能方便地组合使用多个异步验证器。

六、异步验证器与路由导航

在 Angular 应用中,有时需要在用户进行路由导航时,对表单的异步验证状态进行处理。例如,如果表单正在进行异步验证,用户尝试导航离开,需要提示用户是否确认离开。

(一)使用 CanDeactivate 守卫

  1. 创建 CanDeactivate 守卫 首先创建一个 CanDeactivate 守卫,用于检查表单的异步验证状态。

    import { Injectable } from '@angular/core';
    import { CanDeactivate } from '@angular/router';
    import { AbstractControl, FormGroup } from '@angular/forms';
    import { Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    
    @Injectable({
      providedIn: 'root'
    })
    export class FormAsyncValidationGuard implements CanDeactivate<any> {
      canDeactivate(component: any): Observable<boolean> | boolean {
        const form: FormGroup = component.userForm;
        if (!form) {
          return true;
        }
        const asyncValidators = form.controls.userName.asyncValidators;
        if (!asyncValidators) {
          return true;
        }
        return new Observable(observer => {
          const subscription = asyncValidators[0](form.get('userName') as AbstractControl)
           .pipe(
              map(result => {
                if (result) {
                  return window.confirm('Form is still being validated. Are you sure you want to leave?');
                }
                return true;
              })
            )
           .subscribe(observer);
          return () => subscription.unsubscribe();
        });
      }
    }
    

    在上述代码中,FormAsyncValidationGuard 实现了 CanDeactivate 接口的 canDeactivate 方法。它检查组件中的表单是否存在异步验证器,如果存在,则等待异步验证完成,并根据验证结果弹出确认框询问用户是否离开。

  2. 在路由配置中使用守卫 在路由配置文件中:

    import { NgModule } from '@angular/core';
    import { RouterModule, Routes } from '@angular/router';
    import { UserFormComponent } from './user - form.component';
    import { FormAsyncValidationGuard } from './form - async - validation.guard';
    
    const routes: Routes = [
      {
        path: 'user - form',
        component: UserFormComponent,
        canDeactivate: [FormAsyncValidationGuard]
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule]
    })
    export class UserFormRoutingModule {}
    

    通过在路由配置中添加 canDeactivate 属性,并指定 FormAsyncValidationGuard,当用户尝试离开包含异步验证表单的路由时,会触发守卫逻辑。

通过以上全面深入的介绍,从异步验证器的基础概念、创建与使用,到与后端交互、性能优化、多个异步验证器组合使用以及与路由导航的结合,希望能帮助开发者在 Angular 前端开发中更好地应用异步验证器,提升表单验证的功能和用户体验。