# 集成测试
前后台全部完成后,原则可以进行集成测试了。
```
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 ✚
],
```
# 数据准备
现在可以为登录组件先映射一个路由。前台启动的时候做一个用户是否已登录的判断,如果未登录则跳转到登录的界面,用户使用用户名密码登录成功则跳转到首页,否则提示用户名或密码错误。
按此思路先做一些准备工作:启动前后台并添加一个测试教师。
![](https://img.kancloud.cn/03/94/039425327a377436e92255589c9e7b35_316x205.png)
# 添加路由
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');
}
}
```
测试效果
![](https://img.kancloud.cn/06/85/0685b717e7bae2a3711051f701aa8185_914x409.gif)
整体的效果虽然有了,但最上侧的菜单却一直显示着而且点击的时候直接就绕过了认证,这并不是我们想看到了。导航的菜单之所以会出现,是由于游离在路由以外,这点可以由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('用户名密码错误');
}
});
}
```
此方案的测试效果如下:
![](https://img.kancloud.cn/1e/4b/1e4b500f09819b84a6a675f3ba9a8bab_984x390.gif)
该方案虽然暂时的解决了当前的问题,但在生产项目中依赖于"用户是否已登录"的功能不止"决定是否显示导航组件"一个,这将需要在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('用户名密码错误');
}
});
```
最终的测试结果
![](https://img.kancloud.cn/1e/4b/1e4b500f09819b84a6a675f3ba9a8bab_984x390.gif)
但这里有个小问题:无论用户在哪个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://img.kancloud.cn/5c/e0/5ce0f9877db85a9b56c1518858ac1aab_984x390.gif)
至此一个简单的不实用的登录功能便完成了。
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [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
- 总结
- 开发规范
- 备用