iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0

Day 8 - 在 Angular 中加入漸進式網頁應用程式(PWA)支援 - Part 2

昨天,我們看到了觸發 MCP 伺服器工具並生成檔案的不同提示詞。

今天我們將展示具體的實作方式。理解這些程式碼非常重要,這樣我們才能要求 AI 進一步微調架構,甚至在需要時親自修復錯誤。

Angular 中的 PWA 定義

PwaUpdateService 實作了 Service Worker 事件以進行檢查更新、錯誤復原以及背景輪詢。

檢查更新

@Service()
export class PwaUpdateService {
  readonly #swUpdate = inject(SwUpdate);
  readonly #window = inject(WINDOW);

  readonly updateAvailable = toSignal(
    this.#window && this.#swUpdate.isEnabled
      ? this.#swUpdate.versionUpdates.pipe(
          filter((evt): evt is VersionReadyEvent => evt.type === 'VERSION_READY'),
          map(() => true),
        )
      : EMPTY,
    { initialValue: false },
  );

  async reloadPage(): Promise<void> {
    if (this.#window) {
      if (this.#swUpdate.isEnabled) {
        await this.#swUpdate.activateUpdate();
      }
      this.#window.location.reload();
    }
  }
}

updateAvailable 將 Observable 轉換為 Signal,這樣元件就不需要匯入 AsyncPipe 來解析 Observable 以取得數值。

updateAvailable 的初始值為 false,表示目前沒有更新。當 this.#swUpdate.versionUpdates 發出 VersionReadyEvent 時,該 Signal 會變為 true 並顯示 PWA 橫幅(banner)。

reload 方法會重新載入視窗。這是一項安全的操作,因為 #window 參考了全域的 window 物件。

緊急自我修復機制

readonly #destroyRef$ = inject(DestroyRef);

constructor() {
    if (this.#window && this.#swUpdate.isEnabled) {
      this.#swUpdate.unrecoverable
        .pipe(takeUntilDestroyed(this.#destroyRef$))
        .subscribe(() => this.#window?.location.reload());
    }
}

當 Service Worker 發出不可復原(unrecoverable)的事件時,視窗會自動重新載入以獲取新資產並嘗試復原。我們注入了 DestroyRef 並使用 takeUntilDestroyed 操作符來自動取消訂閱。

背景輪詢

正式環境的應用程式每隔 1 小時會進行輪詢以檢查更新。如果有更新,系統會顯示 PWA 橫幅元件以提示下載最新版本。

import { InjectionToken } from '@angular/core';

export const PWA_CHECK_INTERVAL = new InjectionToken<number>('PWA_CHECK_INTERVAL', {
  providedIn: 'root',
  factory: () => {
    const milliseconds = 1000;
    const seconds = 60;
    return 1 * seconds * seconds * milliseconds;
  },
});

我們定義了一個注入權杖 PWA_CHECK_INTERVAL,並在工廠函式中回傳該數值。在開發測試與模擬資料時,將此間隔時間覆寫為較短的數值可大幅簡化流程。

readonly #destroyRef$ = inject(DestroyRef);
readonly #pwaCheckInterval = inject(PWA_CHECK_INTERVAL);
readonly #appRef = inject(ApplicationRef);

constructor() {
    const isAppStable$ = this.#appRef.isStable.pipe(
      filter((isStable) => isStable),
      take(1),
    );
    const polling$ = interval(this.#pwaCheckInterval);

    concat(isAppStable$, polling$)
      .pipe(
        exhaustMap(() =>
          from(this.#swUpdate.checkForUpdate()).pipe(
            catchError((e) => {
              console.error(e);
              return EMPTY;
            }),
          ),
        ),
        takeUntilDestroyed(this.#destroyRef$),
      )
      .subscribe();
}

isAppStable$ Observable 會等待應用程式完全載入完成後透過 take(1) 自動取消訂閱。接著,polling$ 每 1 小時檢查一次更新。當找到新更新時,exhaustMap 會發出 true,否則發出 false

updateAvailable 會訂閱 this.#swUpdate.versionUpdates,並更新為 true。同樣地,PWA 橫幅元件將會出現,我們可以點擊按鈕來重新載入視窗。

這是不是很棒呢?

Angular 中的響應式使用者介面

安裝 PWA 依賴套件(因為我想節省一些 token):

@Component({
  selector: 'app-pwa-update-banner',
  template: `
    @if (pwaUpdateService.updateAvailable()) {
      <div class="pwa-banner">
        <span class="pwa-text">A new version is available!</span>
        <button (click)="pwaUpdateService.reloadPage()" class="pwa-button">Reload</button>
      </div>
    }
  `,
  styleUrl: './pwa-update-banner.css',
})
export class PwaUpdateBanner {
  readonly pwaUpdateService = inject(PwaUpdateService);
}

當新部署完成後,只要 pwaUpdateService.updateAvailable() 為 true,PWA 橫幅元件就會出現。

當使用者點擊 Reload 按鈕時,視窗會重新載入以獲取新的資產與正式版打包檔案。

測試驅動開發與 100% 涵蓋率

當服務新增、修改或刪除程式碼時,我們需要確保各項測試案例都不會失敗,且具備良好的程式碼涵蓋率。

該元件經過了全面的測試,因為程式碼測試涵蓋率達到了 100%。

我們已成功展示了 PWA 的實作方式,它大幅提升了應用程式的使用者體驗。

明天,我們將展示如何在應用程式中實作 Angular Aria。Angular Aria 在 Angular v22 中已經趨於穩定,我想藉由學習它來建立肌肉記憶。這一次,我會使用 tdd 建立測試案例的架構,並親自編寫增強功能的程式碼。最後,套用 code-review 針對 PRD 和 ADR 比對 Git commit SHA,以確保沒有範圍蔓延與遺漏的需求。

相關資源:
Angular Service Worker 與 PWA
Angular Signals 與 RxJS 互通性 (Interop)


上一篇
Day 7 - 在 Angular 中加入漸進式網頁應用程式(PWA)支援 - Part 1
下一篇
Day 9 - 在 Angular 中加入 Angular Aria 支援 - 第一部分
系列文
2026年,如何利用 Antigravity CLI、Gemini、各項技能及 MCP Server 建構基於 Firebase 的 Angular 應用9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言