当所有的盲点都被消除后,最后的功能完成已然成为了最很简单的一环。 ## 服务与实体 在生产项目中,往往会使用服务来完成与后台的交互工作,这在组件需要处理一些逻辑功能,或是需要与其它的服务进行交互时特别重要。 为此首先来到`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) |