开发分页方法有两个方法,第一个是直接在当前的clazz列表组件开发,第二个是将分页专门的剥离出一个组件。虽然第二个方法更正综,但我们初期完成某个功能时,往往是先采用第一种,待使用第一种完成功能后,再使用第二种。 在此仍然采用step by step的敏捷开发模式,所谓的敏捷就是指把大的复杂的功能拆分成小的可衡量的功能模块。然后一步步完成各个功能模块,完成一点测试一点,直至最终功能完成。 ## 静态分页 我们先实现一个静态分页,即html中的页数是固定的,但点击页码却真实的触发C层的分页方法,重新按页数请求数据。 ```html +++ b/first-app/src/app/clazz/clazz.component.html @@ -32,10 +32,10 @@ <nav class="row justify-content-md-center"> <ul class="pagination col-md-auto"> - <li class="page-item disabled"><a class="page-link" href="#">上一页</a></li> - <li class="page-item active"><a class="page-link" href="#">1</a></li> - <li class="page-item"><a class="page-link" href="#">2</a></li> - <li class="page-item"><a class="page-link" href="#">3</a></li> - <li class="page-item"><a class="page-link" href="#">下一页</a></li> + <li class="page-item disabled"><span class="page-link">上一页</span></li> + <li class="page-item active"><span class="page-link" (click)="onPage(1)">1</span></li> + <li class="page-item"><span class="page-link" (click)="onPage(2)">2</span></li> + <li class="page-item"><span class="page-link" (click)="onPage(3)">3</span></li> + <li class="page-item"><span class="page-link">下一页</span></li> </ul> ``` 上述代码使用了`span`标签来替换`a`标签,加入了`onPage()`方法并对应传入了页码,在C层建立`onPage()`方法如下: ```typescript + onPage(page: number): void { + console.log(page); + } ``` 然后在V层点击相应的分页,查看控制台变化: ![image-20210330163243332](https://img.kancloud.cn/fb/16/fb16e6bd2c533699fc7a6a3ac2e49070_1666x318.png) 触发了C层的方法后, 我们便可以利用该分页的值去请求对应的后台数据了: ```typescript this.httpClient.get<Page<Clazz>>('/clazz/page?page=' + page.toString()) .subscribe(pageData => { this.pageData = pageData; console.log(pageData); }); ``` 在上述代码中,我们接收了带有`page`信息的请求地址,这样以来后台便可以接收到`page`的信息从而返回当前页码的数据了。但由于我们引用的`MockApi`天生存在一些缺陷的原因,导致MockApi无法匹配URL中的参数,所以此时点击分页时将得到一个如下错误: ![image-20210331082004280](https://img.kancloud.cn/29/0d/290dfa576d9accce87270ac5015b36b9_2108x130.png) > ​ MockApi是团队的一个开源项目,地址为:[https://github.com/yunzhiclub/ng](https://github.com/yunzhiclub/ng),希望能够得到有能力的小伙伴的帮助,使MockApi越来越好。 其实`MockApi`之所以没有匹配这种直接将参数放到URL的情况是因为:在使用HttpClient发起带有参数的请求时,正确的打卡姿势是使用`HttpParams`,比如我们想在请求中加入`page`参数,则需要如下使用: ```typescript +++ b/first-app/src/app/clazz/clazz.component.ts @@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core'; import {Page} from '../entity/page'; import {Clazz} from '../entity/clazz'; import {Teacher} from '../entity/teacher'; -import {HttpClient} from '@angular/common/http'; +import {HttpClient, HttpParams} from '@angular/common/http'; @Component({ selector: 'app-clazz', @@ -33,7 +33,8 @@ export class ClazzComponent implements OnInit { } onPage(page: number): void { - this.httpClient.get<Page<Clazz>>('/clazz/page?page=' + page.toString()) + const httpParams = new HttpParams().append('page', page.toString()); + this.httpClient.get<Page<Clazz>>('/clazz/page', {params: httpParams}) ``` 此时当我们再次点击分页按扭时,错误消失并且可以在控制台中发现打印的数据返回情况: ![image-20210331083022413](https://img.kancloud.cn/ac/17/ac17f628d29b07afc3beff9c32b1d20d_1454x134.png) 但组件中显示的班级列表,却没有任何变化。这是由于我们在当前的单元测试中使用了`fixture.detectChanges();`来手动控制了组件的渲染。这导致了C层中的数据即使发生了变化,由于没有启用自动检测变更机制,所以组件的V层也不会自动渲染。 所以我们得出的结论是:如果想借助`ng t`来查看一些功能,则需要在测试用例的最后一行启用测试夹具的自动检测变更机制: ```typescript +++ b/first-app/src/app/clazz/clazz.component.spec.ts @@ -36,5 +36,6 @@ describe('ClazzComponent', () => { expect(component).toBeTruthy(); getTestScheduler().flush(); fixture.detectChanges(); + fixture.autoDetectChanges(); }); }); ``` 此时,当我们点击分页按钮时,组件的内容也会随着发生变化,这与应用真实后台的效果一模一样。 **小BUG**:我们刚刚不小心写了一个小BUG。上个小节中的接口规范中,每几页是`0基`而非`1基`的,也就是说如果我们获取第1页的信息,则应该将`page`设置为0。为此我们变更一下V层的代码: ```html - <li class="page-item active"><span class="page-link" (click)="onPage(1)">1</span></li> - <li class="page-item"><span class="page-link" (click)="onPage(2)">2</span></li> - <li class="page-item"><span class="page-link" (click)="onPage(3)">3</span></li> + <li class="page-item active"><span class="page-link" (click)="onPage(0)">1</span></li> + <li class="page-item"><span class="page-link" (click)="onPage(1)">2</span></li> + <li class="page-item"><span class="page-link" (click)="onPage(2)">3</span></li> ``` 此时当我们当击第1页时,向C层传递的是0;点击第2页时,向C层传递的是1。 ## 加入每页大小 前面我们在初始化组件时,初始化了每页大小为3,而当前的模拟数据返回的每页大小为默认的20。参考在请求时加入`page`的代码,我们在请求中加入`size`: ```typescript onPage(page: number): void { - const httpParams = new HttpParams().append('page', page.toString()); + const httpParams = new HttpParams().append('page', page.toString()) + .append('size', this.size.toString()); this.httpClient.get<Page<Clazz>>('/clazz/page', {params: httpParams}) ``` 有了第几页、每页大小后,在当下进行测试,仍然发现返回了20条数据。这是由于我们在ClazzMockApi中的相关方法,并没有对`page`和`size`进行处理的原因。在ClazzMockApi中,我们是可以轻松的获取到请求参数的: ```typescript +++ b/first-app/src/app/mock-api/clazz.mock.api.ts @@ -2,6 +2,7 @@ import {ApiInjector, MockApiInterface, randomNumber, RequestOptions} from '@yunz import {Clazz} from '../entity/clazz'; import {Teacher} from '../entity/teacher'; import {Page} from '../entity/page'; +import {HttpParams} from '@angular/common/http'; /** * 班级模拟API @@ -37,7 +38,9 @@ export class ClazzMockApi implements MockApiInterface { { method: 'GET', url: '/clazz/page', - result: () => { + result: (urlMatches: string[], options: RequestOptions) => { 👈 + const httpParams = options.params as① HttpParams; + console.log(httpParams.get('page'), httpParams.get('size')); const size = 20; const clazzes = new Array<Clazz>(); for (let i = 0; i < size; i++) { ``` result属性设置为回调函数的方法我们在前面已然接触过,该方法支持0个,1个或2个参数。MockApi在模拟返回数据时,将使用相应的值做为参数来调用result属性对应的方法。其中第一个参数`urlMatches`为请求URL应用正则表式的匹配结果,类型为字符串数组;第二个参数`options`为请求的具体信息,包括请求主体,请求`header`,以及我们本次使用的请求参数`params`。 ① `RequestOptions`中的`params`属性有多个类型,其实包括了我们使用的`HttpParams` ,在此使用`as`指定其具体类型。`as`的使用情景为:我们确认数据的确切类型时。由于该`params`实际上为C层中我们使用`get`方法传入的`HttpParams`类型,所以我们在此使用了`as HttpParams`来指定以规避`typescript`的一些语法提示。 此时再次点击相应的分页,则会在控制台中打印具体的分页信息: ![image-20210331091626114](https://img.kancloud.cn/e7/e0/e7e02a6de6c85358f7c39553853c2f1d_1446x128.png) 第一行打印了两个`null`,就是由于在组件的`ngOnInit()`方法中同样调用了后台的分页,在该方法中未指定`page`及`size`,所以在执行`httpParams.get('page')`方法时返回了`null`。 ## 完善模拟数据 完善的模拟数据能够友好的支持组件开发,而且有些代码可以只造一个轮子,在开发的时候是非常具有性价比的。在模拟Api返回数据前,是完全可以根据传入的`page`、`size` 值来定制返回数据的: ```typescript +++ b/first-app/src/app/mock-api/clazz.mock.api.ts result: (urlMatches: string[], options: RequestOptions) => { + // 初始化两个默认值 + let page = 0; + let size = 20; + const httpParams = options.params as HttpParams; - console.log(httpParams.get('page'), httpParams.get('size')); - const size = 20; + if (httpParams.has('page')) { + // 在这里我们使用了`has()`方法来判断是否存在该字段。 + // 所以在此执行httpParams.get('page')必然返回一个非null的值 + // 结合httpParams.get('page')返回值类型规定为null | string + // null | string去了一个null,则返回值类型必然为string,所以在使用as指定 + // + 的目的是将string类型转换为number + page = +(httpParams.get('page') as string); + } + + if (httpParams.get('size')) { + size = +(httpParams.get('size') as string); + } + @@ -55,9 +70,9 @@ export class ClazzMockApi implements MockApiInterface { } return new Page<Clazz>({ content: clazzes, - number: 2, + number: page, size, - numberOfElements: 20 + numberOfElements: size * 10 }); } } ``` 此时当未接收到`page`或`size`时将使用默认值设置`page`,`size`值,接收到`page`或`size`时将使用接收到的值。在返回值中,加入了当前页、每页大小信息,且当数据的总数量控制在10页。此时,在组件初始化时将返回第1页的数据,每页大小20条;点击分页时,分页大小为3条,对应返回当前页码的数据。比如点击第2页: ![image-20210331093356204](https://img.kancloud.cn/bc/17/bc172b1c418b3243a22789b55f42b80d_1868x572.png) 控制台打印的返回数据信息,显示当前页为第2页。 ![image-20210331093346647](https://img.kancloud.cn/05/42/0542a1db37622f1869742a7b76677789_804x238.png) 最后为了保持组件风格统一,我们在组件初始化时当每页大小设置为3: ```typescript +++ b/first-app/src/app/clazz/clazz.component.ts @@ -28,7 +28,8 @@ export class ClazzComponent implements OnInit { } ngOnInit(): void { - this.httpClient.get<Page<Clazz>>('/clazz/page') + this.httpClient.get<Page<Clazz>>('/clazz/page', + {params: new HttpParams().append('size', this.size.toString())}) .subscribe(pageData => this.pageData = pageData); } ``` 对应的后台请求完成后,接下来实现分页的点亮效果:比如当前为第1页,则第1页是选中状态: ![image-20210331093808677](https://img.kancloud.cn/f1/0b/f10b0ccfcd7b0a452848bbac1b6fba67_317x52.png) 如果是第2页,则2是选中状态,以此累推。 ## 点亮效果 观察html我们得知点亮效果是由样式控制的: ```typescript <li class="page-item active👈"><span class="page-link" (click)="onPage(0)">1</span></li> ``` 则我们可以使用以下思路来完成该功能: - 在C层记录当前是第几页 - 在V层的分页按钮上做判断,如果其分页值等于当前页则加入`active`样式 ### 记录当前页 记录当前页比较简单,仅仅需要在`onPage()`方法中对`page`设置值即可: ```typescript +++ b/first-app/src/app/clazz/clazz.component.ts @@ -34,10 +34,14 @@ export class ClazzComponent implements OnInit { } onPage(page: number): void { + // 在请求数据之前设置当前页 + this.page = page; 👈 const httpParams = new HttpParams().append('page', page.toString()) .append('size', this.size.toString()); this.httpClient.get<Page<Clazz>>('/clazz/page', {params: httpParams}) .subscribe(pageData => { + // 在请求数据之后设置当前页 + this.page = page; 👈 this.pageData = pageData; console.log(pageData); }); ``` 在设置当前页时有两种选择:在获取到后台返回数据之前或之后。我们在此使用第二种方案:在获取到后台返回数据之后,为此删除在请求数据之前设置当前页的代码: ```typescript onPage(page: number): void { - // 在请求数据之前设置当前页 - this.page = page; const httpParams = new HttpParams().append('page', page.toString()) .append('size', this.size.toString()); ``` ### ngClass 使用我们前面学习过的`ngIf`指令,可以非常轻松的完成:根据当前页情况选择是否添加`active`样式功能: ```html <ul class="pagination col-md-auto"> <li class="page-item disabled"><span class="page-link">上一页</span></li> <li *ngIf="page !== 0" class="page-item"><span class="page-link" (click)="onPage(0)">1</span></li> <li *ngIf="page === 0" class="page-item active"><span class="page-link" (click)="onPage(0)">1</span></li> <li *ngIf="page !== 1" class="page-item"><span class="page-link" (click)="onPage(1)">2</span></li> <li *ngIf="page === 1" class="page-item active"><span class="page-link" (click)="onPage(1)">2</span></li> <li *ngIf="page !== 2" class="page-item"><span class="page-link" (click)="onPage(2)">3</span></li> <li *ngIf="page === 2" class="page-item active"><span class="page-link" (click)="onPage(2)">3</span></li> <li class="page-item"><span class="page-link">下一页</span></li> </ul> ``` ![image-20210331095454709](https://img.kancloud.cn/f4/ce/f4ce05432ce40144776a8f464c60d6fe_832x305.png) 上述方法虽然可行,但冗余的代码有些过多。Angular当然可以更优雅的处理此种情况---- `[ngClass]`。`[ngClazz]`能够实现根据某种条件来选择是否添加某样式的功能,比如当前情况可以使用`[ngClass]`改写如下: ```html <ul class="pagination col-md-auto"> <li class="page-item disabled"><span class="page-link">上一页</span></li> <li [ngClass]="{active: page === 0}"👈 class="page-item"><span class="page-link" (click)="onPage(0)">1</span></li> <li [ngClass]="{active: page === 1}"👈 class="page-item"><span class="page-link" (click)="onPage(1)">2</span></li> <li [ngClass]="{active: page === 2}"👈 class="page-item"><span class="page-link" (click)="onPage(2)">3</span></li> <li class="page-item"><span class="page-link">下一页</span></li> </ul> ``` `[ngClass]`接收一个对象,将对象上的某个属性值为`true`时,将使用该属性名做为class值,添加其所在的元素(宿主元素)的`class`属性上。所以查看元素观察生成的`html`代码时,会看到如下代码: ![image-20210331100102895](https://img.kancloud.cn/1c/3d/1c3dcf39bf8299dddbf87f74c96adc65_1466x258.png) 同样,我们还可以使用`[ngClass]`来动态的为**上一页**、**下一页**添加相应的`disabled` 样式。 在下个小节中,我们将共同学习如何生成一个动态的分页,从而替换当前手写的1,2,3页。 ## 本节作业 1. `onPage()`方法中使用`this.page = page`更新了当前页,请尝试将代码移至请求数据之前,然后再点击分页,观察两种设置方式的不同。 2. 使用`[ngClass]`为**上一页**、**下一页**添加相应的`disabled` 样式。 | 名称 | 链接 | | ------------------ | ------------------------------------------------------------ | | 配置 HTTP URL 参数 | [https://angular.cn/guide/http#configuring-http-url-parameters](https://angular.cn/guide/http#configuring-http-url-parameters) | | NgClass | [https://angular.cn/guide/built-in-directives#ngclass](https://angular.cn/guide/built-in-directives#ngclass) | | 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.3.4.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.3.4.zip) |