与路由参数快照只能获取一次路由参数不同,可订阅的路由参数可以在路由参数发生变更时时时的通知我们,路由参数每变化一次我们就会得到一个新的通知。
`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() |
- 序言
- 第一章 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 发布部署
- 第九章 总结