本节我们展示学生编辑组件的开发在步骤及每步的详细代码。 ## 初始化 初始化包括组件初始化以及MockApi初始化: ### 组件初始化 来到student模块,并使用angularCli进行完成的快速的初始化: ```bash panjie@panjies-iMac student % pwd /Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app/student panjie@panjies-iMac student % ng g c edit Your global Angular CLI version (11.2.13) is greater than your local version (11.0.7). The local Angular CLI version is used. To disable this warning use "ng config -g cli.warnings.versionMismatch false". CREATE src/app/student/edit/edit.component.css (0 bytes) CREATE src/app/student/edit/edit.component.html (19 bytes) CREATE src/app/student/edit/edit.component.spec.ts (612 bytes) CREATE src/app/student/edit/edit.component.ts (267 bytes) UPDATE src/app/student/student.module.ts (794 bytes) ``` ### MockApi初始化 根据API信息,初始化MockApi信息: ```typescript +++ b/first-app/src/app/mock-api/student.mock.api.ts @@ -78,6 +78,49 @@ export class StudentMockApi implements MockApiInterface { const ids = httpParams.getAll('ids'); Assert.isArray(ids, '未接收到ids'); }) + }, { + method: 'GET', + url: '/student/(\\d+)', + result: (urlMatches: string[]) => { + const id = +urlMatches[1]; + return { + id, + name: randomString('姓名'), + number: randomString('学号'), + phone: (139000000000 + randomNumber(99999999)).toString(), + email: randomString('前缀') + '@yunzhi.club', + clazz: { + id: randomNumber(), + name: randomString('班级名称'), + teacher: { + id: randomNumber(), + name: randomString('教师名称') + } as Teacher + } as Clazz + } as Student; + } + }, { + method: 'PUT', + url: '/student/(\\d+)', + result: (urlMatches: string[], options: RequestOptions) => { + const id = +urlMatches[1]; + const student = options.body as Student; + return { + id, + name: student.name, + number: randomString('学号'), + phone: student.phone, + email: student.email, + clazz: { + id: student.clazz.id, + name: randomString('班级名称'), + teacher: { + id: randomNumber(), + name: randomString('教师名称') + } as Teacher + } as Clazz + } as Student; + } } ]; } ``` ### M层 根据Api信息建立补充M层方法 ---- `getById`: ```bash +++ b/first-app/src/app/service/student.service.ts @@ -37,6 +37,15 @@ export class StudentService { return this.httpClient.delete<void>(url); } + + /** + * 获取学生 + * @param id 学生ID + */ + getById(id: number): Observable<Student> { + return this.httpClient.get<Student>('/student/' + id.toString()); + } + /** ``` `update`: ```typescript +++ b/first-app/src/app/service/student.service.ts /** * 更新学生 * @param id 学生ID * @param student 学生信息 */ update(id: number, student: { name: string, phone: string, email: string, clazz: { id: number } }): Observable<Student> { return this.httpClient.put<Student>(`/student/${id}`, student); } ``` ### 单元测试 对`getById()`方法的测试: ```typescript +++ b/first-app/src/app/service/student.service.spec.ts @@ -4,6 +4,7 @@ import {StudentService} from './student.service'; import {MockApiTestingModule} from '../mock-api/mock-api-testing.module'; import {HttpClient} from '@angular/common/http'; import {getTestScheduler} from 'jasmine-marbles'; +import {randomNumber} from '@yunzhi/ng-mock-api'; describe('StudentService', () => { let service: StudentService; @@ -93,5 +94,21 @@ describe('StudentService', () => { console.log(jsonTest.name); // jsonTest.sayHello(); }); + + fit('getById', () => { + // 返回前面已经请求的数据(如有),避免产生数据污染。 + getTestScheduler().flush(); + + const id = randomNumber(); + let called = false; + service.getById(id).subscribe(student => { + called = true; + expect(student).toBeTruthy(); + }); + expect(called).toBeFalse(); + + getTestScheduler().flush(); + expect(called).toBeTrue(); + }); }); ``` 测试通过: ![image-20210609142349392](https://img.kancloud.cn/6d/4f/6d4f6a02634d77b7d5c7903c34450882_1050x290.png) 对`update`方法的测试: ```typescript +++ b/first-app/src/app/service/student.service.spec.ts @@ -5,6 +5,7 @@ import {MockApiTestingModule} from '../mock-api/mock-api-testing.module'; import {HttpClient} from '@angular/common/http'; import {getTestScheduler} from 'jasmine-marbles'; import {randomNumber} from '@yunzhi/ng-mock-api'; +import {randomString} from '@yunzhi/ng-mock-api/testing'; describe('StudentService', () => { let service: StudentService; @@ -110,5 +111,25 @@ describe('StudentService', () => { getTestScheduler().flush(); expect(called).toBeTrue(); }); + + fit('update', () => { + // 返回前面已经请求的数据(如有),避免产生数据污染。 + getTestScheduler().flush(); + + const id = randomNumber(); + let called = false; + service.update(id, { + name: randomString(), + email: randomString(), + phone: randomString(), + clazz: {id: randomNumber()}}).subscribe(student => { + called = true; + expect(student).toBeTruthy(); + }); + expect(called).toBeFalse(); + + getTestScheduler().flush(); + expect(called).toBeTrue(); + }); }); ``` ![image-20210609142817630](https://img.kancloud.cn/d6/13/d6138980324e789f916cca882eddeb0b_1230x310.png) ## 原型开发 更新学生与新增学生大同小异,不同的是更新学生时不能够对学生的学号进行更新,原型代码如下: ```html <form class="container-sm" (ngSubmit)="onSubmit()" [formGroup]="formGroup"> <div class="mb-3 row"> <label class="col-sm-2 col-form-label">名称</label> <div class="col-sm-10"> <input type="text" class="form-control" formControlName="name"> <small class="text-danger" *ngIf="formGroup.get('name')!.invalid"> 名称不能为空 </small> </div> </div> <div class="mb-3 row"> <label class="col-sm-2 col-form-label">学号</label> <div class="col-sm-10"> <input type="text" readonly class="form-control-plaintext" formControlName="number"> </div> </div> <div class="mb-3 row"> <label class="col-sm-2 col-form-label">手机号</label> <div class="col-sm-10"> <input type="text" class="form-control" formControlName="phone"> <small class="text-danger" *ngIf="formGroup.get('phone')!.invalid"> 手机号格式不正确 </small> </div> </div> <div class="mb-3 row"> <label class="col-sm-2 col-form-label">邮箱</label> <div class="col-sm-10"> <input type="text" class="form-control" formControlName="email"> </div> </div> <div class="mb-3 row"> <label class="col-sm-2 col-form-label">班级</label> <div class="col-sm-10"> <app-clazz-select formControlName="clazzId"></app-clazz-select> <small class="text-danger" *ngIf="formGroup.get('clazzId')!.invalid"> 必须选择班级 </small> </div> </div> <div class="mb-3 row"> <div class="col-sm-10 offset-2"> <button appLoading class="btn btn-primary" [disabled]="formGroup.invalid || formGroup.pending"> <i class="fa fa-save"></i>保存 </button> </div> </div> </form> ``` 根据原型中使用到的组件属性及方法,初始化组件如下: ```typescript +++ b/first-app/src/app/student/edit/edit.component.ts @@ -1,4 +1,7 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; +import {FormControl, FormGroup, Validators} from '@angular/forms'; +import {YzValidators} from '../../yz-validators'; +import {YzAsyncValidators} from '../../yz-async-validators'; @Component({ selector: 'app-edit', @@ -6,10 +9,22 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./edit.component.css'] }) export class EditComponent implements OnInit { + formGroup: FormGroup; - constructor() { } + constructor(private yzAsyncValidators: YzAsyncValidators) { + this.formGroup = new FormGroup({ + name: new FormControl('', Validators.required), + number: new FormControl('', Validators.required, yzAsyncValidators.numberNotExist()), + phone: new FormControl('', YzValidators.phone), + email: new FormControl(), + clazzId: new FormControl(null, Validators.required) + }); + } ngOnInit(): void { } + onSubmit(): void { + + } } ``` ### 单元测试 在组件中注入的`YzAsyncValidators`异步验证器依赖于后台API,在测试过程中,我们使用一个假的后台来提供后台API: ```typescript +++ b/first-app/src/app/student/edit/edit.component.spec.ts @@ -1,6 +1,8 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {EditComponent} from './edit.component'; +import {MockApiTestingModule} from '../../mock-api/mock-api-testing.module'; +import {getTestScheduler} from 'jasmine-marbles'; describe('EditComponent', () => { let component: EditComponent; @@ -8,6 +10,7 @@ describe('EditComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ + imports: [MockApiTestingModule], declarations: [EditComponent] }) .compileComponents(); @@ -22,4 +25,15 @@ describe('EditComponent', () => { fit('should create', () => { expect(component).toBeTruthy(); }); + + /** + * 每个测试用例执行结束后,都执行一次此方法 + */ + afterEach(() => { + // 发送尚未发送的数据,可以避免两次相近执行的单元测试不互相影响 + getTestScheduler().flush(); + + // 统一调用自动检测功能 + fixture.autoDetectChanges(); + }); }); ``` ![image-20210609144053128](https://img.kancloud.cn/cf/4a/cf4a08fe72b7f28ffea5f87a49c96ed0_2430x844.png) 至此,整体的初始化工作宣告结束,下一节开始完成功能实现。 | 链接 | 名称 | | ------------------------------------------------------------ | -------- | | [https://github.com/mengyunzhi/angular11-guild/archive/step7.7.1.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.7.1.zip) | 本节源码 |