更新操作实质上是由获取要新的数据以及更新数据两部分组成的,在组件初始化后根据路由的ID值由后台获取要更新的学生信息,并将其显示出来;待用户修改完要更新的学生信息后,再把预更新的学生信息发送给后台,从而完成学生的更新操作。 ## 获取学生 在由后台获取学生前,需要先需要路由来获取学生ID: ```typescript +++ b/first-app/src/app/student/edit/edit.component.ts @@ -2,6 +2,9 @@ import {Component, OnInit} from '@angular/core'; import {FormControl, FormGroup, Validators} from '@angular/forms'; import {YzValidators} from '../../yz-validators'; import {YzAsyncValidators} from '../../yz-async-validators'; +import {ActivatedRoute} from '@angular/router'; +import {StudentService} from '../../service/student.service'; +import {Assert} from '@yunzhi/ng-mock-api'; @Component({ selector: 'app-edit', @@ -10,8 +13,11 @@ import {YzAsyncValidators} from '../../yz-async-validators'; }) export class EditComponent implements OnInit { formGroup: FormGroup; + id: number | undefined; - constructor(private yzAsyncValidators: YzAsyncValidators) { + constructor(private yzAsyncValidators: YzAsyncValidators, + private activatedRoute: ActivatedRoute, + private studentService: StudentService) { this.formGroup = new FormGroup({ name: new FormControl('', Validators.required), number: new FormControl('', Validators.required, yzAsyncValidators.numberNotExist()), @@ -22,8 +28,31 @@ export class EditComponent implements OnInit { } ngOnInit(): void { + this.id = +this.activatedRoute.snapshot.params.id; + Assert.isNumber(this.id, '接收到的id类型不正确'); + this.loadData(this.id); + }); } + onSubmit(): void { } + + /** + * 根据ID加载学生信息 + * @param id 学生ID + */ + loadData(id: number): void { + this.studentService.getById(id) + .subscribe(student => { + this.formGroup.setValue({ + name: student.name, + number: student.number, + phone: student.phone, + email: student.email, + clazzId: student.clazz.id + }); + }); + } } ``` 上述代码我们通过获取`ActivatedRoute`上的`snapshot.params.id`来获取对应的路由参数。由于当前组件注入了`ActivatedRoute`,所以在执行单元测试时将得到一个注入错误: ![image-20210609152357311](https://img.kancloud.cn/94/1e/941e6ef006b2a687a2a9fc082e4cc04c_1640x134.png) ## Providers 历史上我们采用引入`RouterTestingModule` 的方式来解决此错误,由于`RouterTestingModule`拥有提供`ActivatedRoute`的能力,所以当我们当其引用时当前动态测试模块便拥有了提供`ActivatedRoute`的能力,进而错误消失。 在此我们通过一种自定义`ActivatedRoute`提供者(provider)的方法来完成组件中`ActivatedRoute`的注入。自定义`ActivatedRoute`可以使我们在单元测试更加轻松的对`ActivatedRoute`进行模拟,从而达到在单元测试中快速模拟路由的作用。 在7.3.1小节中我们已经初步学习了如何使用`providers`来提供相关能力。结合前面我们学习过的公有与私有的概念,一个Angular的模块在可见性上大体上应该是这样的: ![image-20210609155134673](https://img.kancloud.cn/4c/ad/4cada5e94e0a8a60887554a405dbb1d4_2750x380.png) 总结如下: 1. 模块由组件、指令、管道及服务四大部分组成。 2. 组件、指令、管道默认为私有,通过`declarations`来声明。 3. 服务默认为公有,通过`providers`来声明。 4. 模块间的引用通过`imports`来声明。 5. 引用某个模块则相当于拥有了该模块声明为公有的资源(组件、指令、管道和服务)。 6. 当前模块将引用的资源默认为设置私有。 7. `exports`关键字的作用即是将私有的资源设置为公有,无论该资源是当前模块自己拥有的还是当前模块由其它模块引入进来的。 在拥有的能力上,则是这样的: ![image-20210609155303741](https://img.kancloud.cn/2a/42/2a42e86a0598e3cf7475ba3db42de1ff_2742x376.png) ### 冲突 有时候多个模块可能会提供相同的能力,比如`RouterModule`与`RouterTestingModule`均有提供`ActivatedRoute`的能力,那么当我们引两个模块则会发生提供能力的冲突: ```typescript +++ b/first-app/src/app/student/edit/edit.component.spec.ts @@ -3,6 +3,8 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {EditComponent} from './edit.component'; import {MockApiTestingModule} from '../../mock-api/mock-api-testing.module'; import {getTestScheduler} from 'jasmine-marbles'; +import {RouterTestingModule} from '@angular/router/testing'; +import {RouterModule} from '@angular/router'; describe('EditComponent', () => { let component: EditComponent; @@ -10,7 +12,7 @@ describe('EditComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MockApiTestingModule], + imports: [MockApiTestingModule, RouterModule, RouterTestingModule], declarations: [EditComponent] }) ``` 如上我们引用两个有提供`ActivatedRoute`的模块,但单元测试并没有报相关的**冲突**异常。这是因为当Angular在`imports`中检测到某个能力有多个提供者时,将使用最后台的提供者来替换前面的。 所以即使上述代码在进行`imports` 时后出现的`RouterTestingModule`实际上起到了提供`ActivatedRoute`的作用。 ## provider 再观察下图不难发现,某个模块的能力除了取决于`imports`外,还取决于其`providers`。 ![image-20210609155303741](https://img.kancloud.cn/2a/42/2a42e86a0598e3cf7475ba3db42de1ff_2742x376.png) 当在`imports`及`porviders`中提供的能力发生冲突时,`providers`将替换`imports`中的相应能力。所以我们还可以在`porviders`增加一个提供`ActivatedRoute`的能力: ```typescript +++ b/first-app/src/app/student/edit/edit.component.spec.ts @@ -4,7 +4,7 @@ import {EditComponent} from './edit.component'; import {MockApiTestingModule} from '../../mock-api/mock-api-testing.module'; import {getTestScheduler} from 'jasmine-marbles'; import {RouterTestingModule} from '@angular/router/testing'; -import {RouterModule} from '@angular/router'; +import {ActivatedRoute, RouterModule} from '@angular/router'; describe('EditComponent', () => { let component: EditComponent; @@ -12,8 +12,11 @@ describe('EditComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MockApiTestingModule, RouterTestingModule, RouterModule], - declarations: [EditComponent] + imports: [MockApiTestingModule, RouterModule, RouterTestingModule], + declarations: [EditComponent], + providers: [ + {provide: ActivatedRoute, useValue: {id: 'yunzhi'}} + ] }) .compileComponents(); }); ``` 然后我们当试在编辑组件中打印注入的`ActivatedRoute`: ```typescript +++ b/first-app/src/app/student/edit/edit.component.ts @@ -18,6 +18,7 @@ export class EditComponent implements OnInit { constructor(private yzAsyncValidators: YzAsyncValidators, private activatedRoute: ActivatedRoute, private studentService: StudentService) { + console.log(this.activatedRoute); this.formGroup = new FormGroup({ name: new FormControl('', Validators.required), number: new FormControl('', Validators.required, yzAsyncValidators.numberNotExist()), ``` 控制台打印信息如下: ![image-20210609161010567](https://img.kancloud.cn/af/01/af012b45b50d6810d3db4935ef1f17f1_1070x194.png) 总结如下: 1. 可使用`imports`及`porviders`的方式来声明模块拥有的能力。 2. 当多个模块的提供的能力冲突时,以最后提供的那个为准。 3. 当`imports`及`providers`提供的能力冲突时,以`providers`提供的能力为准。 下面我们开始利用这个特性,来模拟提供路由上的`snapshot`对象。 ## 单元测试 当前单元测试尚存在两个异常: ![image-20210609161359771](https://img.kancloud.cn/e2/99/e299e5a2d522c24a34d5891a3ad053a8_2062x150.png) 两个异常发生的原因均是未引入相关模块: ```typescript +++ b/first-app/src/app/student/edit/edit.component.spec.ts @@ -5,6 +5,8 @@ import {MockApiTestingModule} from '../../mock-api/mock-api-testing.module'; import {getTestScheduler} from 'jasmine-marbles'; import {RouterTestingModule} from '@angular/router/testing'; import {ActivatedRoute, RouterModule} from '@angular/router'; +import {ClazzSelectModule} from '../../clazz/clazz-select/clazz-select.module'; +import {ReactiveFormsModule} from '@angular/forms'; describe('EditComponent', () => { let component: EditComponent; @@ -12,7 +14,8 @@ describe('EditComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MockApiTestingModule, RouterModule, RouterTestingModule], + imports: [MockApiTestingModule, RouterModule, RouterTestingModule, + ClazzSelectModule, ReactiveFormsModule], declarations: [EditComponent], providers: [ {provide: ActivatedRoute, useValue: {id: 'yunzhi'}} ``` 再次进行单元测试,将出现如下错误: ![image-20210609161542641](https://img.kancloud.cn/88/e6/88e6343fd257c8011ebec85833210bc8_1624x126.png) 这是由于我们使用了`{id: 'yunzhi'}`这个对象来提供了`ActivedRoute`,该对象上并不存在`snapshot`属性,所以才引发了上述异常。解决的方法很简单: ```typescript +++ b/first-app/src/app/student/edit/edit.component.spec.ts @@ -18,7 +18,7 @@ describe('EditComponent', () => { ClazzSelectModule, ReactiveFormsModule], declarations: [EditComponent], providers: [ - {provide: ActivatedRoute, useValue: {id: 'yunzhi'}} + {provide: ActivatedRoute, useValue: {snapshot: {params: {id: '123'}}}} ] }) .compileComponents(); ``` 注意:虽然我们可以使用任何值来提供`ActivatedRoute`,但却应该按钮实际情况提供与原`ActivatedRoute`贴近的值。比如当前的实际情况是我们在组件中使用了`ActivatedRoute`的`snapshot.params.id`属性,该属性的类型实际上应该为`string`,所以我们在此自定义提供`ActivatedRoute`的对象时,也应该将其类型设置为`string`。 如果我们不这么做,便会导致单元测试环境与最终的集成测试环境变量类型不同,这也就失去了单元测试作为保障功能的目的。 此时在组件中调用`this.activatedRoute.snapshot.params.id;`将获取到在单元测试中设定的值`123`: ![image-20210610092824927](https://img.kancloud.cn/2a/83/2a83fb1943f17f032194afffb44ffb27_790x98.png) 单元测试正常,组件成功显示: ![image-20210610092924149](https://img.kancloud.cn/c5/13/c5130bd14c7df5db6483b1a98b6708ae_2314x668.png) 当前组件有两个比较明显的异常(不正常)。第1个是手机号的格式不正确,第二是没有自动选中要编辑的班级。 ### 手机号错误 手机号错误是由于我们在MockApi的返回值不小心将手机号的位数搞错了,本来应该是11位,我们弄成12位了。修正如下: ```typescript +++ b/first-app/src/app/mock-api/student.mock.api.ts @@ -87,7 +87,7 @@ export class StudentMockApi implements MockApiInterface { id, name: randomString('姓名'), number: randomString('学号'), - phone: (139000000000 + randomNumber(99999999)).toString(), + phone: (139 * 10000 * 10000 + randomNumber(99999999)).toString(), email: randomString('前缀') + '@yunzhi.club', clazz: { id: randomNumber(), ``` 使用与两个1000相乘的方法来替换原来在后面加0的情况。 ### 自动选中班级 班级未自动选中,也是由于MockApi在返回学生所在的班级ID时,使用了随机字符串的方式。我们将其限定在0-10之间(班级选择组件的ID范围是0-10),这样便可以自动选中了。 ```typescript +++ b/first-app/src/app/mock-api/student.mock.api.ts @@ -87,7 +87,7 @@ export class StudentMockApi implements MockApiInterface { id, name: randomString('姓名'), number: randomString('学号'), - phone: (139000000000 + randomNumber(99999999)).toString(), + phone: (139 * 10000 * 10000 + randomNumber(99999999)).toString(), email: randomString('前缀') + '@yunzhi.club', clazz: { id: randomNumber(), ``` 最终效果如下: ![image-20210610093559752](https://img.kancloud.cn/1f/bb/1fbb1d68a5167eb7d0897ceb52c03a59_2264x612.png) ### 本节作业 除使用`useValue`来直接声明提供的对象外,我们在前面还使用了`useClass`的方式。请尝试使用该方法来提供`ActivatedRoute` 并完成单元测试。 ## 资源链接 | 链接 | 名称 | | ------------------------------------------------------------ | -------- | | [https://github.com/mengyunzhi/angular11-guild/archive/step7.7.2.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.7.2.zip) | 本节源码 | | [https://angular.cn/guide/dependency-injection-providers](https://angular.cn/guide/dependency-injection-providers) | DI提供者 |