debounceTime
(延遲輸入,避免連續觸發)distinctUntilChanged
(忽略相同輸入)switchMap
(取消舊請求,只保留最新)debounceTime(ms)
:輸入結束後等指定毫秒才送出 → 避免打字中每個字都查一次。distinctUntilChanged()
:如果輸入跟上次一樣,就不觸發。switchMap()
:新請求來時會自動取消舊請求,只保留最新的。 → 適合搜尋。LoadingService
ng g s services/loading
loading.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class LoadingService {
private loadingSubject = new BehaviorSubject<boolean>(false);
loading$ = this.loadingSubject.asObservable();
show() { this.loadingSubject.next(true); }
hide() { this.loadingSubject.next(false); }
}
修改 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, finalize } from 'rxjs/operators';
import { LoadingService } from './loading.service';
@Injectable({ providedIn: 'root' })
export class ProjectsDataService {
private readonly apiUrl = 'assets/projects.json';
constructor(private http: HttpClient, private loading: LoadingService) {}
getAll$(): Observable<Project[]> {
this.loading.show();
return this.http.get<Project[]>(this.apiUrl).pipe(
delay(500), // 模擬延遲
catchError(err => {
console.error('載入失敗', err);
return of([]);
}),
finalize(() => this.loading.hide()) // 無論成功/失敗都關閉 loading
);
}
}
app.component.html
<div *ngIf="loading$ | async" class="loading-overlay">
<div class="spinner">載入中...</div>
</div>
<app-header></app-header>
<router-outlet></router-outlet>
<app-footer></app-footer>
app.component.ts
import { Component } from '@angular/core';
import { LoadingService } from './services/loading.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
loading$ = this.loading.loading$;
constructor(private loading: LoadingService) {}
}
styles.scss
(簡單樣式)
.loading-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(255, 255, 255, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.spinner {
padding: 20px;
background: white;
border: 2px solid #2563eb;
border-radius: 8px;
}
我們把輸入框事件串進 RxJS pipeline,而不是即時更新。
skills.component.ts
import { Component, OnInit } from '@angular/core';
import { UiStateService } from '../../services/ui-state.service';
import { SkillsDataService } from '../../services/skills-data.service';
import { Skill } from '../../models/skill.model';
import { Subject, combineLatest } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, map, startWith } from 'rxjs/operators';
@Component({
selector: 'app-skills',
templateUrl: './skills.component.html'
})
export class SkillsComponent implements OnInit {
skills: Skill[] = [];
private keywordInput$ = new Subject<string>();
filtered$ = combineLatest([
this.ui.skillCategory$,
this.keywordInput$.pipe(
debounceTime(300), // 等 0.3 秒再觸發
distinctUntilChanged(), // 相同輸入忽略
startWith('') // 預設空字串
)
]).pipe(
map(([cat, kw]) =>
this.skills.filter(s => {
const byCat = cat === 'all' || s.category === cat;
const byKw = !kw || s.name.toLowerCase().includes(kw.toLowerCase());
return byCat && byKw;
})
)
);
constructor(private ui: UiStateService, private svc: SkillsDataService) {}
ngOnInit() {
this.skills = this.svc.getAll();
}
setFilter(cat: 'all' | 'frontend' | 'backend' | 'tools') {
this.ui.setSkillCategory(cat);
}
onKeywordChange(kw: string) {
this.keywordInput$.next(kw);
}
}
skills.component.html
<section class="container section">
<h2>技能 Skillset</h2>
<div class="filters">
<button (click)="setFilter('all')">全部</button>
<button (click)="setFilter('frontend')">前端</button>
<button (click)="setFilter('backend')">後端</button>
<button (click)="setFilter('tools')">工具</button>
</div>
<input type="text"
placeholder="搜尋技能…"
(input)="onKeywordChange($event.target.value)" />
<ul>
<li *ngFor="let s of filtered$ | async">{{ s.name }}</li>
</ul>
</section>
switchMap
、debounceTime
、distinctUntilChanged
實戰應用,為未來更複雜的 API 串接鋪路。finalize()
保證成功/失敗都會觸發startWith('')
補上async pipe
或 takeUntil
,避免 memory leak我們將進行 Angular 版履歷網站的最後整理 & 部署 🎉: