debounceTime(延遲輸入,避免連續觸發)distinctUntilChanged(忽略相同輸入)switchMap(取消舊請求,只保留最新)debounceTime(ms):輸入結束後等指定毫秒才送出 → 避免打字中每個字都查一次。distinctUntilChanged():如果輸入跟上次一樣,就不觸發。switchMap():新請求來時會自動取消舊請求,只保留最新的。 → 適合搜尋。LoadingServiceng 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 版履歷網站的最後整理 & 部署 🎉: