本節我們完成班級管理模塊的路由定制、各個模塊間的跳轉關系以及前面遺留的`todo`:
## 路由定制
首先進行教師模塊路由的定制:
```typescript
+++ b/first-app/src/app/clazz/clazz.module.ts
@@ -12,6 +12,14 @@ const routes: Routes = [
{
path: '',
component: ClazzComponent
+ },
+ {
+ path: 'add',
+ component: AddComponent
+ },
+ {
+ path: 'edit/:id',
+ component: EditComponent
}
];
```
新增組件不需要傳入參數,編輯組件需要傳入班級ID。
### 教師列表
教師列表組件是當前模塊的著陸組件,在該組件中需要定制添加及編輯組件:
```html
+++ b/first-app/src/app/clazz/clazz.component.html
@@ -1,6 +1,6 @@
<div class="row">
<div class="col-12 text-right">
- <a class="btn btn-primary mr-2"><i class="fas fa-plus"></i>新增</a>
+ <a class="btn btn-primary mr-2" routerLink="./add"><i class="fas fa-plus"></i>新增</a>
</div>
</div>
@@ -19,7 +19,7 @@
<td>{{clazz.name}}</td>
<td>{{clazz.teacher.name}}</td>
<td>
- <a class="btn btn-outline-primary btn-sm">
+ <a class="btn btn-outline-primary btn-sm" routerLink="edit/{{clazz.id}}">
<i class="fas fa-pen"></i>編輯
</a>
```
**新增**路由中使用了`./add`,以`./`打頭表示相對路徑,如果當前在URL為`http://localhost:4200/clazz`時,訪問`http://localhost:4200/clazz/add`;**編輯**路由中使用了`edit/`,該方法同樣為相對路徑;如果在此使用絕對路徑,則應該使用`/`打頭(不推薦),比如此處的**新增**路由使用`/clazz/add`。
此時,在教師列表組件中點擊新增按鈕,將跳轉到新增組件。
## 新增
在新增組件中,填寫完名稱、選擇好班主任后,點擊保存后需要回跳至教師列表界面。
```typescript
+++ b/first-app/src/app/clazz/add/add.component.ts
@@ -2,6 +2,7 @@ import {Component, OnInit} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Teacher} from '../../entity/teacher';
import {Clazz} from '../../entity/clazz';
+import {Router} from '@angular/router';
@Component({
selector: 'app-add',
@@ -17,7 +18,8 @@ export class AddComponent implements OnInit {
teachers = new Array<Teacher>();
- constructor(private httpClient: HttpClient) {
+ constructor(private httpClient: HttpClient,
+ private router: Router) {
}
ngOnInit(): void {
@@ -32,7 +34,7 @@ export class AddComponent implements OnInit {
teacher: new Teacher({id: this.clazz.teacherId})
});
this.httpClient.post(this.url, newClazz)
- .subscribe(clazz => console.log('保存成功', clazz),
+ .subscribe(clazz => this.router.navigateByUrl('/clazz'),
error => console.log('保存失敗', error));
}
```
在組件中進行頁面的跳轉除使用`Router.navigate()`外,還可以使用`Router.navigateByUrl()`,需要注意的是該方法的參數必須接收一個絕對地址,比如我們在此處不能使用`this.router.navigateByUrl('../')`。
此時當擊保存按鈕后,將跳轉至教師列表界面。保存后的班級顯示在了班級列表中,也驗證了班級列表功能的正常。

## 編輯
在班級列表中點擊編輯按鈕,名稱與班主任信息并未自動填充:

這是由于在編輯組件中,尚存在一個todo:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.ts
@@ -31,8 +31,7 @@ export class EditComponent implements OnInit {
ngOnInit(): void {
const id = this.activatedRoute.snapshot.params.id;
- // todo: 調用loadById方法,獲取預編輯的班級
- console.log('ngOnInit方法獲取路由ID及調用loadById尚未測試,請在集成測試中補充代碼');
+ this.loadById(+id);
}
/**
```
修正todo后,繼續完成路由跳轉功能:
```typescript
+++ b/first-app/src/app/clazz/edit/edit.component.ts
@@ -1,5 +1,5 @@
import {Component, OnInit} from '@angular/core';
-import {ActivatedRoute} from '@angular/router';
+import {ActivatedRoute, Router} from '@angular/router';
import {HttpClient} from '@angular/common/http';
import {Clazz} from '../../entity/clazz';
import {FormControl, FormGroup, Validators} from '@angular/forms';
@@ -26,6 +26,7 @@ export class EditComponent implements OnInit {
});
constructor(private activatedRoute: ActivatedRoute,
+ private router: Router,
private httpClient: HttpClient) {
}
@@ -65,7 +66,7 @@ export class EditComponent implements OnInit {
});
this.httpClient.put<Clazz>(`/clazz/${clazzId}`, clazz)
.subscribe(
- () => console.log('更新成功'),
+ () => this.router.navigate(['../../'], {relativeTo: this.activatedRoute}),
error => console.log(error));
}
}
```
`Router.navigate()`的第二個參數,支持傳入一個對象,當該對象的`relativeTo`屬性為`activatedRoute`時,則將其跳轉地址解析為**相對**地址。當前編輯對應的`url`為`http://localhost:4200/clazz/edit/1`,預由該`url`跳轉至`http://localhost:4200/clazz`,則需要向上翻兩層,所以相對地址應該是`../../`。
至此,模塊間便能夠順利的正進行跳轉了。最后進行**刪除**功能測試,發現一切正常。
集成測試結束。
## 單元測試
每每修改一些功能的同時,都需要關注單元測試是否仍然正常。并需要要根據單元測試的報錯情況對應修正相關代碼。啟用`ng t`后,按提示分別進行修正:

提示說找不到`Router`的提供者。通過IDE的查找功能,查找`clazz add with mockapi`關鍵字能夠快速的找到對應的測試文件:
```typescript
+++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts
@@ -6,6 +6,7 @@ import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {ClazzMockApi} from '../../mock-api/clazz.mock.api';
import {TeacherMockApi} from '../../mock-api/teacher.mock.api';
import {KlassSelectComponent} from '../klass-select/klass-select.component';
+import {RouterTestingModule} from '@angular/router/testing';
describe('clazz add with mockapi', () => {
let component: AddComponent;
@@ -14,7 +15,7 @@ describe('clazz add with mockapi', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddComponent, KlassSelectComponent],
- imports: [HttpClientModule, FormsModule, ReactiveFormsModule],
+ imports: [HttpClientModule, FormsModule, ReactiveFormsModule, RouterTestingModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
```
繼續排查錯誤:

同樣的提示,同樣的問題,同樣的修正方法,但在IDE中使用的快速查找時,卻找到了兩個以`AddComponent`關鍵字命名的文件:

我們此時打開第二個也就是`clazz`文件中的測試文件,然后為這個測試改個名稱:
```typescript
+++ b/first-app/src/app/clazz/add/add.component.spec.ts
@@ -8,7 +8,7 @@ import {ApiInterceptor} from '../../api.interceptor';
import {CommonModule} from '@angular/common';
import {KlassSelectComponent} from '../klass-select/klass-select.component';
-describe('AddComponent', () => {
+describe('clazz -> AddComponent', () => {
let component: AddComponent;
let fixture: ComponentFixture<AddComponent>;
```
如此以來當再發生測試錯誤時,便能夠快速的定位到報錯的文件了:

添加`RouterTestingModule`以提供`Router`:
```typescript
declarations: [AddComponent, KlassSelectComponent],
- imports: [FormsModule, HttpClientModule, CommonModule, ReactiveFormsModule],
+ imports: [FormsModule, HttpClientModule, CommonModule, ReactiveFormsModule,
+ RouterTestingModule],
// 加入自定義的XAuthTokenInterceptor,讓其自動為我們處理認證的header
```
### 控制臺錯誤
打開控制臺,發現報錯信息如下:

點擊左側的小箭頭后展開錯誤信息:

最示為clazz組件中的錯誤。提示`routerLink`并不是元素`a`的已知屬性,該錯誤是由于我們在clazz組件的V層中的a元素上使用了`routerLink`造成的:
```html
<a class="btn btn-primary mr-2" routerLink="./add"><i class="fas fa-plus"></i>新增</a>
```
消除該錯語的方法是為當前測試模塊提供`RouterTestingModule`,因為`RouterTestingModule`為`a`元素擴展了`routerLink`屬性:
```typescript
+++ b/first-app/src/app/clazz/clazz.component.spec.ts
@@ -6,6 +6,7 @@ import {ClazzMockApi} from '../mock-api/clazz.mock.api';
import {getTestScheduler} from 'jasmine-marbles';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {PageComponent} from './page/page.component';
+import {RouterTestingModule} from '@angular/router/testing';
describe('ClazzComponent', () => {
let component: ClazzComponent;
@@ -15,7 +16,8 @@ describe('ClazzComponent', () => {
await TestBed.configureTestingModule({
declarations: [ClazzComponent, PageComponent],
imports: [
- HttpClientModule
+ HttpClientModule,
+ RouterTestingModule
],
providers: [
{
```
繼續查看控制臺的錯誤提示:

或者如下錯誤提示:

> 在使用`ng t`來測試整個項目時,由于測試用例執行順序的隨機性,上述兩個提示會隨機出現在控制臺中。
該提示說當執行`add.compoent.ts`的37行發生了一個錯誤:匹配不到`clazz`路由。`add.compoent.ts`的37行代碼如下:
```typescript
33 onSubmit(): void {
36 this.httpClient.post(this.url, newClazz)
37 .subscribe(clazz => this.router.navigateByUrl('/clazz'), ??
38 error => console.log('保存失敗', error));
```
可見是當組件執行`onSubmit()`方法時發生的錯誤,由提示信息還可得該方法由`add.compoent.spec.ts`文件的第58行觸發:
```typescript
41 it('onSubmit', () => {
55 name: 'test',
56 teacherId: 1
57 };
58 component.onSubmit();
```
由上述代碼可見:在單元測試中執行了`component.onSubmit()`方法,該方法使用了`this.router.navigateByUrl('/clazz'),`嘗試進行路由的跳轉,由于當前動態測試模塊并未設置路由,所以發生了跳轉錯誤。
如果你看到的是第二個錯誤,則是由于`add.component.mock-api.spec.ts`中同樣也觸發了`add.compoent.ts`中的上述代碼。
### 修正錯誤
`RouterTestingModule`提供了`withRoutes([])`來為動態測試模塊設置路由,`withRoutes()`的使用可參考`RouterModule.forRoot()`方法:
```typescript
+++ b/first-app/src/app/clazz/add/add.component.spec.ts
@@ -8,16 +8,29 @@ import {ApiInterceptor} from '../../api.interceptor';
import {CommonModule} from '@angular/common';
import {KlassSelectComponent} from '../klass-select/klass-select.component';
import {RouterTestingModule} from '@angular/router/testing';
+import {Component} from '@angular/core';
+import {Route} from '@angular/router';
+
+@Component({ ①
+ template: 'test'
+})
+class TestComponent ① {
+}
describe('clazz -> AddComponent', () => {
let component: AddComponent;
let fixture: ComponentFixture<AddComponent>;
-
+ const routes: Route[] = [ ②
+ {
+ path: 'clazz', ③
+ component: TestComponent
+ }
+ ];
beforeEach(async () => {
await TestBed.configureTestingModule({
- declarations: [AddComponent, KlassSelectComponent],
+ declarations: [AddComponent, KlassSelectComponent, TestComponent④],
imports: [FormsModule, HttpClientModule, CommonModule, ReactiveFormsModule,
- RouterTestingModule],
+ RouterTestingModule.withRoutes(routes)⑤],
// 加入自定義的XAuthTokenInterceptor,讓其自動為我們處理認證的header
providers: [
{provide: HTTP_INTERCEPTORS, multi: true, useClass: XAuthTokenInterceptor},
```
如上代碼中做了以下三件事:
- ① 定義了一只在本文件中供測試使用的Test組件
- ② 定義了一個路由數組,該數組中有一個路由,該路由配置的`path`為`clazz `③
- ④ 將Test組件聲明至動態測試模塊中的declarations中
- ⑤ 使用`RouterTestingModule.withRoutes()`來配置這個路由
如此,當前測試模塊便擁有了一個`clazz`路由。當組件嘗試跳轉至`clazz`時便可以可以匹配,從而解決了控制臺中顯示的匹配錯誤。
在`add.component.mock-api.spec.ts`同樣加入同樣的代碼,以解決`add.component.mock-api.spec.ts`調用`component.onSubmit()`時發生的路徑未匹配成功錯誤:
```typescript
+++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts
@@ -7,6 +7,13 @@ import {ClazzMockApi} from '../../mock-api/clazz.mock.api';
import {TeacherMockApi} from '../../mock-api/teacher.mock.api';
import {KlassSelectComponent} from '../klass-select/klass-select.component';
import {RouterTestingModule} from '@angular/router/testing';
+import {Component} from '@angular/core';
+
+@Component({
+ template: 'test'
+})
+class TestComponent {
+}
describe('clazz add with mockapi', () => {
let component: AddComponent;
@@ -15,7 +22,13 @@ describe('clazz add with mockapi', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddComponent, KlassSelectComponent],
- imports: [HttpClientModule, FormsModule, ReactiveFormsModule, RouterTestingModule],
+ imports: [HttpClientModule, FormsModule, ReactiveFormsModule,
+ RouterTestingModule.withRoutes([
+ {
+ path: 'clazz',
+ component: TestComponent
+ }
+ ])],
providers: [
{
provide: HTTP_INTERCEPTORS,
```
至此,單元測試中的錯誤消失。
> 如果在學習單元測試時感覺有些力不從心,也可以跳過本節修正單元測試錯誤的環節。等整個教程完成后,對單元測試的認識進一步提升了,再回過頭來繼續學習。
## 本節作業
請測試分頁功能是否正常。
| 名稱 | 鏈接 |
| ------------------- | ------------------------------------------------------------ |
| 指定相對路由 | [https://angular.cn/guide/router#specifying-a-relative-route](https://angular.cn/guide/router#specifying-a-relative-route) |
| RouterTestingModule | [https://angular.cn/api/router/testing/RouterTestingModule](https://angular.cn/api/router/testing/RouterTestingModule) |
| 本節源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.6.3.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.6.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 發布部署
- 第九章 總結