当所有的盲点都被消除后,最后的功能完成已然成为了最很简单的一环。
## 服务与实体
在生产项目中,往往会使用服务来完成与后台的交互工作,这在组件需要处理一些逻辑功能,或是需要与其它的服务进行交互时特别重要。
为此首先来到`src/app/service`文件夹创建StudentService:
```bash
panjie@panjies-iMac service % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app/service
panjie@panjies-iMac service % ng g s student
CREATE src/app/service/student.service.spec.ts (362 bytes)
CREATE src/app/service/student.service.ts (136 bytes)
```
然后该服务器添加新增学生方法:
```typescript
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class StudentService {
constructor() {
}
/**
* 新增学生.
*/
add(): Observable<any> {
return of();
}
}
```
为了在后续的其它学生相关组件中更好的处理学生这个实体,在继续进行之前,还需要来到`src/app/entity`文件夹,新建一个学生实体:
```bash
panjie@panjies-iMac entity % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app/entity
panjie@panjies-iMac entity % ng g class student
CREATE src/app/entity/student.spec.ts (158 bytes)
CREATE src/app/entity/student.ts (25 bytes)
```
然后在学生实体中定义属性、加入构造函数以及特别重要的注释:
```typescript
import {Clazz} from './clazz';
/**
* 学生.
*/
export class Student {
id: number;
/**
* 姓名.
*/
name: string;
/**
* 学号.
*/
number: string;
/**
* 手机号.
*/
phone: string;
/**
* email.
*/
email: string;
/**
* 班级.
*/
clazz: Clazz;
constructor(data = {} as
{
id?: number,
name?: string,
number?: string,
phone?: string,
email?: string,
clazz?: Clazz
}) {
this.id = data.id as number;
this.name = data.name as string;
this.number = data.number as string;
this.phone = data.phone as string;
this.email = data.email as string;
this.clazz = data.clazz as Clazz;
}
}
```
有了Student实体后,开始完成StudentService中的`add`方法。
## Add方法
添加学生时,需要接收姓名、学号、手机号、email、班级信息,故对参数初始化如下:
```typescript
+++ b/first-app/src/app/service/student.service.ts
@@ -1,5 +1,7 @@
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
+import {Student} from '../entity/student';
+import {Clazz} from '../entity/clazz';
@Injectable({
providedIn: 'root'
@@ -12,7 +14,14 @@ export class StudentService {
/**
* 新增学生.
*/
- add(): Observable<any> {
- return of();
+ add(data: ①{name: string, number: string, phone: string, email: string, clazzId: number}): Observable<Student> ②{
+ const student = new Student({
+ name: data.name,
+ number: data.number,
+ phone: data.phone,
+ email: data.email,
+ clazz: new Clazz({id: data.clazzId})
+ })③;
+ return of(student)④;
}
}
```
- ① 当参数类型设置为`{}`,以后扩展增加新字段时更方便。
- ② 新增成功后台将返回新增后的学生信息。
- ③ 使用`new Student()`的方法让编译器来对语法进行校验,防止不小心出现的拼写错误。
- ④ 在没有MockApi以前,暂时返回student。
## MockApi
添加学生的API与添加教师、添加班级的Api一致,在此即使不给出后台API的具体说明,相信我们也能够书写出正确的请求:
```typescript
+++ b/first-app/src/app/mock-api/student.mock.api.ts
@@ -1,5 +1,6 @@
-import {ApiInjector, MockApiInterface, RequestOptions} from '@yunzhi/ng-mock-api';
+import {ApiInjector, MockApiInterface, randomNumber, RequestOptions} from '@yunzhi/ng-mock-api';
import {HttpParams} from '@angular/common/http';
+import {Student} from '../entity/student';
/**
* 学生模拟API.
@@ -21,6 +22,15 @@ export class StudentMockApi implements MockApiInterface {
return false;
}
}
- }];
+ }, {
+ method: 'POST',
+ url: '/student',
+ result: ((urlMatches: string[], options: RequestOptions) => {
+ const student = options.body as Student;
+ // 模拟保存成功后生成ID
+ student.id = randomNumber();
+ return student;
+ })
+ }
+ ];
}
}
```
如果你想使自己的MockApi能够像真实的Api一样可以校验信息,则还可以适当的加入一些断言,比如在新增学生时,要求必须传入预新增学生的基本字段:
```typescript
result: ((urlMatches: string[], options: RequestOptions) => {
const student = options.body as Student;
+ Assert.isString(student.phone, student.email, student.number, student.name, '学生的基本信息未传全');
+ Assert.isNumber(student.clazz.id, '班级id校验失败');
student.id = randomNumber();
return student;
})
```
此时将对该模拟后台发起请求时,如果未传入相应的信息,`HttpClient`则会接收到了一个`error`。我们借用StudentService的测试文件来测试下发起请求时如果没有传入特定的字段,怎样来获取这个`error`:
```typescript
+++ b/first-app/src/app/service/student.service.spec.ts
@@ -1,16 +1,26 @@
-import { TestBed } from '@angular/core/testing';
+import {TestBed} from '@angular/core/testing';
-import { StudentService } from './student.service';
+import {StudentService} from './student.service';
+import {MockApiTestingModule} from '../mock-api/mock-api-testing.module';
+import {HttpClient} from '@angular/common/http';
describe('StudentService', () => {
let service: StudentService;
beforeEach(() => {
- TestBed.configureTestingModule({});
+ TestBed.configureTestingModule({
+ imports: [
+ MockApiTestingModule
+ ]
+ });
service = TestBed.inject(StudentService);
});
- it('should be created', () => {
+ fit('should be created', () => {
expect(service).toBeTruthy();
// TestBed.inject()可获取到当前动态测试模块的所有服务
+ const httpClient = TestBed.inject(HttpClient);
+ httpClient.post('/student', {})
+ .subscribe(success => console.log('success', success),
+ error => console.log('error', error));
});
});
```
当MockApi发生异常时,将会触发`subscribe`中的`error`方法,这与正常的后台请求报错的方式一致:
![image-20210414151827586](https://img.kancloud.cn/60/58/6058f8e3ca38a9f28aedc6e967d098ef_884x278.png)
## 返回预请求
有了MockApi后,我们在StudentService中发起这个请求:
```typescript
+++ b/first-app/src/app/service/student.service.ts
@@ -2,13 +2,14 @@ import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {Student} from '../entity/student';
import {Clazz} from '../entity/clazz';
+import {HttpClient} from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class StudentService {
- constructor() {
+ constructor(private httpClient: HttpClient) {
}
/**
@@ -22,6 +23,6 @@ export class StudentService {
email: data.email,
clazz: new Clazz({id: data.clazzId})
});
- return of(student);
+ // 将预请求信息返回
+ return this.httpClient.post<Student>('/student', student);
}
```
## 组件调用
其它工作准备完毕后,组件调用便成了最简单的一环:
```typescript
+++ b/first-app/src/app/student/add/add.component.ts
@@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {YzValidators} from '../../yz-validators';
import {YzAsyncValidators} from '../../yz-async-validators';
-import {HttpClient} from '@angular/common/http';
+import {StudentService} from '../../service/student.service';
@Component({
selector: 'app-add',
@@ -12,7 +12,7 @@ import {HttpClient} from '@angular/common/http';
export class AddComponent implements OnInit {
formGroup: FormGroup;
- constructor(private httpClient: HttpClient, private yzAsyncValidators: YzAsyncValidators) {
+ constructor(private studentService: StudentService, private yzAsyncValidators: YzAsyncValidators) {
this.formGroup = new FormGroup({
name: new FormControl('', Validators.required),
number: new FormControl('', Validators.required, yzAsyncValidators.numberNotExist()),
@@ -26,6 +26,15 @@ export class AddComponent implements OnInit {
}
onSubmit(): void {
- console.log('submit');
+ const student = this.formGroup.value① as② {
+ name: string,
+ number: string,
+ phone: string,
+ email: string,
+ clazzId: number
+ };
+ this.studentService.add(student)
+ .subscribe(success => console.log('保存成功', success),
+ error => console.log('保存失败', error));
}
}
```
- ① 可以使用`FormGroup.value`来获取整个`FormGroup`中所有`FormControl`的值
- ② 需要注意的是,这个虽然可能使用`as`将其转换为任意值,但这种转换也带来了一定的风险,比如我们在初始化`FormGroup`时,误把`email`写成了`emial`。
填写完所有的字段后,保存成功。
![image-20210414152940550](https://img.kancloud.cn/77/08/7708033943d1e8dff785cb9c8b917cfe_1558x358.png)
其实有时候我们很难将`onSubmit()`一次性的书写成功,比如我们以后需要加入保存成功后路由的跳转信息。所以在开发过程中往往需要屡次点击保存按钮,而点击该按钮前却需要将表单的所有字段输全,这明显是个重复的劳动,做为**懒人**的我们怎么能允许些类事情的发生。
如果在点击保存按钮前这些信息全部都为我们自动填写好,那该多好呀。🙃 还不快用单元测试?
```typescript
+++ b/first-app/src/app/student/add/add.component.spec.ts
@@ -8,6 +8,8 @@ import {getTestScheduler} from 'jasmine-marbles';
import {Observable, of} from 'rxjs';
import {map} from 'rxjs/operators';
import {LoadingModule} from '../../directive/loading/loading.module';
+import {randomString} from '@yunzhi/ng-mock-api/testing';
+import {randomNumber} from '@yunzhi/ng-mock-api';
describe('student -> AddComponent', () => {
let component: AddComponent;
@@ -40,6 +42,24 @@ describe('student -> AddComponent', () => {
fixture.autoDetectChanges();
});
+ fit('自动填充要新建的学生数据', () => {
+ // 固定写法
+ getTestScheduler().flush();
+ fixture.detectChanges();
+
+ component.formGroup.setValue({
+ name: randomString('姓名'),
+ number: randomNumber().toString(),
+ phone: '13900000000',
+ email: '123@yunzhi.club',
+ clazzId: randomNumber(10)
+ });
+
+ // 固定写法
+ getTestScheduler().flush();
+ fixture.autoDetectChanges();
+ });
+
it('理解map操作符', () => {
// 数据源发送数据1
const a = of(1) as Observable<number>;
```
在此单元测试代码的支持上,我们再也不必手动地填写这些数据了:
![image-20210414154202408](https://img.kancloud.cn/11/3b/113b3b3cdc5962ea48cf467b48cbdd08_2276x308.png)
这绝对是个提升生产力的好方法。好了,就到这里,休息一会。
| 名称 | 链接 |
| ----------------- | ------------------------------------------------------------ |
| HTMLElement | [https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) |
| HTMLButtonElement | [https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement) |
| 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step7.2.6.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.2.6.zip) |
- 序言
- 第一章 Hello World
- 1.1 环境安装
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教师管理
- 2.1 教师列表
- 2.1.1 初始化原型
- 2.1.2 组件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 请求后台数据
- 2.2.1 HttpClient
- 2.2.2 请求数据
- 2.2.3 模块与依赖注入
- 2.2.4 异步与回调函数
- 2.2.5 集成测试
- 2.2.6 本章小节
- 2.3 新增教师
- 2.3.1 组件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 对接后台
- 2.3.4 路由
- 2.4 编辑教师
- 2.4.1 组件初始化
- 2.4.2 获取路由参数
- 2.4.3 插值与模板表达式
- 2.4.4 初识泛型
- 2.4.5 更新教师
- 2.4.6 测试中的路由
- 2.5 删除教师
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome图标库
- 2.6.3 firefox
- 2.7 总结
- 第三章 用户登录
- 3.1 初识单元测试
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 着陆组件
- 3.5 @Output
- 3.6 TypeScript 类
- 3.7 浏览器缓存
- 3.8 总结
- 第四章 个人中心
- 4.1 原型
- 4.2 管道
- 4.3 对接后台
- 4.4 x-auth-token认证
- 4.5 拦截器
- 4.6 小结
- 第五章 系统菜单
- 5.1 延迟及测试
- 5.2 手动创建组件
- 5.3 隐藏测试信息
- 5.4 规划路由
- 5.5 定义菜单
- 5.6 注销
- 5.7 小结
- 第六章 班级管理
- 6.1 新增班级
- 6.1.1 组件初始化
- 6.1.2 MockApi 新建班级
- 6.1.3 ApiInterceptor
- 6.1.4 数据验证
- 6.1.5 教师选择列表
- 6.1.6 MockApi 教师列表
- 6.1.7 代码重构
- 6.1.8 小结
- 6.2 教师列表组件
- 6.2.1 初始化
- 6.2.2 响应式表单
- 6.2.3 getTestScheduler()
- 6.2.4 应用组件
- 6.2.5 小结
- 6.3 班级列表
- 6.3.1 原型设计
- 6.3.2 初始化分页
- 6.3.3 MockApi
- 6.3.4 静态分页
- 6.3.5 动态分页
- 6.3.6 @Input()
- 6.4 编辑班级
- 6.4.1 测试模块
- 6.4.2 响应式表单验证
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定义FormControl
- 6.4.6 代码重构
- 6.4.7 小结
- 6.5 删除班级
- 6.6 集成测试
- 6.6.1 惰性加载
- 6.6.2 API拦截器
- 6.6.3 路由与跳转
- 6.6.4 ngStyle
- 6.7 初识Service
- 6.7.1 catchError
- 6.7.2 单例服务
- 6.7.3 单元测试
- 6.8 小结
- 第七章 学生管理
- 7.1 班级列表组件
- 7.2 新增学生
- 7.2.1 exports
- 7.2.2 自定义验证器
- 7.2.3 异步验证器
- 7.2.4 再识DI
- 7.2.5 属性型指令
- 7.2.6 完成功能
- 7.2.7 小结
- 7.3 单元测试进阶
- 7.4 学生列表
- 7.4.1 JSON对象与对象
- 7.4.2 单元测试
- 7.4.3 分页模块
- 7.4.4 子组件测试
- 7.4.5 重构分页
- 7.5 删除学生
- 7.5.1 第三方dialog
- 7.5.2 批量删除
- 7.5.3 面向对象
- 7.6 集成测试
- 7.7 编辑学生
- 7.7.1 初始化
- 7.7.2 自定义provider
- 7.7.3 更新学生
- 7.7.4 集成测试
- 7.7.5 可订阅的路由参数
- 7.7.6 小结
- 7.8 总结
- 第八章 其它
- 8.1 打包构建
- 8.2 发布部署
- 第九章 总结