一切由实践中来,一切又要到实践中去。在没有进行正式的对接前还不能说当前的思想及实现是一定没有问题的。本节将利用上一小节中的/Teacher/me接口来实现简单的个人中心功能。
# 初始化
打开前台并来到app文件夹,使用`ng g c PersonalCenter`初始化个人中心
```
panjiedeMac-Pro:app panjie$ ng g c PersonalCenter
CREATE src/app/personal-center/personal-center.component.sass (0 bytes)
CREATE src/app/personal-center/personal-center.component.html (30 bytes)
CREATE src/app/personal-center/personal-center.component.spec.ts (685 bytes)
CREATE src/app/personal-center/personal-center.component.ts (305 bytes)
UPDATE src/app/app.module.ts (1356 bytes)
```
将personal-center.component.spec.ts中的测试用例由`it`暂时修改为`fit`后启动单元测试。
## C层
personal-center/personal-center.component.ts
```typescript
import { Component, OnInit } from '@angular/core';
import {Teacher} from '../norm/entity/Teacher';
@Component({
selector: 'app-personal-center',
templateUrl: './personal-center.component.html',
styleUrls: ['./personal-center.component.sass']
})
export class PersonalCenterComponent implements OnInit {
/** 绑定到V层 */
public teacher: Teacher;
constructor() { }
ngOnInit() {
// 调用M层的相关方法
}
}
```
## V层
在V层显示当前登录用户的基本信息:用户名、姓名、性别、email四项信息。
personal-center.component.html
```html
<div class="row">
<div class="col-sm-2 text-right">姓名</div>
<div class="col-sm-10">
{{teacher.name}}
</div>
</div>
<div class="row">
<div class="col-sm-2 text-right">用户名</div>
<div class="col-sm-10">
{{teacher.username}}
</div>
</div>
<div class="row">
<div class="col-sm-2 text-right">性别</div>
<div class="col-sm-10">
{{teacher.sex}}
</div>
</div>
<div class="row">
<div class="col-sm-2 text-right">email</div>
<div class="col-sm-10">
{{teacher.email}}
</div>
</div>
```
## 单元测试
在CV层的基础上测试CV层的数据绑定是否成功。测试步骤为:1.在C层设置teacher的值。2.V层获取显示的值并进行断言
测试的重点在于如何通过V层的class值获取到输出的问题,具体的过程略。
# 功能开发
像这种直接由后台获取数据后显示在V层的逻辑非常的简单。C层中直接调用M层的相关方法获取返回值即可。
## M层
打开TeacherService新建me方法
service/teacher.service.ts
```typescript
/**
* 获取当前登录的教师
*/
me(): Observable<Teacher> {
const url = 'http://localhost:8080/Teacher/me';
return this.httpClient.get<Teacher>(url);
}
```
### 单元测试
service/teacher.service.spec.ts
```typescript
fit('me', () => {
// 获取service实例
const service: TeacherService = TestBed.get(TeacherService);
// 调用测试方法
let result;
service.me().subscribe((teacher) => {
result = teacher;
});
// 断言发起了特定的http请求
const httpTestingController: HttpTestingController = TestBed.get(HttpTestingController);
const req = httpTestingController.expectOne('http://localhost:8080/Teacher/me');
expect(req.request.method).toEqual('GET');
// 模拟返回数据,请断言在订阅的方法中成功的接收到了数据
const mockReturnTeacher = new Teacher(null, null, null, null);
req.flush(mockReturnTeacher);
expect(result).toBe(mockReturnTeacher);
});
```
## C层代码对接
personal-center/personal-center.component.ts
```typescript
constructor(private teacherService: TeacherService) { }
ngOnInit() {
// 调用M层的相关方法
this.teacherService.me().subscribe((teacher) => {
this.teacher = teacher;
});
}
```
### 单元测试
在单元测试中,沿用引用TestModule的方式。该方式能够自动注入TeacherService的替身TeacherStubService
personal-center/personal-center.component.spec.ts
```typescript
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PersonalCenterComponent ],
imports: [
TestModule ➊
]
})
.compileComponents();
}));
fit('ngOnInit', () => {
});
```
* ➊ 引入TestModule
初充功能如下:
personal-center/personal-center.component.spec.ts
```typescript
fit('ngOnInit', () => {
const teacherService = TestBed.get(TeacherService) as TeacherService;
const mockReturnTeacher = new Teacher(null, null, null);
spyOn(teacherService, 'me').and.returnValue(of(mockReturnTeacher));
component.ngOnInit();
expect(component.teacher).toBe(mockReturnTeacher);
});
```
测试结果:
```
TypeError: this.teacherService.me is not a function
```
此错误前面碰到过,原因是由于做为替身出现的TeacherStubService中并没有me方法,所以在调用`spyOn(teacherService, 'me')`出现了该错误。找到此替身被加入`me`方法
test/service/teacher-stub.service.ts
```typescript
me(): Observable<Teacher> {
return null;
}
```
出现错误如下:
```
TypeError: Cannot read property 'subscribe' of null
...
at PersonalCenterComponent.ngOnInit (http://localhost:9876/_karma_webpack_/src/app/personal-center/personal-center.component.ts:17:29)
```
错误显示在以下代码的执行过程中发出了null错误:
personal-center/personal-center.component.ts
```typescript
ngOnInit() {
// 调用M层的相关方法
this.teacherService.me()★.subscribe((teacher) => {
this.teacher = teacher;
});
}
```
* ★ 此方法的返回值为null
这个此错误的原因如下:
在单元测试文件中的测试用例执行以前首先会构建一个测试组件,语句如下:
```
beforeEach(() => {
fixture = TestBed.createComponent(PersonalCenterComponent);
component = fixture.componentInstance;
fixture.detectChanges(); ★
});
```
* ★ 此语句首次被执行时,将调用`ngOnInit`方法一次。
该测试组件构造完成后,才会执行测试用例中的测试代码。而前面的错误就在于执行`fixture.detectChanges()`时自动调用的`ngOnInit`上。此时执行`ngOnInit`时,组件中的teacherService为替身TeacherStubService,所以在执行teacherService.me()方法时相当于调用的是TeacherStubService中的me()方法,而该方法的返回值被设置为null。便发生了在null上调用subscribe方法的错误。
>[success] 在构建组件时,组件中的ngOnInit方法会被自动调用一次。测试代码中的fixture.detectChanges()模似了该过程。
修正方法为设置TeacherService.me()方法的返回值:
test/service/teacher-stub.service.ts
```typescript
me(): Observable<Teacher> {
return of(new Teacher(1, 'username', 'name'));
}
```
>[info] 在生产环境中,会为此方法设置与后台返回字段一一对应的示例返回值。
再次运行单元测试通过。
# 集成测试
为个人中心设置路由及菜单项如下
app-routing.module.ts
```typescript
const routes: Routes = [
{
path: '',
component: WelcomeComponent
},
{
path: 'personalCenter',
component: PersonalCenterComponent
},
{
path: 'teacher',
component: TeacherIndexComponent
},
```
nav/nav.component.ts
```typescript
this.menus.push({url: 'student', name: '学生管理'});
this.menus.push({url: 'personalCenter', name: '个人中心'});
}
```
启动前后台,并在数据表中添加登录用户后进行测试:
![](https://img.kancloud.cn/5f/70/5f70736737853e9889802ab296c7ba18_1248x366.gif)
测试成功,当访问个人中心时,显示了当前的登录用户信息。但看似一切正常的程序其实尚有bug:打开控制台会发现控制台中尚有错误:
![](https://img.kancloud.cn/d8/25/d82580f5bb43bdae1d459b2f157678e1_1384x131.png)
提示说在V层的第3行有在undefined上读取name属性的错误。
如果在个人中心处进行页面的刷新,还会产生如下错误:
![](https://img.kancloud.cn/04/f3/04f3fa8696d1eda7f2ac3ee60682973d_1248x366.gif)
刷新页面后个人中心中的内容不见了。。。
## undefined上读取name属性
该错误的产生是由于向后台进行数据请求是异步产生了,在前面的章节中已经介绍过这个问题。在此重复介绍一遍。个人中心的C层代码如下:
```typescript
/** 绑定到V层 */
public teacher: Teacher;
constructor(private teacherService: TeacherService) {
// ➊ 构造此类,此时this.teacher的类型为undefined
}
ngOnInit() ➋ {
// 调用M层的相关方法
this.teacherService.me().subscribe➌((teacher) => {
this.teacher = teacher; ➎
// ➏ 与V层绑定的数据this.teacher发生变化,再次进行渲染。此时this.teacher非undefiend
});
// ➍ ngOnInit方法执行结束后,渲染V层(多次渲染)。此时this.teacher的类型为undefined发生错误
}
```
执行方法➌时,由于该方法最终调用的是httpClient.get向后台发起请求,js无法判断此请求的时间,所以不会等待其返回以后再继续执行,而是选择直接执行➍。➍执行完毕后,请求后台有了结果,此时再执行➎。所以最终的执行顺序为 ➊ -> ➋ -> ➌ -> ➍ -> ➎ -> ➏
为了更加清晰的看到上述的执行过程,使用以下代码改造该文件:
```typescript
export class PersonalCenterComponent implements OnInit {
/** 绑定到V层 */
public teacher: Teacher;
constructor(private teacherService: TeacherService) {
console.log('construct');
}
ngOnInit() {
console.log('ngInit');
// 调用M层的相关方法
this.teacherService.me().subscribe((teacher) => {
console.log('return teacher');
this.teacher = teacher;
console.log('set return teacher');
});
console.log('done');
}
}
```
然后再次测试(不要在个人中心页面刷新,应该先登录然后再点个人中心),控制台信息如下:
```
personal-center.component.ts:14 construct
personal-center.component.ts:18 ngInit
personal-center.component.ts:25 go
PersonalCenterComponent.html:3 ERROR TypeError: Cannot read property 'name' of undefined
PersonalCenterComponent.html:3 ERROR CONTEXT
PersonalCenterComponent.html:3 ERROR TypeError: Cannot read property 'name' of undefined
PersonalCenterComponent.html:3 ERROR CONTEXT
personal-center.component.ts:21 return teacher
personal-center.component.ts:23 set return teacher
```
以上信息足以印证了刚刚对执行过程的猜测,也进一步的验证了在单元测试中`detectChanges()`方法的作用。在单元测试中需要手动的调用`detectChanges()`来完成V层的渲染,在非单元测试中angular会自动调用组件的`detectChanges`方法来完成组件的渲染,且会渲染多次,当连续两次渲染的结果相同时认为渲染成功终止当次渲染。
angular在V层提供了`?`来避免异步请求渲染出现在undefined上读取属性错误的问题。在进行渲染时angular遇到`?`关键字,则首先会查看用`?`标识的变量的类型是否为undefined。如果为undefined,则停止此字段的渲染,否则继续此字段的渲染。所以对V层进行如下的修正:
```
{{teacher?.name}}
```
其它的字段请自行修正,将`teacher.name`修改为`teacher?.name`后,渲染到该字段时会首先对teacher是否为undefined进行判断。再次测试错误消失。
## 刷新个人中心异常
刷新个人中心后未显示当前登录用户的信息。这是由于前台、后台的设计思想都有些问题造成的。
### 前台
对前台而言,每次进行页面刷新后的首次请求会使用空的auth-token请求后台,这导致了后台为刷新的请求分配了新的auth-token,而此auth-token并未与任何的用户进行绑定。所以当用户使用最新分发的未与任何用户绑定的auth-token请求me接口时,接收到了空值,近而导致个人中心的内容为空。
### 后台
对后台而言,用户请求me接口的前提应该是:用户使用了已绑定了用户的auth-token,因为只有在这个前提下,才可能给用户返回预期的值。而当使用了未绑定用户的auth-token时,后台应该给前台发送**请先完成系统登录**的前置条件,而非返回了空值。
在一下个小节中,将使用后台的拦截器完成接口的认证校验问题。每次请求后台均要进行拦截,如果当前请求访问的是用户登录接口则放行,如果是其它接口则对当前auth-token是否已绑定了用户,未绑定则返回相应的错误信息以提示前台。
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.7](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.7) | - |
- 序言
- 第一章: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
- 总结
- 开发规范
- 备用