分頁是一個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,返回數據格式為對象。我們使用瀏覽器直接訪問當前地址,看看返回的分頁數據到底長什么樣子:

這樣的結果看起來亂糟糟的,那就打開控制臺后找到網絡選項卡,刷新后再看看吧:

我們發現該對象的`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
}
```

參考上節的分頁原型,我們在此僅將需要使用到的字段使用??進行了標注,各個字段解釋如下:
- `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();
});
```

## 初始化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>
```

最后顯示班主任的姓名,由于`clazz`中存在`teacher`屬性,所以在V層中可以非常輕構地顯示班主任姓名:
```html
- <td>教師姓名</td>
+ <td>{{clazz.teacher.name}}</td>
```
最終效果如下:

## 本節作業
和后臺的提供的教師分頁接口相同,后臺還提供了一個班級分頁接口:
```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 發布部署
- 第九章 總結