前面的章节中我们都是直接给出了后台的Api情况。其实如果团队中都是全栈工程师的话,后台Api的定义往往是在前台开发过程中被逐步规定的。后台的开发会在前台基本完成,DEMO能够跑通的情况下进行。我们本节也模拟这个过程,在开发过程中逐步的完成后台的模拟接口的设定。 ## Service 来到`student.servicer.ts`增加`pageOfCurrentTeacher`方法用于查询当前登录教师管理的学生分页信息: ```typescript +++ b/first-app/src/app/service/student.service.ts @@ -26,4 +27,15 @@ export class StudentService { // 将预请求信息返回 return this.httpClient.post<Student>('/student', student); } + + /** + * 当前登录用户的分页信息 + * @param data 分页信息 + */ + pageOfCurrentTeacher(①{②page = 0, ②size = 20}③: { page④?: number, size④?: number }): Observable<Page<Student>> { + const httpParams = new HttpParams() + .append('page', page.toString()) + .append('size', size.toString()); + return this.httpClient.get<Page<Student>>('/student/pageOfCurrentTeacher', {params: httpParams}); + } } ``` 定义上述方法时我们使用了几个小技巧: - ① 不定义参数名,直接在参数中使用`{}` - ② 使用`xxx=yyy`的方法设置默认值 - ③ 使用`:`来指明参数的类型 - ④ 使用`?`标记该参数可选 在以上技巧的支持下,实现了多参数默认值的设置,同时由于该参数的类型为`{}`,所以上述方法支持以下调用形式: ```typescript service.pageOfCurrentTeacher({}); service.pageOfCurrentTeacher({page: 1}); service.pageOfCurrentTeacher({page: 1, size: 2}); service.pageOfCurrentTeacher({size: 2}); service.pageOfCurrentTeacher({size: 2, page: 1}); ``` ### JSON对象与对象 如果你仔细观察前面我们写过的MockApi的话,应该会发现一个现象:我们特意的规避了在Mock中使用`new`关键字,而是使用了`as`关键字。比如我们在返回学生时使用如下代码: ```typescript result: { name: 'xxx' } as Student; ``` 而不是: ``` result: new Student({name: 'xxx'}) ``` 我们为什么在这么做,上述两种写法本质上又有何区别呢? 这是由于真实的后台返回返回的数据就是`as`的形式,表示:把返回值**看做是**某个类型,这正好是`as`所要表达的意思。而如果在模拟的后台API中使用`new`,则表示:返回值**是**某个类型。这完全是两个不同的概念,**看做是**则意味着可能是,也可能不是。 在交流中,我们有时也把这种**看做是**的类型称为**JSON对象**,而把使用`new`关键字生成的对象称为**对象**,为此随意找个单元测试简单演示一下来查看下二者的区别: ```typescript fit('JSON对象与对象', () => { class A { a: string; constructor(a: string) { this.a = a; } } const a1 = new A('123'); console.log('对象;', a1); const a2 = {a: '123'} as A; console.log('JSON对象:', a2); }); ``` 控制台打印信息如下: ![image-20210416092338084](https://img.kancloud.cn/c0/81/c08169864bf6515882ce8202f7a6fa04_1082x442.png) 可见:对象a1原型链对应了类A;而JSON对象虽然通过`as`将类型声明为`A`,但其本质(原型链)仍然是`object`。当前类A仅有一个`a`属性,并不存在其它的方法。在未为类A定义方法前,上述两种定义方法并不会有什么不同,但如果类A类中定义一些方法,就不一样了: ```typescript fit('JSON对象与对象', () => { class A { a: string; constructor(a: string) { this.a = a; } getA(): string { return this.a; } } const a1 = new A('123'); console.log('对象;', a1); console.log(a1.getA()); const a2 = {a: '123'} as A; console.log('JSON对象:', a2); 👉 console.log(a2.getA()); }); ``` 👉 由于a2实际的类型并不是A,仅仅是当做A来看待,所以在尝试调用`getA()`方法时将会触发一个异常。 ![image-20210416092930971](https://img.kancloud.cn/c0/b3/c0b32e1bb39baba0b796852093a09ca7_1314x110.png) 查看控制台信息也的确如此,第二个使用`as`方法声明的JSON对象并不存在`getA()`方法: ![image-20210416101000081](https://img.kancloud.cn/b9/55/b955441ff2a51d37a845d8260c64da46_1880x622.png) 实际上由于JSON对象的本质是个`object`,在javascript中,任意对象的本质都是`object`,这个`object`是`javascript`的最基础的类。 ![image-20210416101204463](https://img.kancloud.cn/1a/61/1a6183aa7478dc010f34d6d6f4a94204_1006x344.png) 如上图示对象`a1`由类A实例化而来,而A的父类(原型)同样也是`object`。 通过上述学习我们发现使用`new`关键字来实例化对象与使用`as`关键字来将某对象**看做**是某类的实例化,其实是有些本质区别的。而`this.httpClient.get<T>`中的`T`的本质则是一个`as`,也就是说`httpClient`会将请求的数据标识为我们传入`T`,而不是根据`T`的的类型返回一`T`的实例。 所以在MockApi中我们也应该用`as`关键字将JSON对象**标识**为返回值类型,而不是用`new`关键字真正返回某个类的实例。 最后我们加入对异常的断言,以使上述单元测试顺利通过: ```typescript +++ b/first-app/src/app/yz-validators.spec.ts @@ -61,6 +61,12 @@ describe('YzValidators', () => { const a2 = {a: '123'} as A; console.log('JSON对象:', a2); - console.log(a2.getA()); + let catchException = false; + try { + console.log(a2.getA()); + } catch (e) { + catchException = true; + } + expect(catchException).toBeTrue(); }); }); ``` 上述代码展示了测试异常的一种常规方法:在调用异常的代码前定义个boolean类型的局部变量并设置为fasle,然后在`try`中执行异常代码,在`catch`中将boolean值设置为true。如果代码如我们期望的一样执行,则boolean类型的值必然为true,此时断言通过。否则断言不通过。所以断言是否通过可以做为验证执行某些代码是否发生了异常的依据。 > [info] 初次接触前台这个即简单又深奥的领域时,搞不太清JSON对象与对象的异同是很正常的事情。所以看完上面的讲解仍然云里雾里的话,这并不是你的错,而是我们在这里为大家讲解的篇幅太少了。在教程中及以后的生产开发中,我们只需要记住:在MocaApi中不能够使用`new`关键字来返回数据便可以了。 ## MockApi 我们继续使用`as`关键字来返回一个`Page`类型,打开`student.mock.api.ts`添加如下API: ```typescript { method: 'GET', url: '/student/pageOfCurrentTeacher', result: (urlMatches: string[], options: RequestOptions) => { const httpParams = options.params as HttpParams; const page = +(httpParams.get('page') as string); const size = +(httpParams.get('size') as string); Assert.isNumber(page, size, 'page size must be number'); const students = [] as Array<Student>; for (let i = 0; i < size; i++) { students.push({ id: i + 1, name: randomString('姓名'), number: randomNumber(10000).toString(), phone: '13900001111', clazz: { name: randomString('班级名称'), teacher: { name: randomString('教师名称') } as Teacher } as Clazz } as Student); } return { content: students, number: page, size, totalPages: (page + 1 + randomNumber(10)) * size } as Page<Student>; } } ``` ## 测试 确保某一段代码是否正常运行的最简单的方法是写个单元测试,这比用鼠标点一点,键盘输入一下要可靠、稳定的多。而且从长久来看也省时的多。当Service以及MockApi都准备好以后,我们启用单元测试来测试一下,以保证`Service`发起请求后MockApi可以做出响应: ```typescript +++ b/first-app/src/app/service/student.service.spec.ts @@ -3,6 +3,7 @@ import {TestBed} from '@angular/core/testing'; 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'; describe('StudentService', () => { let service: StudentService; @@ -24,4 +25,19 @@ describe('StudentService', () => { .subscribe(success => console.log('success', success), error => console.log('error', error)); }); + + fit('pageOfCurrentTeacher', () => { + let called = false; + service.pageOfCurrentTeacher({page: 1, size: 2}) + .subscribe(data => { + // 当called为true时,说明接收到了数据 + called = true; + expect(data.number).toBe(1); + expect(data.size).toBe(2); + }); + + // 手动发送数据并断言已成功接收 + getTestScheduler().flush(); + expect(called).toBeTrue(); + }); }); ``` 测试通过。 ![image-20210416161337997](https://img.kancloud.cn/00/ad/00ad814bec1a6898a457cb581ef30551_790x102.png) 有了测试保证的Service,接下来完成组件C层初始化的对接。 ## C层 在C层的`ngOnInit()`方法中增加后台请求的代码。虽然我们可以将组件初始化的代码直接写在构造函数中,但Angular官方并不建议我们这样做,之所以这样Angular给出的答案是:这样会更高效。 ```typescript +++ b/first-app/src/app/student/student.component.ts @@ -1,6 +1,7 @@ import {Component, OnInit} from '@angular/core'; import {Page} from '../entity/page'; import {Student} from '../entity/student'; +import {StudentService} from '../service/student.service'; @Component({ selector: 'app-student', @@ -9,11 +10,17 @@ import {Student} from '../entity/student'; }) export class StudentComponent implements OnInit { pageData = {} as Page<Student>; + page = 0; + size = 20; - constructor() { + constructor(private studentService: StudentService) { } ngOnInit(): void { + this.studentService.pageOfCurrentTeacher({ + page: this.page, + size: this.size + }).subscribe(data => this.pageData = data); } onDelete(index: number, id: number): void { ``` ## 测试 测试大家已经轻车熟路了: ```typescript +++ b/first-app/src/app/student/student.component.spec.ts @@ -2,6 +2,8 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {StudentComponent} from './student.component'; import {RouterTestingModule} from '@angular/router/testing'; +import {getTestScheduler} from 'jasmine-marbles'; +import {MockApiTestingModule} from '../mock-api/mock-api-testing.module'; describe('StudentComponent', () => { let component: StudentComponent; @@ -11,7 +13,8 @@ describe('StudentComponent', () => { await TestBed.configureTestingModule({ declarations: [StudentComponent], imports: [ - RouterTestingModule + RouterTestingModule, + MockApiTestingModule ] }) .compileComponents(); @@ -26,4 +29,9 @@ describe('StudentComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + fit('onInit', () => { + getTestScheduler().flush(); + fixture.autoDetectChanges(); + }); }); ``` ![image-20210416162431130](https://img.kancloud.cn/33/7c/337cf663142a2b91550e1c8fd2292d7c_1626x438.png) | 名称 | 链接 | | -------- | ------------------------------------------------------------ | | 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step7.4.1.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.4.1.zip) |