分页是一个web项目的必备功能。本节让我们看看一个真实分页数据到底长啥样? 当前后台提供了一个不需要登录认证的教师分页接口,具体信息如下: ```bash GET /teacher/page ``` | **类型Type** | **名称Name** | **描述Description** | 必填 | **类型Schema** | 默认值 | | :------------ | :----------- | :----------------------- | ---- | :----------------------------------------------- | --------- | | Param请求参数 | `page` | 第几页 | 否 | `number` | 0 | | Param请求参数 | `size` | 每页大小 | 否 | `number` | 20 | | Param请求参数 | sort | 排序字段及方式(支持多个) | 否 | `string`例:`sort=name,desc` | `id,desc` | | Response响应 | | Status Code: 200 | | `{content: Teacher[], 其它与分页排序相关的信息}` | | 如上,该接口接收3个请求参数 ,分别为请求的当前页page,每页大小size,以及排序sort。请求成功时将返回状态码200,返回数据格式为对象。我们使用浏览器直接访问当前地址,看看返回的分页数据到底长什么样子: ![image-20210330092017951](https://img.kancloud.cn/62/e1/62e1211553541fac875d83d73d92f8c8_922x151.png) 这样的结果看起来乱糟糟的,那就打开控制台后找到网络选项卡,刷新后再看看吧: ![image-20210330092111020](https://img.kancloud.cn/11/14/11141ea298afb0cc3289d07971191508_1906x426.png) 我们发现该对象的`content`属性存放着返回的主体内容,即当前页对应的教师数组,以外还有很多其它的属性: ```json { "content": Teacher[], "pageable": { "sort": { "sorted":true, "unsorted":false, "empty":false }, "offset":0, "pageNumber":0, "pageSize":10, "unpaged":false, "paged":true }, "last":true, 👈 "totalPages":1, 👈 "totalElements":2, 👈 "size":10, "number":0, 👈 "numberOfElements":2, 👈 "sort": { "sorted":true, "unsorted":false, "empty":false }, "first":true, 👈 "empty":false } ``` ![image-20210329174345386](https://img.kancloud.cn/54/34/54344cf6330df3db3b636fde8b73757f_746x128.png) 参考上节的分页原型,我们在此仅将需要使用到的字段使用👈进行了标注,各个字段解释如下: - `last` 当前页是否为最后一页 - `totalPages` 共几页 - `number`当前为第几页(由0开始) - `numberOfElements` 数据总条数 - `first` 当前页是否为第一页 至于其它的字段的含义都不难,大概猜一猜吧。 除了返回值外,接口还说自己是支持`page`,`size`,`sort`做为分页查询,索性我们再多测试几个: 第1页,每页大小为1,则访问:[http://angular.api.codedemo.club:81/teacher/page?page=0&size=1](http://angular.api.codedemo.club:81/teacher/page?page=0&size=1)或[http://angular.api.codedemo.club:81/teacher/page?size=1&page=0](http://angular.api.codedemo.club:81/teacher/page?size=1&page=0) 加入按id正向排序,则访问:[http://angular.api.codedemo.club:81/teacher/page?page=0&sort=id,asc&size=1](http://angular.api.codedemo.club:81/teacher/page?page=0&sort=id,asc&size=1) ## 分页类 前面我们为了开发的便利性新建过了教师类及班级类,这使得我们可以充分的发挥出TypeSciprt强类型的优势,减少拼写错误的同时,在IDE的帮助下还能够在开发时自动填充对象的属性。 在此我们后台返回的分页信息以及我们刚刚确认所需要的字段信息,新建分页类Page。来到`src/app/entity`文件夹,使用`ng g class page`自动生成: ```bash panjie@panjie-de-Mac-Pro entity % pwd /Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app/entity panjie@panjie-de-Mac-Pro entity % ng g class page Your global Angular CLI version (11.2.6) 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/entity/page.spec.ts (146 bytes) CREATE src/app/entity/page.ts (22 bytes) ``` 接着在其中初始化如下字段: ```typescript /** * 分页. * @author 河北工业大学梦云智开发团队 */ export class Page { content: []; last: boolean; number: number; size: number; numberOfElements: number; first: boolean; } ``` ### 添加泛型 由于分页中的`content`数组中元素的类型是不确定的,比如在教师分页时`content`中的元素类型为`Teacher`,而在班级分页时`content`中的元素类型为`Clazz`。所以此时我们需要一个**泛型**来表示`content`中的数组元素类型需要根据使用时的情况确定。 ```typescript +++ b/first-app/src/app/entity/page.ts @@ -2,8 +2,8 @@ * 分页. * @author 河北工业大学梦云智开发团队 */ -export class Page { - content: []; +export class Page<T> { + content: T[]; last: boolean; number: number; size: number; ``` 我们在`Page`后面增加了`<T>`以表示该类中有些属性的类型需要在使用时指定,我们把这个`T`应该用在`content`字段上。这样一来,便达到了使用`const page = new Page<Teacher>()`时则将`content`的类型设置为`Teacher[]`;而当使用`const page = new Page<Clazz>()`时则将`content`的类型设置为`Clazz[]`的目的。 ### 构造函数 最后新建构造函数,并在其中对其属性完成初始化操作: ```typescript constructor(data①: { content: T[], ② last?: boolean, ③ number: number, ② size: number, ② numberOfElements: number, ② first?: boolean ③ }) { this.content = data.content; this.number = data.number; this.size = data.size; this.numberOfElements = data.numberOfElements; if (data.last !== undefined) { this.last = data.last; } else { this.last = (this.number + 1) * this.size >= this.numberOfElements ? true : false; ④ } if (data.first !== undefined) { this.first = data.first; } else { this.first = this.number === 0 ? true : false; ⑤ } } ``` 需要注意的时,在构造函数中我们使用了一些小技巧: - ① 我们并没有为参数`data`设置默认值,这是由于我们认为在使用`Page`时必须在设置参数`data`的值。 - ② 我们为`data`规定了几个必填属性,因为我们认为这些属性是必然设置的,否则`Page`将无法正常工作。 - ③ 我们为`data`规定了几个选填属性,因为我们认为这些属性可以不设置,即使没有设置,我们的`Page`仍然可以正常工作。 - ④ 当构造函数中未传入`last`字段时,可能通过计算当前页、每页大小、总条数三者间的关系来计算出当前是否为最后一页。 - ⑤ 当构造函数中未传入`first`字段时,可以判断当前页码是否为0来计算出当前是否为第一页。 我们比较容易犯两个小毛病,一个毛病是懒,第二个是自信。比如我们刚刚在构造函数中写了一些小在逻辑,但该逻辑就真的一点也没有问题吗?这时候就需要克服一下懒和自信的毛病,写一些代码测试一下: ```typescript +++ b/first-app/src/app/entity/page.spec.ts fit('should create an instance', () => { // 不加入last, first初始化 let page = new Page({ number: 2, size: 20, numberOfElements: 200, content: [] }); expect(page).toBeTruthy(); expect(page.first).toBeFalse(); expect(page.last).toBeFalse(); // 第1页,首页 page = new Page({ number: 0, size: 20, numberOfElements: 200, content: [] }); expect(page.first).toBeTrue(); expect(page.last).toBeFalse(); // 共41条数据,当前第3页,每页20条,所以当前页为尾页 page = new Page({ number: 2, size: 20, numberOfElements: 41, content: [] }); expect(page.first).toBeFalse(); expect(page.last).toBeTrue(); }); ``` ![image-20210330101908501](https://img.kancloud.cn/3a/c3/3ac368cce5f22769c4e0ef1025bd9b96_626x118.png) ## 初始化C层 接下来我们使用刚刚建立的`Page`类来初始化C层: ```typescript +++ b/first-app/src/app/clazz/clazz.component.ts export class ClazzComponent implements OnInit { + // 默认显示第1页的内容 + page = 0; + // 每页默认为3条 + size = 3; + + // 初始化一个有0条数据的 + pageData = new Page<Clazz>({ + content: [], + number: this.page, + size: this.size, + numberOfElements: 0 + }); + constructor() { } ``` 上述代码我们增加了三个属性,并分别设置了默认值。接下来我们在C层中的`ngOnInit()`方法中模拟生成分页数据: ```typescript +++ b/first-app/src/app/clazz/clazz.component.ts ngOnInit(): void { const clazzes = new Array<Clazz>(); for (let i = 0; i < this.size; i++) { clazzes.push(new Clazz({ id: i, name: '班级', teacher: new Teacher({ id: i, name: '教师' }) })); } this.pageData = new Page<Clazz>({ content: clazzes, number: 2, size: this.size, numberOfElements: 20 }); } ``` ## V层对接 C层模拟数据准备好后,我们来到V层完成对接。首先删除原来的测试数据,仅保留有用的表头和基础结构: ```html <div class="row"> <div class="col-12 text-right"> <a class="btn btn-primary mr-2"><i class="fas fa-plus"></i>新增</a> </div> </div> <table class="table table-striped mt-2"> <thead> <tr class="table-primary"> <th>序号</th> <th>名称</th> <th>班主任</th> <th>操作</th> </tr> </thead> <tbody> </tbody> </table> <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> </ul> </nav> ``` 接着引入`*ngFor`完成班级的循环输出: ```html +++ b/first-app/src/app/clazz/clazz.component.html @@ -14,6 +14,19 @@ </tr> </thead> <tbody> + <tr *ngFor="let clazz of pageData.content; index as index"> + <td>{{index + 1}}</td> + <td>{{clazz.name}}</td> + <td>教师姓名</td> + <td> + <a class="btn btn-outline-primary btn-sm"> + <i class="fas fa-pen"></i>编辑 + </a> + <span class="btn btn-sm btn-outline-danger"> + <i class="far fa-trash-alt"></i>删除 + </span> + </td> + </tr> </tbody> ``` ![image-20210330104757923](https://img.kancloud.cn/f5/6f/f56f1250413377a4389d9a2c83cfdf2e_1582x448.png) 最后显示班主任的姓名,由于`clazz`中存在`teacher`属性,所以在V层中可以非常轻构地显示班主任姓名: ```html - <td>教师姓名</td> + <td>{{clazz.teacher.name}}</td> ``` 最终效果如下: ![image-20210330105031585](https://img.kancloud.cn/c4/65/c46558ff3969b5396bfb9a09137ca663_1308x364.png) ## 本节作业 和后台的提供的教师分页接口相同,后台还提供了一个班级分页接口: ```bash GET /clazz/page ``` | **类型Type** | **名称Name** | **描述Description** | 必填 | **类型Schema** | 默认值 | | :------------ | :----------- | :----------------------- | ---- | :--------------------------- | --------- | | Param请求参数 | `page` | 第几页 | 否 | `number` | 0 | | Param请求参数 | `size` | 每页大小 | 否 | `number` | 20 | | Param请求参数 | sort | 排序字段及方式(支持多个) | 否 | `string`例:`sort=name,desc` | `id,desc` | | Response响应 | | Status Code: 200 | | `Page<Clazz>` | | 请根据该接口,建立相应的MockApi,在组件中应用MockApi来达到模拟获取后台数据的目的。 | 名称 | 链接 | | -------------- | ------------------------------------------------------------ | | REST分页及排序 | [https://docs.spring.io/spring-data/rest/docs/3.4.6/reference/html/#paging-and-sorting](https://docs.spring.io/spring-data/rest/docs/3.4.6/reference/html/#paging-and-sorting) | | 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.3.2.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.3.2.zip) |