Angular表单验证的异步验证器应用
一、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 中是一种特殊类型的验证器,它返回一个 Promise
或 Observable
。Angular 会等待这个 Promise
被解决或 Observable
发出值,以此来确定验证结果。这种特性使得异步验证器非常适合处理需要时间来完成的验证任务,比如与后端服务器通信验证数据的唯一性。
(二)创建异步验证器
-
使用
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
操作符创建一个Observable
,delay
操作符模拟网络延迟,最后map
操作符根据检查结果返回验证错误对象或null
。 -
在响应式表单中使用异步验证器 创建好异步验证器后,就可以在响应式表单中使用它。
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
中,userName
的FormControl
不仅使用了同步的required
验证器,还使用了我们自定义的异步验证器asyncUserNameValidator
。注意,异步验证器是作为第三个参数传递给FormControl
构造函数的。 -
在模板驱动表单中使用异步验证器 在模板驱动表单中使用异步验证器稍微复杂一些,需要借助
ngForm
和FormControlName
指令。 首先,在组件类中定义验证器: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
,请求体包含用户名。
-
创建服务 首先创建一个服务来处理与后端的通信:
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
方法接受一个用户名作为参数,通过HttpClient
的post
方法向后端发送请求,并返回一个Observable
,该Observable
发出的对象包含一个exists
属性,用于表示用户名是否已存在。 -
更新异步验证器 然后更新异步验证器,使用这个服务来进行实际的验证:
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
作为参数,并在验证逻辑中调用UserService
的checkUserNameExists
方法,根据后端返回的结果确定验证是否通过。 -
在组件中使用 最后在组件中使用这个更新后的异步验证器:
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
,从而实现与后端的异步验证交互。
(二)处理后端响应错误
在与后端交互过程中,可能会遇到各种错误,比如网络故障、后端服务异常等。需要在异步验证器中妥善处理这些错误情况。
-
使用
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
错误标识的验证错误对象,这样在表单中可以根据这个错误标识显示相应的错误信息。 -
在模板中显示错误信息 在模板中添加错误信息显示:
<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)
当用户在输入框中快速输入时,频繁触发异步验证可能会导致性能问题,特别是与后端交互时。可以使用防抖技术来解决这个问题。
- 使用
rxjs
的debounceTime
操作符 在异步验证器中应用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)
节流也是一种优化性能的方式,它限制异步验证在一定时间间隔内只能执行一次。
- 自定义节流函数
首先创建一个自定义的节流函数:
然后在异步验证器中使用这个节流函数: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
方法,从而避免频繁请求后端。
五、多个异步验证器的组合使用
在实际项目中,可能会遇到需要多个异步验证器同时工作的情况。例如,在注册表单中,可能需要同时验证用户名和邮箱的唯一性。
(一)在响应式表单中组合异步验证器
- 创建多个异步验证器
假设已经有两个异步验证器,
asyncUserNameValidator
和asyncEmailValidator
,分别用于验证用户名和邮箱的唯一性。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 }); }) ); }; }
- 在响应式表单中使用多个异步验证器
在响应式表单组件中:
在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
中,userName
和email
的FormControl
分别使用了对应的异步验证器。
(二)在模板驱动表单中组合异步验证器
- 在组件类中准备验证器
在模板驱动表单的组件类中:
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); } }
- 在模板中使用多个异步验证器
在模板中:
通过这种方式,在模板驱动表单中也能方便地组合使用多个异步验证器。<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 守卫
-
创建 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
方法。它检查组件中的表单是否存在异步验证器,如果存在,则等待异步验证完成,并根据验证结果弹出确认框询问用户是否离开。 -
在路由配置中使用守卫 在路由配置文件中:
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 前端开发中更好地应用异步验证器,提升表单验证的功能和用户体验。