上一篇將待辦事項資料從元件 (Component) 抽離至服務 (Service) 中;接下來,這一篇將利用 Angular 內建的 httpClient 從伺服器取得待辦事項資料。
為了可以從伺服器中取得資料,因此需要在終端機執行 npm install -g json-server
,透過 json-server 來模擬後端服務。並在 task.ts 加入 id
屬性,以便之後可以新增、編輯與刪除資料。
json-server 主要用來模擬後端服務的 API,並不建議使用在正式環境中。
import { TaskState } from "../enum/task-state.enum";
export class Task {
constructor(
public id: number,
public subject: string,
public state: TaskState = TaskState.None
) {}
level?: "XS" | "S" | "M" | "L" | "XL";
tags?: string[];
expectDate?: Date;
finishedDate?: Date;
}
接著,在 /src/assets/data 加入 db.json 檔案,並在此檔案加入待辦事項資料清單,此檔案即為 json-server 所存取的資料。
{
"tasks": [
{
"id": 1,
"subject": "頁面需要顯示待辦事項主旨",
"state": 0,
"level": "XS",
"tags": ["FEATURE", "ISSUE", "enhancement", "discussion"]
},
{
"id": 2,
"subject": "可以設定待辦事項的狀態",
"state": 1,
"level": "S",
"tags": ["Feature", "Issue", "document"],
"expectDate": "2020-10-31T16:00:00.000Z"
},
{
"id": 3,
"subject": "頁面需要顯示待辦事項主旨",
"state": 2,
"level": "M",
"tags": ["feature", "issue"],
"expectDate": "2020-09-30T16:00:00.000Z",
"finishedDate": "2020-09-30T16:00:00.000Z"
}
]
}
最後,在終端機執行 json-server src/assets/data/db.json
,就可以透過 GET、POST、PUT 與 DELETE 等 HTTP 方法針對 http://localhost:3000/tasks
路徑來新增、修改與刪除待辦事項資料。
Angualr 內建的 HttpClient 類別提供了 get()
、post()
、put()
與 delete()
等方法對應 HTTP 方法來使用後端服務 API。因此,先執行 'ng g s task/services/task-remote' 指令來建立 TaskRemoteService 服務,並在此服務中利用 HttpClient 的 get()
方法取得資料;不過在使用之前需要將 HttpClientModule
模組匯入 TaskModule 內。
@NgModule({
imports: [CommonModule, HttpClientModule],
declarations: [
TaskComponent,
TaskStateColorDirective,
TaskListComponent,
TaiwanDatePipe,
],
exports: [TaskComponent, TaskListComponent],
})
export class TaskModule {}
在 Httpclient 類別內的方法,會以非同步發出一個 HTTP 請求,並回傳的是 Observable 型別;利用 Observable 的回傳值,可以監控與訂閱非同步請求的狀態,或利用 RxJs 來調整資料內容的狀態。因此,在 task-remote.service.ts 中的 getData()
會使用 HttpClient.get()
方法來取得待辦事項,並回傳一 Observable<Task[]>
型別資料。
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { Task } from "../../model/task";
@Injectable({
providedIn: "root",
})
export class TaskRemoteService {
private _url = "http://localhost:3000/tasks";
constructor(private httpClient: HttpClient) {}
getData(): Observable<Task[]> {
return this.httpClient.get<Task[]>(this._url);
}
}
最後,在 task-list.component.ts 注入 TaskRemoteService 服務,並訂閱 getDate()
方法將取得到的清單資料設定至待辦事項屬性中。
import { Component, OnInit } from "@angular/core";
import { Task } from "../../model/task";
import { TaskRemoteService } from "../services/task-remote.service";
@Component({
selector: "app-task-list",
templateUrl: "./task-list.component.html",
styleUrls: ["./task-list.component.css"],
})
export class TaskListComponent implements OnInit {
tasks: Task[];
constructor(private taskService: TaskRemoteService) {}
ngOnInit(): void {
this.taskService.getData().subscribe((tasks) => (this.tasks = tasks));
}
}
需要注意的是
Observable
資料型別需要訂閱 (subscribe) 才會被執行。
從上面結果可看到是否為沒有待辦事項的顯示判斷中拋出了例外,這是因為使用了非同步取得資料,而在資料未取得到之前,tasks
屬性值則為 undefined
而拋出例外。要解決這個問題,可以在 task-list.component.html 利用 ?
關鍵字,使其在 tasks
屬性為 undefined
時不取得 'length' 資訊。
<ng-container
*ngIf="tasks?.length === 0; then dataEmpty; else list"
></ng-container>
另外,Angular 也提供了 AsyncPipe 來訂閱 Observable 物件。因此,也可以將 task-list.component.ts 的 tasks
屬性修改為 tasks$: Observable<Task[]>
,以表示一個可監控的待辦事項屬性;接著將此屬性值設定待辦事項服務的 getData()
方法回傳值。
import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { Task } from "../../model/task";
import { TaskRemoteService } from "../services/task-remote.service";
@Component({
selector: "app-task-list",
templateUrl: "./task-list.component.html",
styleUrls: ["./task-list.component.css"],
})
export class TaskListComponent implements OnInit {
tasks$: Observable<Task[]>;
constructor(private taskService: TaskRemoteService) {}
ngOnInit(): void {
this.tasks$ = this.taskService.getData();
}
}
在 Angular 的命名習慣上,會用
$
作為結尾來代表 Observable 物件。
而在 task-liet.component_html 中,利用 AsyncPipe 訂閱 task$
屬性,在取得到資料時顯示 #list
範本,否則顯示 #dataEmpty
。
<ng-container *ngIf="tasks$ | async; then list; else dataEmpty"></ng-container>
<ng-template #dataEmpty>
<div class="data-empty">無待辦事項</div>
</ng-template>
<ng-template #list>
<app-task
*ngFor="let task of tasks$ | async; let odd = odd"
[class.odd]="odd"
[subject]="task.subject"
[(state)]="task.state"
[level]="task.level"
[tags]="task.tags"
[expectDate]="task.expectDate"
[finishedDate]="task.finishedDate"
></app-task>
</ng-template>
不過,從上面結果可以看到,因為使用了兩次 AsyncPipe 而發送了兩次 Http 請求,進一步影響了使用者體驗。此時可以透過在 AsyncPipe 後加入 as
關鍵字 ,來建立樣版區域變數供 ngFor
使用。
<ng-container *ngIf="tasks$ | async as tasks; else dataEmpty">
<app-task
*ngFor="let task of tasks; let odd = odd"
[class.odd]="odd"
[subject]="task.subject"
[(state)]="task.state"
[level]="task.level"
[tags]="task.tags"
[expectDate]="task.expectDate"
[finishedDate]="task.finishedDate"
></app-task>
</ng-container>
<ng-template #dataEmpty>
<div class="data-empty">無待辦事項</div>
</ng-template>
這一篇利用了 HttpClient 類別,來從後端服務 API 取得資料清單,並進一步使用了 AsyncPipe 訂閱 Observable 物件,實作程式碼放在 GitHub。
謝謝你的分享,但是關於這個code
<ng-container
*ngIf="tasks?.length === 0; then dataEmpty; else list"
></ng-container>
「當tasks為undefined就不會執行.length」
相當於
// 偷推一下JSX
if(undefined===0)
dataEmpty
else
list
(undefined===0)的結果是false
分支會走向list
這也合理,還沒要到資料,並不代表「無待辦事項」
但下面這部分
ng-container *ngIf="tasks$ | async as tasks; else dataEmpty">
<app-task
*ngFor="let task of tasks; let odd = odd"
[class.odd]="odd"
[subject]="task.subject"
[(state)]="task.state"
[level]="task.level"
[tags]="task.tags"
[expectDate]="task.expectDate"
[finishedDate]="task.finishedDate"
></app-task>
</ng-container>
<ng-template #dataEmpty>
<div class="data-empty">無待辦事項</div>
</ng-template>
在tasks.length為0時,不會進到dataEmpty也
這裡的else 是不是指subscribe失敗了?
自答一下:原來還沒subscribe到資料前,會進else
我懂了所以上面其實是
<ng-container *ngIf="tasks$ | async as tasks; else tasksUndefined">
<app-task
*ngFor="let task of tasks; let odd = odd"
[class.odd]="odd"
[subject]="task.subject"
[(state)]="task.state"
[level]="task.level"
[tags]="task.tags"
[expectDate]="task.expectDate"
[finishedDate]="task.finishedDate"
></app-task>
</ng-container>
<ng-template #tasksUndefined>
<div class="data-loading">還在loading</div>
</ng-template>