開發分頁方法有兩個方法,第一個是直接在當前的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層點擊相應的分頁,查看控制臺變化:

觸發了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中的參數,所以此時點擊分頁時將得到一個如下錯誤:

> ? 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})
```
此時當我們再次點擊分頁按扭時,錯誤消失并且可以在控制臺中發現打印的數據返回情況:

但組件中顯示的班級列表,卻沒有任何變化。這是由于我們在當前的單元測試中使用了`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`的一些語法提示。
此時再次點擊相應的分頁,則會在控制臺中打印具體的分頁信息:

第一行打印了兩個`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頁:

控制臺打印的返回數據信息,顯示當前頁為第2頁。

最后為了保持組件風格統一,我們在組件初始化時當每頁大小設置為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頁是選中狀態:

如果是第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>
```

上述方法雖然可行,但冗余的代碼有些過多。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`代碼時,會看到如下代碼:

同樣,我們還可以使用`[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) |
- 序言
- 第一章 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 發布部署
- 第九章 總結