学生管理为学生模块的第一个组件。所以在生成新增学生组件前,我们先在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
![](https://img.kancloud.cn/ea/99/ea9956715f1d34f7cecfe55cff3c3706_326x74.png)
而在klass模块中,我们并没有专门的路由文件:
![](https://img.kancloud.cn/b9/ee/b9eec9b7a8fcdddd295995ac4235eed0_236x63.png)
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://img.kancloud.cn/e5/5a/e55a6c6f851089990df246e29ee34757_608x95.png)
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [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
- 总结
- 开发规范
- 备用