async
pipe 的基礎用法Angular 內建的 HTTP 工具,支援:
http.get<T>(url)
:發 GET 請求,回傳 Observablehttp.post<T>(url, body)
:發 POST 請求👉 需要在 AppModule 匯入 HttpClientModule
才能用。
subscribe()
。範例:
projects$ = this.http.get<Project[]>('api/projects');
<li *ngFor="let p of projects$ | async">{{ p.title }}</li>
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
BrowserModule,
HttpClientModule,
// 其他模組
],
})
export class AppModule {}
projects-data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Project } from '../models/project.model';
import { Observable, of } from 'rxjs';
import { catchError, delay } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class ProjectsDataService {
private readonly apiUrl = 'assets/projects.json';
// 這裡用本地 JSON 模擬,未來可換成真正 API
constructor(private http: HttpClient) {}
getAll$(): Observable<Project[]> {
return this.http.get<Project[]>(this.apiUrl).pipe(
delay(500), // 模擬網路延遲
catchError(err => {
console.error('載入失敗', err);
return of([]); // 出錯回傳空陣列
})
);
}
}
在 src/assets/projects.json
建立檔案:
[
{
"title": "毛毛購物(寵物電商)",
"tech": "Angular + Node.js|購物車、結帳、RWD",
"desc": "主導前端架構,完成商品列表、購物流程與訂單頁。",
"link": "#"
},
{
"title": "LINE Bot 預約系統",
"tech": "Cloud Functions + LINE API|時段預約",
"desc": "整合 LINE 聊天介面與雲端排程,完成會員預約流程。",
"link": "#"
}
]
改用 Observable
+ async pipe
。
projects.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Project } from '../../models/project.model';
import { ProjectsDataService } from '../../services/projects-data.service';
@Component({
selector: 'app-projects',
templateUrl: './projects.component.html',
styleUrls: ['./projects.component.scss']
})
export class ProjectsComponent {
projects$!: Observable<Project[]>;
isLoading = true;
constructor(private projectsSvc: ProjectsDataService) {
this.projects$ = this.projectsSvc.getAll$();
// 使用 async pipe 時不用手動 subscribe
}
}
projects.component.html
<section id="projects" class="container section">
<h2>作品集 Projects</h2>
<!-- Loading 狀態 -->
<p *ngIf="isLoading">載入中...</p>
<!-- 用 async pipe 訂閱並顯示 -->
<div class="project-grid" *ngIf="projects$ | async as projects; else errorTpl">
<article class="card" *ngFor="let project of projects; trackBy: trackByTitle">
<h3>{{ project.title }}</h3>
<p class="muted">{{ project.tech }}</p>
<p>{{ project.desc | summary:50 }}</p>
<a class="btn small" [href]="project.link" target="_blank">Live Demo</a>
</article>
</div>
<!-- 錯誤處理 -->
<ng-template #errorTpl>
<p class="error">無法載入專案資料,請稍後再試。</p>
</ng-template>
</section>
NullInjectorError: No provider for HttpClient!
AppModule.imports
要加上 HttpClientModule
subscribe()
要自己管理 unsubscribeasync
pipe Angular 自動處理訂閱/取消,更安全http.get()
,出錯畫面會壞掉catchError
或在 template 加 ngIf
明天我們會深入 Router 進階:
src/assets/projects.json
)新增 id(數字)、slug(字串)、images、tags、createdAt、repo、demo、featured 等欄位,之後可依 id 或 slug 查詢。
[
{
"id": 1,
"slug": "maomao-shop",
"title": "毛毛購物(寵物電商)",
"tech": "Angular + Node.js|購物車、結帳、RWD",
"desc": "主導前端架構,完成商品列表、購物流程與訂單頁。",
"tags": ["angular", "node", "rwd", "ecommerce"],
"images": ["assets/projects/maomao-1.png", "assets/projects/maomao-2.png"],
"demo": "#",
"repo": "#",
"createdAt": "2024-08-12",
"featured": true},
{
"id": 2,
"slug": "line-bot-reservation",
"title": "LINE Bot 預約系統",
"tech": "Cloud Functions + LINE API|時段預約",
"desc": "整合 LINE 聊天介面與雲端排程,完成會員預約流程。",
"tags": ["line", "gcp", "serverless"],
"images": ["assets/projects/line-1.png", "assets/projects/line-2.png"],
"demo": "#",
"repo": "#",
"createdAt": "2024-05-30",
"featured": false}
]
src/app/models/project.model.ts
)export interface Project {
id: number;
slug: string;
title: string;
tech: string;
desc: string;
tags: string[];
images: string[];
demo: string;
repo: string;
createdAt: string; // ISO 日期字串;顯示時可配合 date pipe
featured: boolean;
}
src/app/services/projects-data.service.ts
)加入 getAll$()、getById$()、getBySlug$(),方便 Resolver 或詳情頁取用。保留錯誤處理與延遲模擬。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Project } from '../models/project.model';
import { Observable, of } from 'rxjs';
import { catchError, delay, map } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class ProjectsDataService {
private readonly apiUrl = 'assets/projects.json';
constructor(private http: HttpClient) {}
getAll$(): Observable<Project[]> {
return this.http.get<Project[]>(this.apiUrl).pipe(
delay(300),
catchError(err => {
console.error('載入專案列表失敗', err);
return of([]);
})
);
}
getById$(id: number): Observable<Project | undefined> {
return this.getAll$().pipe(map(list => list.find(p => p.id === id)));
}
getBySlug$(slug: string): Observable<Project | undefined> {
return this.getAll$().pipe(map(list => list.find(p => p.slug === slug)));
}
}
ProjectsComponent
)小改列表用 routerLink 帶 id 或 slug。建議用 slug,網址更漂亮:/projects/maomao-shop
<!-- projects.component.html -->
<section id="projects" class="container section">
<h2>作品集 Projects</h2>
<div class="project-grid" *ngIf="projects$ | async as projects">
<article class="card" *ngFor="let project of projects; trackBy: trackByTitle">
<h3>{{ project.title }}</h3>
<p class="muted">{{ project.tech }}</p>
<p>{{ project.desc | summary:60 }}</p>
<a class="btn small" [routerLink]="['/projects', project.slug]">查看詳情</a>
</article>
</div>
</section>
如果先用 id 也行:[routerLink]="['/projects', project.id]"
兩種方案擇一,明天 Day 17 我會示範用 Resolver 先載入單筆資料。
路由(slug 版)
const routes: Routes = [
{ path: '', redirectTo: 'projects', pathMatch: 'full' },
// 列表
{ path: 'projects', loadChildren: () => import('./projects/projects.module').then(m => m.ProjectsModule) },
// 兜底
{ path: '**', redirectTo: 'projects' }
];
projects-routing.module.ts
(子模組內)
const routes: Routes = [
{ path: '', component: ProjectsComponent },
{ path: ':slug', component: ProjectDetailComponent } // 明天會加 resolver
];
詳情頁讀參數(暫時版,Day 17 會改用 Resolver 注入資料)
// project-detail.component.ts(暫時直接從參數載入)
import { ActivatedRoute } from '@angular/router';
import { switchMap } from 'rxjs/operators';
slug$ = this.route.paramMap.pipe(
map(params => params.get('slug') ?? ''),
);
project$ = this.slug$.pipe(
switchMap(slug => this.projectsSvc.getBySlug$(slug))
);
<!-- project-detail.component.html(暫時用 async 取值) -->
<div class="container section" *ngIf="project$ | async as project">
<h2>{{ project.title }}</h2>
<p class="muted">{{ project.tech }}</p>
<div class="gallery" *ngIf="project.images?.length">
<img *ngFor="let src of project.images" [src]="src" alt="專案截圖" width="400">
</div>
<p>{{ project.desc }}</p>
<div class="actions">
<a class="btn" [href]="project.demo" target="_blank">Live Demo</a>
<a class="btn btn-outline" [href]="project.repo" target="_blank">GitHub</a>
</div>
<br />
<a routerLink="/projects" class="btn btn-outline">返回列表</a>
</div>
/projects/:slug
前先把單筆 Project
載入好,失敗時導回 /projects
或顯示 Not Found。switchMap
)改成 透過路由設定注入資料,component 直接讀取 this.route.data
.