与路由参数快照只能获取一次路由参数不同,可订阅的路由参数可以在路由参数发生变更时时时的通知我们,路由参数每变化一次我们就会得到一个新的通知。 `ActivatedRoute`提供的`params`属性具有上述特性,我们可以通过订阅它来完成:当路由参数发生变化时得到一个最新的通知。 ## `ActivatedRoute.params` 在编辑组件中订阅`ActivatedRoute.params`看看会发生什么: ```typescript +++ b/first-app/src/app/student/edit/edit.component.ts @@ -30,6 +30,9 @@ export class EditComponent implements OnInit { } ngOnInit(): void { + this.activatedRoute.params.subscribe(params => { + console.log('路由参数发生了变化', params); + }); this.id = +this.activatedRoute.snapshot.params.id; Assert.isNumber(this.id, '接收到的id类型不正确'); this.loadData(this.id); ``` 此时: ➊ 浏览器地址为`http://localhost:4200/student`时,学生列表组件不存在,则初始化学生列表组件。 ➋ 第1次点击编辑按钮时,浏览器地址由`http://localhost:4200/student`跳转至`http://localhost:4200/student/edit/1`,学生编辑组件不存在,则初始化学生编辑组件。该组件中的`ngOnInit`方法被自动调用1次。在该方法中对`ActivatedRoute.params`进行订阅,通过订阅操作接收到了路由参数信息: ![image-20210611171405883](https://img.kancloud.cn/87/69/8769797a657b5d0873f82ddc253819ba_1266x140.png) ➌ 第2次点击编辑按钮时,浏览器地址由`http://localhost:4200/student/edit/1`变更为`http://localhost:4200/student/edit/2`,学生编辑组件已存在,组件什么也不做。但由于在➋中对路由参数进行了订阅,所以仍然可以在控制台中打印最新的路由参数信息: ![image-20210611171619260](https://img.kancloud.cn/61/1e/611ea5bd2837fafce87c661a804dcb56_1070x156.png) 如此以来,即使组件没有重新初始化,我们仍然可以在路由参数发生变化时重新请求后台,进而达到显示待更新学生的目的: ```typescript +++ b/first-app/src/app/student/edit/edit.component.ts ngOnInit(): void { this.activatedRoute.params.subscribe(params => { console.log('路由参数发生了变化', params); this.id = +params.id; Assert.isNumber(this.id, '接收到的id类型不正确'); this.loadData(this.id); }); } ``` 此时,无论我们怎么点击编辑按钮,学生编辑组件都会根据最新的路由参数重新发起对后台的数据请求,然后将请求的数据显示在待学生更新组件上。 ## 万物归一 解决完上述BUG后,我们接着利用该思想继续解决下一个BUG: ➊ 访问`http://localhost:4200/student`学生列表。 ➋ 点击编辑按钮,编辑某个学生。 ➌ 变更学生的基本信息后,点击保存。 ➍ 但是:原学生信息并未更新。 ➎ 然而:刷新当前界面后发生学生信息已更新。 其实这个BUG产生的原因与前面我们刚刚遇到的问题完全一致。都是由于在路由跳转时Angular检测到了可以重复利用的组件,进而没有重新实例化新组件的造成的: ➊ 访问`http://localhost:4200/student`学生列表。 - 学生列表组件初始化,执行1次`ngOnInit()`,在此方法中请求后台数据,使用后台数据完成渲染,显示学生列表。 ➋ 点击编辑按钮,编辑某个学生。 - 学生列表组件已存在,复用原组件。 ➌ 变更学生的基本信息后,点击保存。 - 学生列表组件已存在,复用原组件。 ➍ 但是:原学生信息并未更新。 - 学生列表组件并没有感知到路由发生变化的能力,所以什么也不会做。 ➎ 然而:刷新当前界面后发生学生信息已更新。 - 重新实例化学生列表组件,执行1次`ngOnInit()`,在此方法中请求后台数据,使用后台数据完成渲染,显示学生列表。 分析完组件的初始化过程后,解决的思路也有了:使学生列表组件感知到路由参数的变化。那么是否可以像`edit`组件一样直接来订阅`ActivatedRoute.params`呢?很遗憾,答案是否定的。 这是由于以下路由配置决定的: ```typescript const routes = [ { path: '', component: StudentComponent, 👉 children: [ { path: 'add', component: AddComponent }, { path: 'edit/:id', component: EditComponent } ] } ] as Route[]; ``` 这个`children`意味着:无论是由`http://localhost:4200/student`跳转到`http://localhost:4200/student/edit/1`;还是由`http://localhost:4200/student/edit/1`跳转到`http://localhost:4200/student`。对于`StudentComponent`中的`ActivatedRoute.params`而言,均未发生变化。 总结:订阅`ActivatedRoute.params`无法感觉到路由在子路由及当前路由间的跳转。 这时候就需要请出`Router`这个大佬了。与`ActivatedRoute`不同,`Router`大佬可以感知到任何的路由事件,而不会局限在某一方面。 --------------------------------------**以下内容了解即可**-------------------------------------- 这时候我们可以这样做: ```typescript +++ b/first-app/src/app/student/student.component.ts @@ -1,25 +1,44 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; import {Page} from '../entity/page'; import {Student} from '../entity/student'; import {StudentService} from '../service/student.service'; import {environment} from '../../environments/environment'; import {Confirm, Report} from 'notiflix'; +import {NavigationEnd, Router} from '@angular/router'; +import {filter} from 'rxjs/operators'; +import {Subscription} from 'rxjs'; @Component({ selector: 'app-student', templateUrl: './student.component.html', styleUrls: ['./student.component.css'] }) -export class StudentComponent implements OnInit { +export class StudentComponent implements OnInit, OnDestroy { pageData = {} as Page<Student>; page = 0; size = environment.size; - constructor(private studentService: StudentService) { + /** + * 当前组件所有的订阅信息 + */ + subscriptions = new Array<Subscription>(); + + + constructor(private studentService: StudentService, + private router: Router) { } ngOnInit(): void { this.loadData(this.page); + this.subscriptions.push(this.router.events + .pipe(filter(e => e instanceof NavigationEnd)) + .subscribe(event => { + event = event as NavigationEnd; + if (event.url === '/student') { + console.log('感知到了路由事件,重新请求数据'); + this.loadData(this.page); + } + })); } /** @@ -74,4 +93,11 @@ export class StudentComponent implements OnInit { }); } } + + /** + * 生产项目中,应该在组件销毁时,取消所有的订阅 + */ + ngOnDestroy(): void { + this.subscriptions.forEach(s => s.unsubscribe()); + } } ``` 该部分内容有一定难度,超出了本教程(入门)的教学范围,具体代码功能不做解释。 此时,将编辑完某个学生信息后回跳到学生列表界面时数据便会重新请求1次。 **注意**:由于上述方法直接使用了`url`进行判断,这将导致其将随着在生产环境中重新配置路由而失效。在生产环境中,应该通过`service`来发送子组件的更新、新增事件,然后在学生列表组件中进行订阅。 ^^^^^^^^^^^^^^^^^^^^^^^^^^以上内容了解即可**^^^^^^^^^^^^^^^^^^^^^^^^^^ ## 单元测试 功能实现后,再介绍下类似于这种**可订阅的数据**应该如何进行单元测试,即在单元测试中如何给出一个**假的**可订阅数据。 先启动学生编辑组件的单元测试: ```typescript +++ b/first-app/src/app/student/edit/edit.component.spec.ts @@ -31,7 +31,7 @@ describe('EditComponent', () => { fixture.detectChanges(); }); - it('should create', () => { + fit('should create', () => { expect(component).toBeTruthy(); }); ``` 错误如下: ![image-20210611182450891](https://img.kancloud.cn/4b/d8/4bd87684bdb9cc54c67a3626c8be0379_1582x102.png) 报错的原因是我们在单元测试中没能提供一个带有可订阅属性`params`的`ActivatedRoute`,那么提供一个好了。 ```typescript +++ b/first-app/src/app/student/edit/edit.component.spec.ts @@ -7,6 +7,7 @@ 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'; +import {of} from 'rxjs'; describe('EditComponent', () => { @@ -19,7 +20,12 @@ describe('EditComponent', () => { ClazzSelectModule, ReactiveFormsModule], declarations: [EditComponent], providers: [ - {provide: ActivatedRoute, useValue: {snapshot: {params: {id: '123'}}}} + { + provide: ActivatedRoute, + useValue: { + params: of({id: '123'})① + } + } ] }) ``` ① `of`函数可快速的提供一个可被订阅的数据源。 ① `of`函数接收任意类型,并将该值在被订阅时发送出去,可以测试以下代码: ```typescript const a = of('123'); a.subscribe(s => console.log(s)); ``` 上述代码我们使用`of`将`{id: '123'}`做为数据源发送给了订阅者。此时,当在编辑组件中订阅`ActivatedRoute.params`时,则将得到`{id: '123'}`。编辑组件接收到`123`后,则会使用该`id`来请求数据,近而完成待更新学生数据的展示: ![image-20210611183325592](https://img.kancloud.cn/d6/2c/d62c12bab6af48264e55f51ab52a89f9_1454x630.png) 此时当点击保存按钮时,则会发生如下异常: ![image-20210611183417365](https://img.kancloud.cn/f4/b8/f4b8ea89ad370dfc79b57ff3ef5a4bb2_1210x132.png) 这是由于此时编辑组件在更新成功后,执行了如下代码: ```typescript this.router.navigate(['../../'], {relativeTo: this.activatedRoute}); ``` ### Router 由于我们并没有声明提供`Router`,所以此时组件中的`this.router`仍然为Angular提供的。此`router` 在进行跳转时需要依赖于直接的地址,而当前单元测试并没有这样的环境,所以报出了上述异常。 解决该问题的方法与解决可订阅的路由参数的方法异曲同工:手动提供一个`Router`。 ```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 {ActivatedRoute, RouterModule} from '@angular/router'; +import {ActivatedRoute, Router, RouterModule} from '@angular/router'; import {ClazzSelectModule} from '../../clazz/clazz-select/clazz-select.module'; import {ReactiveFormsModule} from '@angular/forms'; import {of} from 'rxjs'; @@ -25,6 +25,14 @@ describe('EditComponent', () => { useValue: { params: of({id: '123'}) } + }, + { + provide: Router, + useValue: { + navigate: (...args: any) => { + console.log('调用了navigate方法', args); + } + } } ] }) ``` 此时在单元测试中再次点击保存按钮,则会在控制台中打印一条信息: ![image-20210611183951122](https://img.kancloud.cn/6e/20/6e2053f9c64d780a1c2926650b429fc6_898x154.png) 学生编辑组件的单元测试完成后,将`fit`恢复为`it`,查看项目整体单元测试情况。 ## ng t 错误如下: ![image-20210611184327526](https://img.kancloud.cn/f6/6a/f66a27dad66bc23f8a113d480637e875_1524x148.png) 该错误是由于单元测试未能提供`Router`造成的: ```typescript +++ b/first-app/src/app/student/add/add.component.spec.ts @@ -10,6 +10,7 @@ import {map} from 'rxjs/operators'; import {LoadingModule} from '../../directive/loading/loading.module'; import {randomString} from '@yunzhi/ng-mock-api/testing'; import {randomNumber} from '@yunzhi/ng-mock-api'; +import {RouterTestingModule} from '@angular/router/testing'; describe('student -> AddComponent', () => { let component: AddComponent; @@ -19,6 +20,7 @@ describe('student -> AddComponent', () => { await TestBed.configureTestingModule({ declarations: [AddComponent], imports: [ + RouterTestingModule, ReactiveFormsModule, ClazzSelectModule, MockApiTestingModule, ``` 修正后错误消失,好了,就到这里。 ## 资源链接 | 链接 | 名称 | | ------------------------------------------------------------ | ------------- | | [https://github.com/mengyunzhi/angular11-guild/archive/step7.7.5.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.7.5.zip) | 本节源码 | | [https://angular.cn/guide/dependency-injection-providers](https://angular.cn/guide/dependency-injection-providers) | DI提供者 | | [https://angular.cn/api/router/NavigationEnd](https://angular.cn/api/router/NavigationEnd) | NavigationEnd | | [https://cn.rx.js.org/class/es6/Observable.js~Observable.html#static-method-of](https://cn.rx.js.org/class/es6/Observable.js~Observable.html#static-method-of) | rxjs - of() |