# 集成测试 前后台全部完成后,原则可以进行集成测试了。 ``` 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 |