學生管理為學生模塊的第一個組件。所以在生成新增學生組件前,我們先在app根路徑使用`ng g m student`中生成了一個學生模塊。再使用`ng g c add`來生成添加學生組件。
```
panjiedeMac-Pro:app panjie$ ng g m student
CREATE src/app/student/student.module.ts (193 bytes)
panjiedeMac-Pro:app panjie$ cd student/
panjiedeMac-Pro:student panjie$ ng g c add
CREATE src/app/student/add/add.component.sass (0 bytes)
CREATE src/app/student/add/add.component.html (18 bytes)
CREATE src/app/student/add/add.component.spec.ts (607 bytes)
CREATE src/app/student/add/add.component.ts (258 bytes)
UPDATE src/app/student/student.module.ts (257 bytes)
```
其實做到這里便可以繼續開發了。但為了更加的貼近于`更佳實踐`,我們首先做些重構的工作。
## 剝離路由
正式開始本節內容以前,先給上一章班級管理補個刀。仔細觀察下 app模塊和klass模塊模塊我們會發現,這兩個模塊在路由的配置上有所不同:
在app模塊中,有一個專門來定義路由的app-routing.module.ts

而在klass模塊中,我們并沒有專門的路由文件:

angular的[官方文檔](https://www.angular.cn/guide/router#milestone-2-routing-module)對這兩種方式分別進行描述,并指出并不強制使用哪種模式,但同時也指出在一個項目我們應該統一風格。要么將路由配置直接寫到模塊中,要么將路由配置統一寫到路由模塊中。在團隊的實際生產環境中我們更愿意將路由模塊寫到單獨的路由模塊中,這樣做最少有2個好處:① 減少模塊類的代碼量,更易讀;② 是否在某個模塊中配置了路由一目了解。
### 重構klass模塊
首先,我們對歷史的klass模塊進行重構,進入klass文件夾并新建`klass-routing.module.ts`,然后將路由設置的信息由`klass/klass.module.ts`轉移到`klass-routing.module.ts`中。
klass/klass-routing.module.ts
```
import {RouterModule, Routes} from '@angular/router';
import {IndexComponent} from './index/index.component';
import {AddComponent} from './add/add.component';
import {EditComponent} from './edit/edit.component';
import {NgModule} from '@angular/core';
/*定義路由*/
const routes: Routes = [
{
path: '',
component: IndexComponent
}, {
path: 'add',
component: AddComponent
}, {
path: 'edit/:id',
component: EditComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)]
})
export class KlassRoutingModule {}
```
我們在此聲明的路由對應的組件均屬于`KlassModule`,所以想讓此路由信息生效則需要將其添加到對應的`KlassModule`中。在angular中,想讓其它模塊使用本模塊內部的東西,則需要將其添加到`export`中:
klass/klass-routing.module.ts
```
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule] ①
})
export class KlassRoutingModule {}
```
* ① 將使用本模塊routes變量配置過的RouterModule拋出。
此操作的作用是:在`KlassRoutingModule`上捆綁`RouteModule`,其它模塊在`import KlassRoutingModule`時,將自動的引入`KlassRoutingModule`身上捆綁的`RouteModule`。
最后我們在KlassModule中引入該路由模塊,重構完畢。
klass/klass.module.ts
```
@NgModule({
declarations: [IndexComponent, AddComponent, EditComponent, TeacherSelectComponent],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
KlassRoutingModule ①
]
})
export class KlassModule {
}
```
* ① 引入KlassRoutingModule的同時,引用了其捆綁的`RouteModule`。該`RouteModule`已經配置了路由信息,進而使得路由信息在本模塊中生效。
補刀結束,回歸主題。
### 新增student路由
參考剛剛補刀的過程為student模塊來建立單獨的路由模塊。
student/student-routing.module.ts
```javascript
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
@NgModule({
exports: [RouterModule]
})
export class StudentRoutingModule {
}
```
然后在student模塊中引入該路由模塊
student/student.module.ts
```javascript
@NgModule({
declarations: [AddComponent],
imports: [
CommonModule,
StudentRoutingModule
]
})
export class StudentModule {
}
```
## V層初始化
student/add/add.component.html
```html
<h2>編輯教師①</h2>
<form (ngSubmit)="onSubmit()">
<label>姓名:<input name="name"/></label>
<label>學號:<input name="sno"/></label>
<label>班級:todo?</label>
<button>保存</button>
</form>
```
* ① 此處筆者在復制內容時發生了錯誤,正常的標題為:添加學生
* ? 此處應該用班級列表組件,由于還不存在,所以我們用TODO來標記一下。
> 此處有錯誤
在進行初始化時不要怕錯,也不要怕界面難看。界面錯了我們后面會結合C層及單元測試進行修正,而界面的好看則應該是**集成測試**的任務而非當前初始化的任務。
## 建立實體類
angular的cli除了可以幫助我們快速的建立模塊、組件以外還可以做很多我們想到的或是想不到的事實,比如創建實體。來到norm/entity文件夾,并執行`ng g class student`,則會自動生成實體及實體的測試文件。
norm/entity/student.ts (angular為我們生成的是student而不是Student,看來我們以前對Klass及Teacher的命名都錯了。。)
```javascript
import {NgModule} from '@angular/core';
@NgModule({})
export class Student {
id: number;
name: string;
sno: string;
constructor(data???: { id?: number; name?: string; sno?: string }) {
if (!data) { ?
return;
}
this.id = data.id ? data.id : null; ?
this.name = data.name ? data.name : '';
this.sno = data.sno ? data.sno : '';
}
}
```
在此,我們在構造函數中使用了一種更優的實踐。該方法將使得實例化該類具有高度的靈活性。
* ? 構造函數直接接收對象,而非某個字段。當實體屬性發生變動時整體項目的改動最小
* ? `?`表示此參數為可選參數。可以傳、也可以不傳
* ? 規避未傳data時可以造成的錯誤
* ? 按傳入的參數值賦初值或設置默認值
### 單元測試
norm/entity/student.spec.ts
```javascript
// @ts-ignore ?
import {Student} from './student';
describe('Student', () => {
fit('should create an instance', () => {
expect(new Student()).toBeTruthy(); ?
expect(new Student({})).toBeTruthy(); ?
expect(new Student({id: 1, name: 'test', sno: '100021'})); ?
expect(new Student({id: 1})).toBeTruthy(); ?
expect(new Student({name: 'hello', id: 2, sno: '123'})).toBeTruthy(); ?
expect(new Student({sno: '456'})).toBeTruthy(); ?
});
});
```
* ? 忽略IDE報的TS語法錯誤(實際上并沒有問題)。
* ? 支持空參數初始化
* ? 支持空object初始化
* ? 支持傳入所有的字段初始化
* ? 支持傳入個別字段初始化
* ? 支持調換字段的書寫順序初始化
* ? 支持只傳入個別非首參數初始化
```
Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 1 of 17 (skipped 16) SUCCESS (0.04 secs / 0.003 secs)
TOTAL: 1 SUCCESS
TOTAL: 1 SUCCESS
```
如我們上面的單元測試所示,在student的構造函數中我們使用一種更優的方法后,在初始化Student時便更加靈活了。不僅如此,假設有一天有了新需求:需要為學生增加入學年份字段,那么只需要在構造函數中增加`year?:number`即可,而項目中的其它代碼我們完全不需要進行改動。
**小測試:** 分別為Student及Teacher類增加一個字段`createTime: number`,并將其添加到構造函數中,然后體現一下兩者的區別。
## C層
student/add/add.component.ts
```javascript
import {Component, OnInit} from '@angular/core';
import {Student} from '../../norm/entity/student';
import {FormControl, FormGroup} from '@angular/forms';
@Component({
selector: 'app-add',
templateUrl: './add.component.html',
styleUrls: ['./add.component.sass']
})
export class AddComponent implements OnInit {
student: Student;
formGroup: FormGroup;
constructor() {
}
ngOnInit() {
this.student = new Student();
this.formGroup = new FormGroup({
name: new FormControl(''),
sno: new FormControl('')
});
}
onSubmit(): void {
this.student = this.formGroup.value;
console.log(this.student);
}
}
```
### 修正V層
C層代碼完成后,我們繼續修正V層。將表單與C層中的屬性相關聯:
student/add/add.component.ts
```html
<h2>編輯教師</h2>
<form (ngSubmit)="onSubmit()" [formGroup]="formGroup">
<label>姓名:<input name="name" formControlName="name"/></label>
<label>學號:<input name="sno" formControlName="sno" /></label>
<label>班級:todo</label>
<button>保存</button>
</form>
```
#### 單元測試
測試初始化
student/add/add.component.spec.ts
```javascript
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {AddComponent} from './add.component';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
describe('student/AddComponent', () => {
let component: AddComponent;
let fixture: ComponentFixture<AddComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AddComponent],
imports: [
ReactiveFormsModule
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
fit('should create', () => {
expect(component).toBeTruthy();
});
});
```
### 重構測試公用類
在前面的測試中,我們開發了testing/FormTest來輔助進行一些表單的測試。在實際的使用中我們發現,進行任何表單的操作都需要一個`fixture`平具,在此我們將夾具剝離到構造函數中。
在testing/FormTest.ts的首部添加如下代碼:
```javascript
/**
* 表單測試
*/
export class FormTest<T?> {
private readonly? fixture: ComponentFixture<T?>;
constructor(fixture: ComponentFixture<T?>) {
this.fixture = fixture;
}
```
* ? 在小容器ComponentFixture外面加了一個包裝FormTest。實際上能裝物質的還是ComponentFixture。一旦在包裝上規定了要裝個物質的種類,則包裝中的容器只能裝后該種類。
* ? 字段為只讀屬性。
在該文件的尾部我們加入以下方法:
```javascript
/**
* 設置input輸入的值
* @param cssSelector CSS選擇器
* @param value 值
*/
setInputValue(cssSelector: string, value: string): boolean {
return FormTest.setInputValue(this.fixture, cssSelector, value); ?
}
/**
* 點擊某個按鈕
* @param cssSelector CSS選擇器
*/
clickButton(cssSelector: string): boolean {
return FormTest.clickButton(this.fixture, cssSelector);?
}
```
* ? 不造重復的輪子,直接調用原來存在的靜態方法。
## 完善測試
student/add/add.component.spec.ts
```javascript
describe('student/AddComponent', () => {
let component: AddComponent;
let fixture: ComponentFixture<AddComponent>;
let formTest: FormTest<AddComponent>; ?
...
beforeEach(() => { ?
fixture = TestBed.createComponent(AddComponent);
component = fixture.componentInstance;
fixture.detectChanges();
formTest = new FormTest(fixture); ?
});
/**
* 1. 向表單中輸入值
* 2. 點擊保存按鈕
* 3. 斷言輸入的值傳入到了C層
*/
fit('should create', () => {
expect(component).toBeTruthy();
formTest.setInputValue('input[name="name"]', 'testname'); ①
formTest.setInputValue('input[name="sno"]', 'testno'); ①
formTest.clickButton('button[type="submit"]'); ②
fixture.detectChanges(); ③
expect(component.student.name).toEqual('testname'); ④
expect(component.student.sno).toEqual('testno'); ④
});
```
* ? 將一些測試用例可能會公共的對象,抽離到方法上層。
* ? 在beforeEach中出現的代碼將在每次測試用例被執行**前**,執行一次。
```
LOG: Object{name: 'testname', sno: 'testno'}
Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 0 of 17 SUCCESS (0 secs / 0 secs)
Chrome 78.0.3904 (Mac OS X 10.13.6): Executed 1 of 17 (skipped 16) SUCCESS (0.14 secs / 0.108 secs)
TOTAL: 1 SUCCESS
TOTAL: 1 SUCCESS
```

# 參考文檔
| 名稱 | 鏈接 | 預計學習時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.5.1](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.5.1) | \- |
- 序言
- 第一章: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
- 總結
- 開發規范
- 備用