按由后到前的順序初始化如下:
## service
service/course.service.ts
```typescript
/**
* 分頁
* @param params name課程名稱 klassId 班級 teacherId 教師
*/
page(params?: {name?: string, klassId?: number, teacherId?: number}): Observable<Page?> {
return null;
}
```
* ? 分頁信息屬于常用數據結構,自定Page類以便復用。
## page
Page接口
```
panjiedeMac-Pro:norm panjie$ ng g class Page
CREATE src/app/norm/page.spec.ts (146 bytes)
CREATE src/app/norm/page.ts (22 bytes)
panjiedeMac-Pro:norm panjie$
```
參考學生管理中后臺返回的分頁數據初始化Page類如下:
norm/page.ts
```typescript
/**
* 分頁數據
*/
export class Page<T?> {
/* 內容 */
content: Array<T>?;
/* 總頁數 */
totalPages: number;
constructor(params?: { content?: T, totalPages?: number }) {
if (params) {
if (params.content) {
this.content = params.content;
}
if (params.totalPages) {
this.totalPages = params.totalPages;
}
}
}
}
```
* ? 聲明Page具有容器的性質,可以裝入不同的對象
* ? 內部容器,該類型與Page類型的泛型相同。
新建Page類后在service/course.service.ts中引用Page并設置泛型。
service/course.service.ts
```typescript
import {Page} from '../norm/page'; ?
page(params?: {name?: string, klassId?: number, teacherId?: number}): Observable<Page<Course?>> {
return null;
}
```
* ? 引入Page類型
* ? 聲明Page里裝入的對象類型為Course
## ServiceStub
對應在service的替身中增加page方法
service/course-stub.service.ts
```typescript
page(params?: {name?: string, klassId?: number, teacherId?: number}): Observable<Page<Course>> {
return null;
}
```
## 組件
在course中新建index組件:
```
panjiedeMac-Pro:course panjie$ ng g c index
CREATE src/app/course/index/index.component.sass (0 bytes)
CREATE src/app/course/index/index.component.html (20 bytes)
CREATE src/app/course/index/index.component.spec.ts (621 bytes)
CREATE src/app/course/index/index.component.ts (266 bytes)
UPDATE src/app/course/course.module.ts (761 bytes)
panjiedeMac-Pro:course panjie$
```
### V層初始化
course/index/index.component.html
```html
<form (ngSubmit)="onQuery()">
<label>課程名稱:<input name="name" [formControl]="params.name" type="text"/></label>
<label>教師:
<app-teacher-select (selected)="onSelectTeacher($event)"></app-teacher-select>
</label>
<label>班級:
<app-klass-select (selected)="onSelectKlass($event)"></app-klass-select>
</label>
<button type="submit">查詢</button>
</form>
<div class="row">
<div class="col text-right">
<a class="btn btn-primary" routerLink="./add"><span class="oi oi-plus"></span>新增課程</a>
</div>
</div>
<table class="table">
<tr>
<th>序號</th>
<th>名稱</th>
<th>任課教師</th>
<th>班級</th>
<th>操作</th>
</tr>
<tr *ngFor="let course of coursePage.content; index as index">
<td>{{index + 1}}</td>
<td>{{course.name}}</td>
<td>{{course.teacher.name}}</td>
<td>{{course.klass.name}}</td>
<td>
<a routerLink="./edit/{{course.id}}" class="btn btn-sm btn-info"><span class="oi oi-pencil"></span>編輯</a>
<button (click)="onDelete(course)" class="btn btn-sm btn-danger"><span class="oi oi-trash"></span>刪除</button>
</td>
</tr>
</table>
```
以下是分頁信息,暫時省略
### C層初始化
course/index/index.component.ts
```typescript
import {Component, OnInit} from '@angular/core';
import {Course} from '../../norm/entity/course';
import {Page} from '../../norm/page';
import {FormControl} from '@angular/forms';
import {Klass} from '../../norm/entity/Klass';
import {Teacher} from '../../norm/entity/Teacher';
@Component({
selector: 'app-index',
templateUrl: './index.component.html',
styleUrls: ['./index.component.sass']
})
export class IndexComponent implements OnInit {
params = {
name: new FormControl('')
};
coursePage: Page<Course>;
constructor() {
}
ngOnInit() {
}
onQuery() {
}
onSelectTeacher($event: Teacher) {
}
onSelectKlass($event: Klass) {
}
onDelete(course: Course) {
}
}
```
### 單元測試初始化
course/index/index.component.spec.ts
```typescript
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ IndexComponent ],
imports: [
ReactiveFormsModule, ?
FormsModule,?
TestModule ?
]
})
.compileComponents();
}));
```
* ? formControl指令
* ? (ngSubmit)方法
* ? app-teacher-select

單元測試提示不能解析app-klass-select。app-klass-select位于student模塊中的KlassSelect組件中。但由于歷史原因尚未生成KlassSelect組件對應的測試替身。
## 遷移公共組件
我們之所以要將項目分成多個模塊,有一個重要的原因是為了:惰性加載。將項目分離成多個模塊可以將一個大型的項目分成多次被用戶加載。這樣可以使得一個較大的項目能夠有著比較良好的使用體驗。而這一切的前提是:各個模塊互相獨立。如若在當前Course模塊中使用位于Student模塊中的KlassSelect組件,則Course模塊產生了對Student模塊的依賴,也就無法獨立于Student模塊存在了。在實際的生產項目中往往把一些公用的組件放到一個單獨的模塊中。為此將
Student模塊中的KlassSelect組件遷移到Core模塊中。
剪切

粘貼

然后將此組件添加到Core模塊的declarations中以及exports中,同時刪除原Student模塊declarations中對此組件的聲明。
core/core.modult.ts
```typescript
@NgModule({
declarations: [SelectComponent,
MultipleSelectComponent,
KlassSelectComponent?],
...
exports: [
SelectComponent,
MultipleSelectComponent,
KlassSelectComponent?
]
```
student/student.module.ts
```typescript
import { KlassSelectComponent } from '../core/klass-select/klass-select.component'; ?
@NgModule({
declarations: [AddComponent, KlassSelectComponent?, IndexComponent, EditComponent],
export class StudentModule {
}
```
### 新建組件測試替身
遷移完成后為其建立測試替身以便其被其它模塊使用時能夠優雅的進行單元測試。
```
panjiedeMac-Pro:core-testing panjie$ ng g c klassSelect -s -t --skip-tests
CREATE src/app/core/core-testing/klass-select/klass-select.component.ts (273 bytes)
UPDATE src/app/core/core-testing/core-testing.module.ts (562 bytes)
panjiedeMac-Pro:core-testing panjie$
```
初始化輸入、輸出并加入到測試控制器中:
```typescript
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {Klass} from '../../../norm/entity/Klass';
import {CoreTestingController} from '../core-testing-controller';
@Component({
selector: 'app-klass-select',
template: `
<p>
klass-select works!
</p>
`,
styles: []
})
export class KlassSelectComponent implements OnInit {
@Output() selected = new EventEmitter<Klass>(); ?
@Input() klass: Klass; ?
constructor(private controller: CoreTestingController) {
this.controller.addUnit(this); ?
}
ngOnInit() {
}
}
```
* ? 對應原組件的輸出
* ? 對應原組件的輸入
* ? 添加到測試控制器中以便在單元測試中由測試控制器中獲取該組件
在測試模塊中將此組件替身輸出:
core/core-testing/core-testing.module.ts
```typescript
exports: [
MultipleSelectComponent,
KlassSelectComponent ?
],
...
export class CoreTestingModule { }
```
## 單元測試
準備好測試組件替身后將CoreTestingModule開入到課程列表組件測試文件中:
course/index/index.component.spec.ts
```typescript
imports: [
ReactiveFormsModule,
TestModule,
CoreTestingModule ?
]
```

```typescript
TestModule,
CoreTestingModule,
RouterTestingModule?
]
```
## V層中的?
引用了多個模塊后再次執行單元測試,錯誤如下:

此錯誤在說:在undefined上讀取content時發生了錯誤。此錯誤產生的原因是由于在組件中使用了`xxxx.content`嘗試獲取數據,但在執行該語句時`xxxx`的值為undefined。
在組件的C層中并沒有讀取content屬性,所以錯誤并不是在C層發生了。在V層中以content進行搜索發現有以下語句:`<tr *ngFor="let course of coursePage.content; index as index">`。該錯誤便是由此發生了。
這是由于:C層在初始化時只是聲明了coursePage的類型,并未對其進行初始化,所以coursePage當前的值為undefined。在V層中嘗試執行`coursePage.content`時便發生了`TypeError: Cannot read property 'content' of undefined`的錯誤。解決該問題的方法最少有兩種:由于V層的渲染操作發生在C層的ngOnInit方法以后,所以第一種方法可以在ngOnInit中對coursePage進行初始化,此后在進行V層的渲染時coursePage的值并不為undefined,當然也就不會發生此錯誤了;除此以外angular也充分地考慮到了此問題,可以在V層中使用`?`來標注某個變量以表示:當此值為undefined時暫停渲染,當此值不為undefined時繼續完成渲染。
course/index/index.component.html
```html
<tr *ngFor="let course of coursePage??.content; index as index">
```
* ? 在可能為undefined的變量后添加?以防止undefined錯誤。
在渲染V層時,除要規避在undefined上讀取某個屬性發生錯誤以外,還要規避類似的在null上讀取某個屬性發生錯誤。而以下代碼則可能引發在null讀取某個屬性的錯誤:
```html
<tr *ngFor="let course of coursePage.content; index as index">
<td>{{index + 1}}</td>
<td>{{course.name}}</td>
<td>{{course.teacher.name}}</td> ?
<td>{{course.klass.name}}</td>
<td>
```
* ? 在進行數據表的設計時,課程是可以不設置任課教師的,所以course.teacher的值可能為null。當獲取的某個course沒有設置任課教師時,則會發生在null讀取name屬性的錯誤。
修正如下:
```html
<td>{{course.teacher?.name}}</td>
```
最終單元測試通過:

# 修正其它測試
由于變更了KlassSelectComponent的位置以及所屬模塊所以必然會引起其它的單元測試錯誤。逐個解決如下:
## 某組件同時存在于多個模塊中
```
student/AddComponent > should create
Failed: Type KlassSelectComponent is part of the declarations of 2 modules: CoreModule and DynamicTestModule! Please consider moving KlassSelectComponent to a higher module that imports CoreModule and DynamicTestModule. You can also create a new NgModule that exports and includes KlassSelectComponent then import that NgModule in CoreModule and DynamicTestModule.
```
在angular中某個組件不能同時存在于兩個模塊中(除非這兩個模塊不知道對方的存在)。這時由于在測試模塊(文件)中將KlassSelectComponent聲明到了declarations中。修正如下:
student/add/add.component.spec.ts
```typescript
declarations: [AddComponent, KlassSelectComponent?],
```
student/index/index.component.spec.ts
```typescript
declarations: [AddComponent, KlassSelectComponent?],
```
core/klass-select/klass-select.component.spec.ts
```typescript
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [KlassSelectComponent, SelectComponent??],
imports: [CoreModule??, HttpClientTestingModule]
})
```
* ? KlassSelectComponent依賴于原CoreModule的SelectComponent,此時兩個組件位于同一個模塊下,直接引用即可
* ? KlassSelectComponent當前已經屬于CoreModule,不能再進行引用,否則將發生組件位于多模塊的錯誤
最后加入其它依賴:
```typescript
imports: [HttpClientTestingModule,
ReactiveFormsModule?]
})
```
單元測試整體通過。
# 參考文檔
| 名稱 | 鏈接 | 預計學習時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.2.1](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.2.1) | - |
| 安全導航運算符( ? )和空屬性路徑 | [https://www.angular.cn/guide/template-syntax#the-safe-navigation-operator----and-null-property-paths](https://www.angular.cn/guide/template-syntax#the-safe-navigation-operator----and-null-property-paths) | 10 |
- 序言
- 第一章:Hello World
- 第一節:Angular準備工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二節:Hello Angular
- 第三節:Spring Boot準備工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四節:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven國內源配置
- 4 package與import
- 第五節:Hello Spring Boot + Angular
- 1 依賴注入【前】
- 2 HttpClient獲取數據【前】
- 3 數據綁定【前】
- 4 回調函數【選學】
- 第二章 教師管理
- 第一節 數據庫初始化
- 第二節 CRUD之R查數據
- 1 原型初始化【前】
- 2 連接數據庫【后】
- 3 使用JDBC讀取數據【后】
- 4 前后臺對接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三節 CRUD之C增數據
- 1 新建組件并映射路由【前】
- 2 模板驅動表單【前】
- 3 httpClient post請求【前】
- 4 保存數據【后】
- 5 組件間調用【前】
- 第四節 CRUD之U改數據
- 1 路由參數【前】
- 2 請求映射【后】
- 3 前后臺對接【前】
- 4 更新數據【前】
- 5 更新某個教師【后】
- 6 路由器鏈接【前】
- 7 觀察者模式【前】
- 第五節 CRUD之D刪數據
- 1 綁定到用戶輸入事件【前】
- 2 刪除某個教師【后】
- 第六節 代碼重構
- 1 文件夾化【前】
- 2 優化交互體驗【前】
- 3 相對與絕對地址【前】
- 第三章 班級管理
- 第一節 JPA初始化數據表
- 第二節 班級列表
- 1 新建模塊【前】
- 2 初識單元測試【前】
- 3 初始化原型【前】
- 4 面向對象【前】
- 5 測試HTTP請求【前】
- 6 測試INPUT【前】
- 7 測試BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后臺對接【前】
- 第三節 新增班級
- 1 初始化【前】
- 2 響應式表單【前】
- 3 測試POST請求【前】
- 4 JPA插入數據【后】
- 5 單元測試【后】
- 6 惰性加載【前】
- 7 對接【前】
- 第四節 編輯班級
- 1 FormGroup【前】
- 2 x、[x]、{{x}}與(x)【前】
- 3 模擬路由服務【前】
- 4 測試間諜spy【前】
- 5 使用JPA更新數據【后】
- 6 分層開發【后】
- 7 前后臺對接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五節 選擇教師組件
- 1 初始化【前】
- 2 動態數據綁定【前】
- 3 初識泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再識單元測試【前】
- 7 其它問題
- 第六節 刪除班級
- 1 TDD【前】
- 2 TDD【后】
- 3 前后臺對接
- 第四章 學生管理
- 第一節 引入Bootstrap【前】
- 第二節 NAV導航組件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三節 footer組件【前】
- 第四節 歡迎界面【前】
- 第五節 新增學生
- 1 初始化【前】
- 2 選擇班級組件【前】
- 3 復用選擇組件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校驗【后】
- 7 唯一性校驗【后】
- 8 @PrePersist【后】
- 9 CM層開發【后】
- 10 集成測試
- 第六節 學生列表
- 1 分頁【后】
- 2 HashMap與LinkedHashMap
- 3 初識綜合查詢【后】
- 4 綜合查詢進階【后】
- 5 小試綜合查詢【后】
- 6 初始化【前】
- 7 M層【前】
- 8 單元測試與分頁【前】
- 9 單選與多選【前】
- 10 集成測試
- 第七節 編輯學生
- 1 初始化【前】
- 2 嵌套組件測試【前】
- 3 功能開發【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成測試
- 7 @Input 異步傳值【前】
- 8 值傳遞與引入傳遞
- 9 @PreUpdate【后】
- 10 表單驗證【前】
- 第八節 刪除學生
- 1 CSS選擇器【前】
- 2 confirm【前】
- 3 功能開發與測試【后】
- 4 集成測試
- 5 定制提示框【前】
- 6 引入圖標庫【前】
- 第九節 集成測試
- 第五章 登錄與注銷
- 第一節:普通登錄
- 1 原型【前】
- 2 功能設計【前】
- 3 功能設計【后】
- 4 應用登錄組件【前】
- 5 注銷【前】
- 6 保留登錄狀態【前】
- 第二節:你是誰
- 1 過濾器【后】
- 2 令牌機制【后】
- 3 裝飾器模式【后】
- 4 攔截器【前】
- 5 RxJS操作符【前】
- 6 用戶登錄與注銷【后】
- 7 個人中心【前】
- 8 攔截器【后】
- 9 集成測試
- 10 單例模式
- 第六章 課程管理
- 第一節 新增課程
- 1 初始化【前】
- 2 嵌套組件測試【前】
- 3 async管道【前】
- 4 優雅的測試【前】
- 5 功能開發【前】
- 6 實體監聽器【后】
- 7 @ManyToMany【后】
- 8 集成測試【前】
- 9 異步驗證器【前】
- 10 詳解CORS【前】
- 第二節 課程列表
- 第三節 果斷
- 1 初始化【前】
- 2 分頁組件【前】
- 2 分頁組件【前】
- 3 綜合查詢【前】
- 4 綜合查詢【后】
- 4 綜合查詢【后】
- 第節 班級列表
- 第節 教師列表
- 第節 編輯課程
- TODO返回機制【前】
- 4 彈出框組件【前】
- 5 多路由出口【前】
- 第節 刪除課程
- 第七章 權限管理
- 第一節 AOP
- 總結
- 開發規范
- 備用