上个小节中我们共同学习了如何自定义验证器,我们把上节中那种可以马上得到验证结果的验证器称为同步验证器。有些时候,我们在进行数据验证时,还需要去请求后台相关的API,比如后台不允许两个学生使用相同的学号,这时候验证器则需要后台的协助。我们把这种需要后台协助的验证器称为异步验证器。
## Api
当前后台提供了一个校验学号是否可用的方法,其API如下:
```bash
GET /student/numberIsExist
```
| **类型Type** | **名称Name** | **描述Description** | 必填 | **类型Schema** | 默认值 |
| :------------ | :----------- | :------------------ | ---- | :---------------------------------------------------- | ------ |
| Param请求参数 | `number` | 学号 | 是 | `string` | |
| Response响应 | | Status Code: 200 | | 学号已存在,则返回`true`;学号不存在,则返回`false`。 | |
### MockApi
依据API我们建立MockApi,模拟实现如下逻辑,传入的学号为`032282`时,则返回`true`,表示该学号已存在;传入其它的学号时,返回`false`,表示该学号尚不存在。
我们来到`mock-api`文件夹,新建`student.mock.api.ts`:
```bash
panjiedeMacBook-Pro:mock-api panjie$ pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app/mock-api
panjiedeMacBook-Pro:mock-api panjie$ touch student.mock.api.ts
panjiedeMacBook-Pro:mock-api panjie$ ls
clazz.mock.api.ts student.mock.api.ts
mock-api-testing.module.ts teacher.mock.api.ts
```
然后初始化如下:
```typescript
import {ApiInjector, MockApiInterface} from '@yunzhi/ng-mock-api';
/**
* 学生模拟API.
*/
export class StudentMockApi implements MockApiInterface {
getInjectors(): ApiInjector<any>[] {
return [];
}
}
```
最后加入模拟信息:
```typescript
export class StudentMockApi implements MockApiInterface {
getInjectors(): ApiInjector<any>[] {
return [{
method: 'GET',
url: '/student/numberIsExist',
result: (urlMatches: any, options: RequestOptions): boolean => {
const params = options.params as HttpParams;
if (!params.has('number')) {
throw new Error('未接收到查询参数number');
}
const stuNumber = params.get('number') as string;
if (stuNumber === '032282') {
return true;
} else {
return false;
}
}
}];
}
```
此时一个MockApi便建立完成了。若想使其生效,还要保证将其加入到`MockApiTestingModule`中:
```typescript
+++ b/first-app/src/app/mock-api/mock-api-testing.module.ts
@@ -4,6 +4,7 @@ import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {MockApiTestingInterceptor} from '@yunzhi/ng-mock-api/testing';
import {ClazzMockApi} from './clazz.mock.api';
import {TeacherMockApi} from './teacher.mock.api';
+import {StudentMockApi} from './student.mock.api';
@NgModule({
@@ -17,7 +18,8 @@ import {TeacherMockApi} from './teacher.mock.api';
provide: HTTP_INTERCEPTORS, multi: true,
useClass: MockApiTestingInterceptor.forRoot([
ClazzMockApi,
- TeacherMockApi
+ TeacherMockApi,
+ StudentMockApi
])
}
]
```
## 创建异步验证器
在`src/app`文件夹中创建一个`YzAsyncValidators`来存放所有的异步验证器:
```bash
panjiedeMacBook-Pro:app panjie$ pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app
panjiedeMacBook-Pro:app panjie$ ng g class YzAsyncValidators
CREATE src/app/yz-async-validators.spec.ts (200 bytes)
CREATE src/app/yz-async-validators.ts (35 bytes)
```
初始化的类如下:
```typescript
export class YzAsyncValidators {
}
```
与同步验证器相同,异步验证器同样需要符合某些规范。作为异步验证器的方法而言,该方法需要返回一个`Observable`,即可被观察的对象,如果最终该`Observable`发送的为`null`,则表示验证通过;如果该`Observable`最终发送的为`ValidationErrors`,则说明验证失败。
```typescript
import {AbstractControl, ValidationErrors} from '@angular/forms';
import {Observable, of} from 'rxjs';
import {delay} from 'rxjs/operators';
/**
* 异步验证器.
*/
export class YzAsyncValidators {
/**
* 验证方法,学号不存在验证通过
* @param control FormControl
*/
static numberNotExist(control: AbstractControl): Observable<ValidationErrors | null> {
return of(null) ①
.pipe②(delay(1000)③);
}
}
```
上述代码使用了几个小技巧:
- ① `of(null)`方法返回了一个可被订阅的(可)观察者`Observable`,该`Observable`发送的数据为`null`。
- ② 在发送数据以前,加入`pipe()`进行处理。
- ③ 处理的方法是延迟1秒钟再发送数据。
- 所以最终订阅该`Observable`将在1秒后得到一个值为`null`的数据,这个1秒的延迟模拟了后台的异步请求。
## 使用异步验证器
`FormControl`支持多个验证器,可以同时使用同步或异步验证器,同步与异步验证器的使用方法相同:
```typescript
+++ b/first-app/src/app/student/add/add.component.ts
@@ -1,6 +1,7 @@
import {Component, OnInit} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {YzValidators} from '../../yz-validators';
+import {YzAsyncValidators} from '../../yz-async-validators';
@Component({
selector: 'app-add',
@@ -10,7 +11,7 @@ import {YzValidators} from '../../yz-validators';
export class AddComponent implements OnInit {
formGroup = new FormGroup({
name: new FormControl('', Validators.required),
- number: new FormControl('', Validators.required),
+ number: new FormControl('', Validators.required, YzAsyncValidators.numberNotExist👈),
phone: new FormControl('', YzValidators.phone),
email: new FormControl(),
clazzId: new FormControl(null, Validators.required)
```
异步验证器,放到`FormControl`的第三个参数上👈。**如果**只存在异步验证器而不存在同步验证器,则可以向第二个参数传入一个空数组`number: new FormControl('', []👈, YzAsyncValidators.numberNotExist)`。
接下来让我们在异步验证器上打两个断点,来查看异步验证器的调用时机:
```typescript
static numberNotExist(control: AbstractControl): Observable<ValidationErrors | null> {
+ console.log('异步验证器被调用');
return of(null)
.pipe(delay(1000), tap(data => console.log('验证器返回数据', data));
}
```
启用学生添加组件对应的单元测试:
测试结果一:组件启动时,学号为空,此时异步验证器未被调用。得出结论:①异步验证器在组件启动时不工作或②异步验证器在组件值为空时不工作。
测试结果二:输入学号后,异步验证器工作,并在1S后接收到返回null时,此时错误提示消失,说明学号对应的`FormControl`的`invalid`值为`false`。得出结论:③异步验证器在组件非空时工作。
![image-20210413112641398](https://img.kancloud.cn/df/95/df952468070cb04919476a7142c6fd7f_2366x144.png)
测试结果三:填写学号,然后删除学号,异步验证器不工作。得出结论:④异步验证器在组件值为空时不工作,所以测试结果一中得出的结论①可能是错误的。
测试结果四:输入学号后,异步验证器开始工作,在尚未返回数据期间,保存按钮处于可用状态。得出结论:⑤异步验证器在未接收到后台的返回值前,会将表单对应的`FormGroup`的`invalid`值置于`true`。
![image-20210413112850653](https://img.kancloud.cn/15/f6/15f64b19166b881b60330d84f722ee3c_620x166.png)
测试结果五:快速的输入学号,比如输入1234567,异步验证器将被调用7次,但只会订阅一个返回结果。得出结论:⑥`FormControl`的值每改变一次,则会调用一次异步验证器,但如果异步验证器没有及时地接收到后台的结果,则只会获取最后一次的值。
![image-20210413113312856](https://img.kancloud.cn/71/61/71617b64c06df2222218d8a83dad6a24_1000x112.png)
上述结论只是我们根据现像的猜想,其实大多数时候猜想如果能解决我们当下遇到的问题,也是完完全全可以的。而如果能在猜想以后再了解到真实的原因,则会对我们知识的提升大有帮助!
真实的原因是这样:
- 当`FormControl`即存在同步验证器,又存在异步验证器时。只有当所有的同步验证器全部通过后,才会调用异步验证器。所以组件初始化时异步验证器未调用的真实原因是:存在同步验证器`Validators.required`,当内容为空时,该验证器未通过,所以异步验证器未被调用。
- 在异步验证器发起请求而未接收到返回值前,`FormControl`的`pending`字段的值将被设置为`true`,接收到返回值后,`FormControl`的`pending`字段的值将被设置为`false`。
- 一个`FormGroup`中的任意`FormControl`的`pending`值为`true`时,该`FormGroup`中的`pengding`为`true`。
- `FormGroup`中的`pending`值为`true`时,其`invalid`值为`false`。
- 异步验证器在进行验证时,将忽略历史上尚未返回的请求值,只使用最近一次请求成功的值。
以上便是发生上述问题的原因。
## 返回类型为方法
真实的异步验证器需要进行后台的请求,所以必然需要`HttpClient`来帮忙发起请求。所以我们在验证器上需要注入`HttpClient`:
```typescript
export class YzAsyncValidators {
+
+ constructor(private httpClient: HttpClient) {
+ }
+
```
但在`numberNotExist`方法上使用`httpClient`则会碰到一个根本的问题:无法在静态方法上调用对象中的属性:
![image-20210413134910123](https://img.kancloud.cn/b0/10/b010155968180b43601992ee062a3ef1_2014x348.png)
若想在异步验证器上使用注入到对象的`HttpClient`,则需要一些小技巧:
```typescript
export class YzAsyncValidators {
constructor(private httpClient: HttpClient) {
}
/**
* 验证方法,学号不存在验证通过
* @param control FormControl
*/
👉 numberNotExist(): (control: AbstractControl) => Observable<ValidationErrors | null> ① {
return ②(control: AbstractControl): Observable<ValidationErrors | null> => {
console.log(this.httpClient);
console.log('异步验证器被调用');
return of(null)
.pipe(delay(1000), tap(data => console.log('验证器返回数据', data)));
};
}
}
```
- 👉该方法不再声明为`static`
- ① 将返回值的类型设置为**方法**。该方法接收一个参数,类型为`AbstractControl`,该方法的返回值类型为:`Observable<ValidationErrors | null>`
- ② `return`返回一个**方法**,**该方法**的参数类型与返回值类型与`numberNotExist()`方法声明的**返回方法**相同。
如果你是第一次将某个方法做为返回值可能会有些不适应。其实返回值类型为方法或是对象或是普通的类型并没什么不同:
```typescript
// 返回值类型为number
a(): number {
return 1;
}
// 返回值类型为string
a(): string {
return '1';
}
// 返回值类型为对象
a(): {a: number} {
return {a: 123}
}
// 返回值类型为方法。返回方法的参数为空,返回方法的返回值类型为number
a(): () => number {
return (): number => {
return 3;
}
}
// 返回值类型为方法。返回方法的参数为空,返回方法的返回值类型为string
a(): () => string {
return (): string => {
return '3';
}
}
// 返回值类型为方法。返回方法的参数有1个,参数类型为number,返回方法的返回值类型为number
a(): (a: number) => number {
return (a: number): number => {
return a + a;
}
}
// 返回值类型为方法。返回方法的参数有1个,参数类型为number,返回方法的返回值类型为string
a(): (a: number) => string {
return (a: number): string => {
return a.toString();
}
}
```
## 单元测试
在`YzAsyncValidators`的构造函数中加入`HttpClient`后,原来的单元测试会一个构造函数异常:
```typescript
describe('YzAsyncValidators', () => {
it('should create an instance', () => {
expect(new YzAsyncValidators()).toBeTruthy();
});
});
```
出错的原因也很简单:我们刚刚为`YzAsyncValidators`的构造函数指定了一个`HttpClient`参数,但在单元测试时进行实例化时却没有传入这个参数。下面让我们启用该单元测试,获取一个`HttpClient`实例后解决该错误:
```typescript
+++ b/first-app/src/app/yz-async-validators.spec.ts
@@ -1,7 +1,16 @@
import { YzAsyncValidators } from './yz-async-validators';
+import {TestBed} from '@angular/core/testing';
+import {HttpClient, HttpClientModule} from '@angular/common/http';
describe('YzAsyncValidators', () => {
- it('should create an instance', () => {
- expect(new YzAsyncValidators()).toBeTruthy();
+ fit('should create an instance', async👈 () => {
+ // 配置动态测试模块
+ await TestBed.configureTestingModule({
+ imports: [HttpClientModule]
+ });
+ // 获取动态测试模块中可被注入的HttpClient实例
+ const httpClient = TestBed.inject(HttpClient);
+
+ expect(new YzAsyncValidators(httpClient)).toBeTruthy();
});
});
```
- 在此偷偷的加入了与`await`配合使用的`async`关键字
单元测试错误消失:
![image-20210413151249679](https://img.kancloud.cn/dc/1b/dc1b644506f50489aa4ad95dc6305051_746x102.png)
## 应用验证器
调用`YzAsyncValidators`上的`numberNotExist()`则可获取到一个异步验证器。去除`numberNotExist()`方法前的`static`关键字后,该方法由一个类的静态方法变更为了一个对象的非静态方法,所以以下代码已经不适用了:
```typescript
number: new FormControl('', Validators.required, YzAsyncValidators.numberNotExist👈),
```
为此,我们删除实例化中的相关代码:
```typescript
+++ b/first-app/src/app/student/add/add.component.ts
formGroup = new FormGroup({
name: new FormControl('', Validators.required),
- number: new FormControl('', Validators.required, YzAsyncValidators.numberNotExist),
+ number: new FormControl('', Validators.required),
phone: new FormControl('', YzValidators.phone),
email: new FormControl(),
clazzId: new FormControl(null, Validators.required)
```
当`numberNotExist()`方法变更为对象的方法后,若想调用该方法,则首先需要一个对象,然后将该对象`numberNotExist()`方法值应用到`FormControl`上。为此,我们对组件的代码进行简单的改造:
```typescript
export class AddComponent implements OnInit {
formGroup: FormGroup;
constructor() {
this.formGroup = new FormGroup({
name: new FormControl('', Validators.required),
number: new FormControl('', Validators.required),
phone: new FormControl('', YzValidators.phone),
email: new FormControl(),
clazzId: new FormControl(null, Validators.required)
});
}
```
然后我们尝试获取一个`YzAsyncValidators`实例,并将其方法的返回值做为异步验证器来设置学号对应的`FormControl`:
```typescript
+++ b/first-app/src/app/student/add/add.component.ts
@@ -1,6 +1,7 @@
import {Component, OnInit} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {YzValidators} from '../../yz-validators';
+import {YzAsyncValidators} from '../../yz-async-validators';
@Component({
selector: 'app-add',
@@ -11,9 +12,10 @@ export class AddComponent implements OnInit {
formGroup: FormGroup;
constructor() {
+ const yzAsyncValidators = null as unknown as YzAsyncValidators; 👈
this.formGroup = new FormGroup({
name: new FormControl('', Validators.required),
- number: new FormControl('', Validators.required),
+ number: new FormControl('', Validators.required, yzAsyncValidators.numberNotExist()①),
phone: new FormControl('', YzValidators.phone),
email: new FormControl(),
clazzId: new FormControl(null, Validators.required)
```
- ① 这是使用的是`yzAsyncValidators.numberNotExist()`而非`yzAsyncValidators.numberNotExist`
- 我们在这里使用了`as unknown as YzAsyncValidators`进行类型的强制转换,这使得原本为`null`的值被做为了`YzAsyncValidators`类型来处理。这明显是有风险的,在启用单元测试对组件进行测试时,则会显露出该风险点:👈
![image-20210413151503548](https://img.kancloud.cn/7e/2a/7e2ad2baa4832015b94c2e938f39f85c_1456x214.png)
上图报了一个类型错误说:你不是说`yzAsyncValidators`的类型是`YzAsyncValidators`吗?怎么在这个类型上执行①`numberNotExist()`方法时却不行?所以在开发过程中,除非我们对类型非常的有把握,否则一定**不**要使用`as unknown as Xxxx`来进行强制转换。
好了,暂时就到这里。下一节中,我们来展示如何在组件中注入`YzAsyncValidators`并完善一下细节。
| 名称 | 链接 |
| -------------- | ------------------------------------------------------------ |
| 创建异步验证器 | [https://angular.cn/guide/form-validation#creating-asynchronous-validators](https://angular.cn/guide/form-validation#creating-asynchronous-validators) |
| 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step7.2.3.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.2.3.zip) |
- 序言
- 第一章 Hello World
- 1.1 环境安装
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教师管理
- 2.1 教师列表
- 2.1.1 初始化原型
- 2.1.2 组件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 请求后台数据
- 2.2.1 HttpClient
- 2.2.2 请求数据
- 2.2.3 模块与依赖注入
- 2.2.4 异步与回调函数
- 2.2.5 集成测试
- 2.2.6 本章小节
- 2.3 新增教师
- 2.3.1 组件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 对接后台
- 2.3.4 路由
- 2.4 编辑教师
- 2.4.1 组件初始化
- 2.4.2 获取路由参数
- 2.4.3 插值与模板表达式
- 2.4.4 初识泛型
- 2.4.5 更新教师
- 2.4.6 测试中的路由
- 2.5 删除教师
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome图标库
- 2.6.3 firefox
- 2.7 总结
- 第三章 用户登录
- 3.1 初识单元测试
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 着陆组件
- 3.5 @Output
- 3.6 TypeScript 类
- 3.7 浏览器缓存
- 3.8 总结
- 第四章 个人中心
- 4.1 原型
- 4.2 管道
- 4.3 对接后台
- 4.4 x-auth-token认证
- 4.5 拦截器
- 4.6 小结
- 第五章 系统菜单
- 5.1 延迟及测试
- 5.2 手动创建组件
- 5.3 隐藏测试信息
- 5.4 规划路由
- 5.5 定义菜单
- 5.6 注销
- 5.7 小结
- 第六章 班级管理
- 6.1 新增班级
- 6.1.1 组件初始化
- 6.1.2 MockApi 新建班级
- 6.1.3 ApiInterceptor
- 6.1.4 数据验证
- 6.1.5 教师选择列表
- 6.1.6 MockApi 教师列表
- 6.1.7 代码重构
- 6.1.8 小结
- 6.2 教师列表组件
- 6.2.1 初始化
- 6.2.2 响应式表单
- 6.2.3 getTestScheduler()
- 6.2.4 应用组件
- 6.2.5 小结
- 6.3 班级列表
- 6.3.1 原型设计
- 6.3.2 初始化分页
- 6.3.3 MockApi
- 6.3.4 静态分页
- 6.3.5 动态分页
- 6.3.6 @Input()
- 6.4 编辑班级
- 6.4.1 测试模块
- 6.4.2 响应式表单验证
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定义FormControl
- 6.4.6 代码重构
- 6.4.7 小结
- 6.5 删除班级
- 6.6 集成测试
- 6.6.1 惰性加载
- 6.6.2 API拦截器
- 6.6.3 路由与跳转
- 6.6.4 ngStyle
- 6.7 初识Service
- 6.7.1 catchError
- 6.7.2 单例服务
- 6.7.3 单元测试
- 6.8 小结
- 第七章 学生管理
- 7.1 班级列表组件
- 7.2 新增学生
- 7.2.1 exports
- 7.2.2 自定义验证器
- 7.2.3 异步验证器
- 7.2.4 再识DI
- 7.2.5 属性型指令
- 7.2.6 完成功能
- 7.2.7 小结
- 7.3 单元测试进阶
- 7.4 学生列表
- 7.4.1 JSON对象与对象
- 7.4.2 单元测试
- 7.4.3 分页模块
- 7.4.4 子组件测试
- 7.4.5 重构分页
- 7.5 删除学生
- 7.5.1 第三方dialog
- 7.5.2 批量删除
- 7.5.3 面向对象
- 7.6 集成测试
- 7.7 编辑学生
- 7.7.1 初始化
- 7.7.2 自定义provider
- 7.7.3 更新学生
- 7.7.4 集成测试
- 7.7.5 可订阅的路由参数
- 7.7.6 小结
- 7.8 总结
- 第八章 其它
- 8.1 打包构建
- 8.2 发布部署
- 第九章 总结