# 集成測試
前后臺全部完成后,原則可以進行集成測試了。
```
compiler.js:2175 Uncaught Error: Template parse errors:
Can't bind to 'formGroup' since it isn't a known property of 'form'. ("<div class="row justify-content-center">
<div class="col-4">
<form [ERROR ->][formGroup]="formGroup" (ngSubmit)="onSubmit()">
```
src/app/app.module.ts
```javascript
RouterModule,
ReactiveFormsModule ?
],
```
# 數據準備
現在可以為登錄組件先映射一個路由。前臺啟動的時候做一個用戶是否已登錄的判斷,如果未登錄則跳轉到登錄的界面,用戶使用用戶名密碼登錄成功則跳轉到首頁,否則提示用戶名或密碼錯誤。
按此思路先做一些準備工作:啟動前后臺并添加一個測試教師。

# 添加路由
src/app/app-routing.module.ts
```javascript
{
path: 'login',
component: LoginComponent
},
```
打開`http://localhost:4200/login`測試路由綁定成功。
# 默認跳轉到首頁
當前系統的啟動組件為Appcomponent,同時意味著只要前臺啟動那么AppComponent必然被渲染。那么可以將系統啟動時跳轉到登錄界面的按鈕
src/app/app.component.ts
```javascript
export class AppComponent implements OnInit {
constructor(private route: Router) {
}
ngOnInit(): void {
this.route.navigateByUrl('login');
}
}
```
測試效果

整體的效果雖然有了,但最上側的菜單卻一直顯示著而且點擊的時候直接就繞過了認證,這并不是我們想看到了。導航的菜單之所以會出現,是由于游離在路由以外,這點可以由app.component.html得到驗證:
src/app/app.component.html
```html
<app-nav></app-nav> ?
<div class="container">
<router-outlet></router-outlet> ?
</div>
<app-footer></app-footer>
```
* ? 顯示導航菜單
* ? 顯示登錄組件
# 登錄狀態數據源
解決這個問題的方法有幾種,比如先在app組件中設置isLogin字段來記錄用戶是否登錄的信息,當用戶于登錄組件登錄成功后登錄app組件改變isLogin的值。然后在V層來使用ng-if來控制是否顯示導航組件。示例代碼如下:
src/app/app.component.ts
```javascript
isLogin = false;
```
src/app/app.component.html
```html
<app-nav *ngIf="isLogin"></app-nav>
```
src/app/login/login.component.ts
```
constructor(private teacherService: TeacherService,
private appComponent: AppComponent,
private router: Router) {
}
onSubmit() {
const username = this.formGroup.get('username').value;
const password = this.formGroup.get('password').value;
this.teacherService.login(username, password).subscribe(result => {
if (result) {
this.appComponent.isLogin = true;
this.router.navigateByUrl('');
} else {
console.log('用戶名密碼錯誤');
}
});
}
```
此方案的測試效果如下:

該方案雖然暫時的解決了當前的問題,但在生產項目中依賴于"用戶是否已登錄"的功能不止"決定是否顯示導航組件"一個,這將需要在login組件中依次對依賴用戶登錄狀態的組件做通知。這不僅破壞了"對擴展開放、對修改關閉"的原則,而且在login組件中獲取不到非app模塊中聲明的其它組件。
>[success] 對擴展開放、對修改關閉。以login組件為例,對修改關閉是指:其它的組件需要是用戶的登錄狀態時,不應該修正login組件中的內容;對擴展開放是指:其它的組件需要是用戶的登錄狀態時,可以調用login組件的相關方法來獲得。
## 服務層數據流
服務層的數據源恰好能夠很好的解決此類問題。把一些公共的數據由組件中抽離到服務層中,由于服務層是貫穿于整個應用的,所以任何組件都可以很輕松的獲取到該服務。用戶的登錄狀態信息會隨著用戶登錄->用戶注銷->用戶再登錄->用戶再注銷而不斷的產生新值,這是非常典型的數據流。
所以可以在TeacherService中建立是否登錄數據流,其它需要用戶登錄狀態的組件可以隨時的訂閱該數據流從而獲取用戶是否登錄的狀態。
service/teacher.service.ts
```javascript
export class TeacherService {
/** 數據源 */
private isLogin = new BehaviorSubject<boolean>(false); ?
/** 數據源對應的訂閱服務 */
public isLogin$ = this.isLogin.asObservable(); ?
/**
* 設置登錄狀態
* @param isLogin 登錄狀態
*/
setIsLogin(isLogin: boolean) {
this.isLogin.next(isLogin); ?
}
```
* ? BehaviorSubject是新建數據源的一種方式。該數據源中緩存著最后一次發送的數據,當有新的訂閱者時,首先將緩存的數據發送給訂閱者。
* ? BehaviorSubject相當于雜志社,調用其asObservable()相當于返回其報刊訂閱部門,專門提供訂閱服務。(雜志社可以做的事情大多了,直接使用雜志社對外提供所有的服務是會出亂子的)
* ? 接收到新的登錄狀態時,向所有的訂閱者們發送最新的登錄狀態的值
然后App組件便可改寫為:
src/app/app.component.ts
```
export class AppComponent implements OnInit {
isLogin = false;
constructor(private route: Router,
private teacherService: TeacherService) {
}
ngOnInit(): void {
this.teacherService.isLogin$.subscribe(isLogin => this.isLogin = isLogin);
this.route.navigateByUrl('login');
}
}
```
login組件改寫為:
src/app/login/login.component.ts
```
constructor(private teacherService: TeacherService,
private appComponent: AppComponent, ?
private router: Router) {
}
...
this.teacherService.login(username, password).subscribe(result => {
if (result) {
this.appComponent.isLogin = true; ?
this.router.navigateByUrl('');
this.teacherService.setIsLogin(true);
} else {
console.log('用戶名密碼錯誤');
}
});
```
最終的測試結果

但這里有個小問題:無論用戶在哪個url刷新頁面,用戶輸入用戶名密碼后都會跳轉到首頁。而用戶更希望能夠跳轉到原來的url。比如用戶在教師管理上點刷新頁面,此時跳轉到登錄頁,輸入用戶名密碼后用戶還希望能夠自動的跳轉到教師管理頁面。這當然可以通過加入用戶點擊緩存來實現:先把用戶跳轉到登錄頁面前的地址緩存起來,登錄成功后再取出該緩存的地址。接下來我們會提供一種更好更簡單的解決方法。
# 集成登錄界面
找開src/app/app.component.html,按以下代碼進行整理:
```html
<app-nav *ngIf="isLogin"></app-nav>
<div class="container">
<app-login *ngIf="!isLogin"></app-login> ?
<router-outlet *ngIf="isLogin"></router-outlet> ?
</div>
<app-footer></app-footer>
```
* ? 將組件與路由并列直接顯示在啟動組件中
* ? 如果用戶未登錄,則顯示登錄組件;如果已登錄則顯示路由渲染的內容
此時,便可以一些冗余的邏輯了:
src/app/login/login.component.ts
```javascript
constructor(private teacherService: TeacherService,
private appComponent: AppComponent,
private router: Router?) {
}
if (result) {
this.router.navigateByUrl(''); ?
this.teacherService.setIsLogin(true);
} else {
console.log('用戶名密碼錯誤');
}
```
src/app/app-routing.module.ts
```javascript
{
path: '',
component: WelcomeComponent
},
{ ?
path: 'login', ?
component: LoginComponent ?
}, ?
{
```
src/app/app.component.ts
```javascript=
constructor(private teacherService: TeacherService,
private router: Router ?) {
ngOnInit(): void {
this.teacherService.isLogin$.subscribe(isLogin => this.isLogin = isLogin);
this.route.navigateByUrl('login'); ?
}
```
測試效果:

至此一個簡單的不實用的登錄功能便完成了。
# 參考文檔
| 名稱 | 鏈接 | 預計學習時長(分) |
| --- | --- | --- |
| 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.1.4](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.1.4) | - |
| BehaviorSubject | [https://cn.rx.js.org/manual/overview.html#h26](https://cn.rx.js.org/manual/overview.html#h26) | 5 |
- 序言
- 第一章: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
- 總結
- 開發規范
- 備用