#  教师选择列表 前面的小节中选择班主任时,我们手写了张三、李四两个教师信息。显然这与实际的需求是不相符的。在教师选择组件中,我们希望显示当前系统的所有教师。 本节我们将在MockApi的支持下,模拟由后台获取所有教师数据,然后在此基础上,打造一个真实的教师选择列表。 ## 初始化 开发新的组件离不开原型的初始化,开发新功能也是如此,这有利于我们掌握真实的功能需求。 ## C层初始化 教师列表组件需要显示所有的教师信息,所以我们在C层初始化一个教师的数组: ```typescript +++ b/first-app/src/app/clazz/add/add.component.ts @@ -13,6 +13,8 @@ export class AddComponent implements OnInit { teacherId: null as unknown as number }; + teachers = []; + constructor(private httpClient: HttpClient) { } ``` 既然是初始化,我们还是希望能够看到一些效果的。根据当前V层的教师列表相关代码: ```html <select id="teacher" class="form-control" name="teacher" [(ngModel)]="clazz.teacherId"> <option [ngValue]="1">张三</option> <option [ngValue]="2">李四</option> </select> ``` 我们确认一个教师列表选择功能,需要教师的两个属性:1. 教师id;2.教师姓名。据此,我们在C层中初始化这两项信息: ```typescript - teachers = []; + teachers = [ + { + id: 1, + name: '张三' + }, + { + id: 2, + name: '李四' + } + ]; ``` 直接使用`[]`来对数组初始化显示比较方法,但这样做却使TypeScript强类型的优势完全失效。所以在定义数组时,正确的做法应该是 `teachers = new Array<Teacher>;` ```typescript import {Teacher} from '../../entity/teacher'; teachers = new Array<Teacher>( { id: 1, name: '张三' }, { id: 2, name: '李四' } ); ``` TypeScript的强类型能够防止我们犯一些拼写方面的错误,比如我们在此将数组的元素类型约束为Teacher后,会在IDE下发现如下错误提醒: ![image-20210322101959790](https://img.kancloud.cn/1b/f0/1bf047ec894f3e05866688be47acf6d4_2108x524.png) 这是在告诉我们:当前数组中的元素类型应该为Teacher,则其中的每一项的类型都应该是Teacher。Teacher的类型中除了`id`,`name`属性以外,还存在着`email` 、`password`、`sex`、`username`属性。 消除该提示有多种方法,最简单方法是加入`email`等属性,比如: ```typescript { id: 1, name: '张三', email: 'zhangsan@yunzhi.club', username: 'zhangsan', sex: true, password: '' }, ``` 但是我们明明只需要`id`,`name`就可以了,初始化时加入其它不需要的属性便是画蛇添足了。在这种仅需要某几个属性的情况下,我们往往使用`as`来进行类型的转换来消除此类错误。 ```typescript teachers = new Array<Teacher>( { id: 1, name: '张三' } as Teacher, { id: 2, name: '李四' } as Teacher ); ``` ## V层初始化 V层的实现则可以使用我们前面学习过的`ngFor`指令: ```html +++ b/first-app/src/app/clazz/add/add.component.html @@ -16,8 +16,9 @@ <select id="teacher" class="form-control" name="teacher" [(ngModel)]="clazz.teacherId"> - <option [ngValue]="1">张三</option> - <option [ngValue]="2">李四</option> + <option *ngFor="let teacher of teachers" [ngValue]="teacher.id"> + {{teacher.name}} + </option> </select> <small class="text-danger" *ngIf="clazz.teacherId === null"> ``` ## 测试 使用`ng t`在`add.component.spec.ts`或`add.component.mock-api.spec.ts`的支持下,对上述代码进行测试。比如我们以`add.component.spec.ts`做为测试文件,将`it`变更为`fit`: ```typescript fit('should create', () => { expect(component).toBeTruthy(); fixture.autoDetectChanges(); }); ``` ![image-20210322103940085](https://img.kancloud.cn/2f/67/2f67c22ec0408ed9af79b21ac2940fa1_706x290.png) 在开发某个组件时,应该时刻保持仅有一个测试用例生效,也就是整个项目中,最多存在一个`fit`。 ![image-20210322104045306](https://img.kancloud.cn/09/bd/09bde2516aa6e5347dd04a1eb4cf96e0_2410x208.png) ## 对接后台 初始化完成了、效果也有了,接下来便是正式的对接后台。既然是正式对接后台,那么C层中则仅需要初始化一个空的教师数组即可: ```typescript +++ b/first-app/src/app/clazz/add/add.component.ts @@ -14,16 +14,7 @@ export class AddComponent implements OnInit { teacherId: null as unknown as number }; - teachers = new Array<Teacher>( - { - id: 1, - name: '张三' - } as Teacher, - { - id: 2, - name: '李四' - } as Teacher - ); + teachers = new Array<Teacher>(); constructor(private httpClient: HttpClient) { } ``` 接下来调用后台API获取全部教师。获取所有教师的API我们在前面的章节中已经给出了,为`GET /teacher`。在Angular中,若想在组件初始化后执行某些代码,则仅需要将该代码添加到`ngOnInit`方法中(这也是已经讲过的知识,相信你一定还记得): ```typescript +++ b/first-app/src/app/clazz/add/add.component.ts @@ -20,6 +20,9 @@ export class AddComponent implements OnInit { } ngOnInit(): void { + // 获取所有教师 + this.httpClient.get('teacher') + .subscribe(teachers => this.teachers = teachers); } ``` 上述代码从理论上来讲是没有任何问题的:我们通过GET方法来获取全部的教师数据,然后在将其赋值给当前组件的teachers属性。但是TypeScript却读不懂我们心中所想,由于`httpClient.get`返回的数据类型被定义为`Object`,所以以下代码实际上是在将一个类型为`Object`的变量赋值为类型为`Array<Teacher>`的变量。 ```typescript this.httpClient.get('teacher') .subscribe(teachers 类型为Object) => this.teachers 类型为Array<Teacher> = teachers 类型为Object); ``` `Object`类型是个0属性的对象,比如`{}`的类型便是`Object` ,在面向对象的世界里,它是最**小**的小弟。所以只要是个对象,便可以转换为Object,而Object却不能转换为其它任意非Object类型。 TypeScript聪明的发现了这一点,它在告诉我们说:类型Object这个最小的小弟,怎么能转换为`Array<Teacher>`这个大哥呢? ![image-20210322105832384](https://img.kancloud.cn/d8/77/d877f7183b78aa6702b2236f5bbe21a7_2340x266.png) 为了解决这一问题,httpClient除提供了`get()`方法外,还提供了一个可以指定返回值类型的`get<T>()`: ```typescript // 获取所有教师 this.httpClient.get<Array<Teacher>>('teacher') .subscribe(teachers => this.teachers = teachers); ``` 此时TypeScript就明白了:原来在此处进行`get`方法的请求,将得到一个类型为`Array<Teacher>`的返回数据。该类型与当前组件声明的`teachers`类型相同,错误自然也就消失了。 此时在`add.component.spec.ts`测试文件的支持下,打开浏览器控制台,发现发起了真实的网络请求,并且在教师列表中显示了后台拥有的教师列表: ![image-20210322110456557](https://img.kancloud.cn/df/e9/dfe9e09b58e849c03db59475becbe02b_994x214.png) ![image-20210322104045306](https://img.kancloud.cn/09/bd/09bde2516aa6e5347dd04a1eb4cf96e0_2410x208.png) **注意**:我们在这里所说的**返回值**,并不是指函数的返回值,而是特指在发起http请求时,后台返回来的值。 ## Mock测试 我们刚刚非常**幸运**的利用真实的后台API完成了教师选择的功能。之所以说幸运,是由于后台这个接口即不需要先完成用户登录,又不需要满足任何的前提。但真实的生产环境中,我们却很少碰到这样的情况。如果是在MockApi的情况下,又该如何测试呢? 为此我们移除`add.component.spec.ts`的`fit`,启用`add.component.mock-api.spec.ts`的相关测试用例: ```typescript +++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts @@ -34,7 +34,7 @@ describe('clazz add with mockapi', () => { component.onSubmit(); }); - it('should create', () => { + fit('should create', () => { fixture.autoDetectChanges(); }); }); ``` 然后我们简单思索一会,想像一下可能会发生什么样的错误,又为什么会发生这样的错误。思索完成后打开控制台来验证自己的错误并尝试解决。 | 名称 | 地址 | 备注 | | ---------------------- | ------------------------------------------------------------ | -------- | | TypeScript基础类型 | [https://www.tslang.cn/docs/handbook/basic-types.html](https://www.tslang.cn/docs/handbook/basic-types.html) | 必学内容 | | 请求输入一个类型的响应 | [https://angular.cn/guide/http#requesting-a-typed-response](https://angular.cn/guide/http#requesting-a-typed-response) | | | 本节源码 | [[https://github.com/mengyunzhi/angular11-guild/archive/step6.1.5.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.1.5.zip) | |