分页是一个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) |
- 序言
- 第一章 Hello World
- 1.1 环境安装
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教师管理
- 2.1 教师列表
- 2.1.1 初始化原型
- 2.1.2 组件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 请求后台数据
- 2.2.1 HttpClient
- 2.2.2 请求数据
- 2.2.3 模块与依赖注入
- 2.2.4 异步与回调函数
- 2.2.5 集成测试
- 2.2.6 本章小节
- 2.3 新增教师
- 2.3.1 组件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 对接后台
- 2.3.4 路由
- 2.4 编辑教师
- 2.4.1 组件初始化
- 2.4.2 获取路由参数
- 2.4.3 插值与模板表达式
- 2.4.4 初识泛型
- 2.4.5 更新教师
- 2.4.6 测试中的路由
- 2.5 删除教师
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome图标库
- 2.6.3 firefox
- 2.7 总结
- 第三章 用户登录
- 3.1 初识单元测试
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 着陆组件
- 3.5 @Output
- 3.6 TypeScript 类
- 3.7 浏览器缓存
- 3.8 总结
- 第四章 个人中心
- 4.1 原型
- 4.2 管道
- 4.3 对接后台
- 4.4 x-auth-token认证
- 4.5 拦截器
- 4.6 小结
- 第五章 系统菜单
- 5.1 延迟及测试
- 5.2 手动创建组件
- 5.3 隐藏测试信息
- 5.4 规划路由
- 5.5 定义菜单
- 5.6 注销
- 5.7 小结
- 第六章 班级管理
- 6.1 新增班级
- 6.1.1 组件初始化
- 6.1.2 MockApi 新建班级
- 6.1.3 ApiInterceptor
- 6.1.4 数据验证
- 6.1.5 教师选择列表
- 6.1.6 MockApi 教师列表
- 6.1.7 代码重构
- 6.1.8 小结
- 6.2 教师列表组件
- 6.2.1 初始化
- 6.2.2 响应式表单
- 6.2.3 getTestScheduler()
- 6.2.4 应用组件
- 6.2.5 小结
- 6.3 班级列表
- 6.3.1 原型设计
- 6.3.2 初始化分页
- 6.3.3 MockApi
- 6.3.4 静态分页
- 6.3.5 动态分页
- 6.3.6 @Input()
- 6.4 编辑班级
- 6.4.1 测试模块
- 6.4.2 响应式表单验证
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定义FormControl
- 6.4.6 代码重构
- 6.4.7 小结
- 6.5 删除班级
- 6.6 集成测试
- 6.6.1 惰性加载
- 6.6.2 API拦截器
- 6.6.3 路由与跳转
- 6.6.4 ngStyle
- 6.7 初识Service
- 6.7.1 catchError
- 6.7.2 单例服务
- 6.7.3 单元测试
- 6.8 小结
- 第七章 学生管理
- 7.1 班级列表组件
- 7.2 新增学生
- 7.2.1 exports
- 7.2.2 自定义验证器
- 7.2.3 异步验证器
- 7.2.4 再识DI
- 7.2.5 属性型指令
- 7.2.6 完成功能
- 7.2.7 小结
- 7.3 单元测试进阶
- 7.4 学生列表
- 7.4.1 JSON对象与对象
- 7.4.2 单元测试
- 7.4.3 分页模块
- 7.4.4 子组件测试
- 7.4.5 重构分页
- 7.5 删除学生
- 7.5.1 第三方dialog
- 7.5.2 批量删除
- 7.5.3 面向对象
- 7.6 集成测试
- 7.7 编辑学生
- 7.7.1 初始化
- 7.7.2 自定义provider
- 7.7.3 更新学生
- 7.7.4 集成测试
- 7.7.5 可订阅的路由参数
- 7.7.6 小结
- 7.8 总结
- 第八章 其它
- 8.1 打包构建
- 8.2 发布部署
- 第九章 总结