為 Flutter Web App 加入 PWA 支援完整教學

前言

PWA(Progressive Web App)讓網頁應用也能像原生 App 一樣被安裝到裝置上、擁有離線功能和推送通知。最近我在為自己的開發者網站加入 PWA 支援的過程中學到了不少東西,這篇文章就來分享完整的實作過程。

PWA 是什麼?為什麼需要它?

簡單來說,PWA 是一組讓網頁應用「進化」的技術標準。一個完整的 PWA 需要具備:

  • 可安裝:使用者可以把網站「安裝」到手機桌面或電腦上
  • 離線支援:透過 Service Worker 快取資源,即使沒有網路也能使用
  • 安全連線:必須透過 HTTPS 提供服務
  • Responsive:適應不同裝置的螢幕尺寸

對獨立開發者來說,PWA 的好處是讓你的網站也能提供接近 App 的體驗,而不需要額外上架到 App Store。

第一步:設定 Web App Manifest

manifest.json 是 PWA 的核心設定檔,告訴瀏覽器你的應用資訊。在 Flutter Web 專案中,這個檔案位於 web/manifest.json

{
  "name": "Atlantis Kid - App Developer",
  "short_name": "Atlantis Kid",
  "description": "台灣獨立 App 開發者 - 專注於兒童遊戲與教育應用",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#0f172a",
  "theme_color": "#6366f1",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ]
}

幾個重要的設定說明:

  • display: "standalone":讓 App 開啟時隱藏瀏覽器的網址列,看起來像原生 App
  • theme_color:影響手機狀態列和視窗標題列的顏色
  • icons:至少需要 192x192 和 512x512 兩個尺寸,maskable 用途的圖示可以被系統自動裁切成圓形或其他形狀

然後在 HTML 的 <head> 中引用:

<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#6366f1">
<link rel="apple-touch-icon" href="/icons/icon-192x192.png">

第二步:實作 Service Worker

Service Worker 是 PWA 的靈魂,負責攔截網路請求和管理快取。在專案根目錄建立 sw.js

const CACHE_NAME = 'atlantiskid-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/css/style.css',
  '/icons/icon-192x192.png',
  '/icons/icon-512x512.png'
];

// 安裝階段:快取核心資源
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => {
        console.log('快取核心資源');
        return cache.addAll(urlsToCache);
      })
  );
});

// 啟動階段:清除舊版快取
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheName !== CACHE_NAME) {
            console.log('清除舊快取:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

// 攔截請求:優先使用快取,失敗才走網路
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        if (response) {
          return response;
        }
        return fetch(event.request).then((response) => {
          // 只快取成功的 GET 請求
          if (!response || response.status !== 200 || response.type !== 'basic') {
            return response;
          }
          const responseToCache = response.clone();
          caches.open(CACHE_NAME)
            .then((cache) => {
              cache.put(event.request, responseToCache);
            });
          return response;
        });
      })
  );
});

快取策略選擇

Service Worker 有幾種常見的快取策略:

  1. Cache First(快取優先):先查快取,沒有才走網路。適合靜態資源。
  2. Network First(網路優先):先走網路,失敗才用快取。適合需要即時性的內容。
  3. Stale While Revalidate:先回傳快取的內容,同時在背景更新快取。體驗最好。

上面的範例使用的是 Cache First 策略,對於靜態網站來說非常適合。

第三步:註冊 Service Worker

index.html 中加入 Service Worker 的註冊程式碼:

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/sw.js')
        .then((registration) => {
          console.log('Service Worker 註冊成功,範圍:', registration.scope);
        })
        .catch((error) => {
          console.log('Service Worker 註冊失敗:', error);
        });
    });
  }
</script>

第四步:準備應用程式圖示

PWA 需要多種尺寸的圖示。以下是我建議的最小圖示清單:

尺寸 用途
72x72 舊版 Android
96x96 舊版 Android
128x128 Chrome Web Store
144x144 Windows 動態磚
152x152 iPad
192x192 Android Chrome
384x384 Android Chrome
512x512 Android 啟動畫面

你可以用一張高解析度的原圖,透過工具自動產生各種尺寸。我通常會用線上工具或是寫一個簡單的腳本:

# 使用 ImageMagick 批次產生圖示
sizes=(72 96 128 144 152 192 384 512)
for size in "${sizes[@]}"; do
  convert original-icon.png -resize ${size}x${size} icon-${size}x${size}.png
done

第五步:離線頁面

當使用者在離線狀態下嘗試存取未快取的頁面時,應該顯示一個友善的離線提示,而不是瀏覽器預設的錯誤畫面。建立一個 offline.html

<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>離線中 - Atlantis Kid</title>
  <style>
    body {
      font-family: 'Inter', sans-serif;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      margin: 0;
      background: #0f172a;
      color: #e2e8f0;
    }
    .container {
      text-align: center;
      padding: 2rem;
    }
    h1 { font-size: 1.5rem; margin-bottom: 1rem; }
    p { color: #94a3b8; }
  </style>
</head>
<body>
  <div class="container">
    <h1>目前沒有網路連線</h1>
    <p>請確認網路連線後重新整理頁面。</p>
  </div>
</body>
</html>

然後修改 Service Worker 的 fetch 事件,在所有快取和網路都失敗時回傳離線頁面:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        return response || fetch(event.request);
      })
      .catch(() => {
        // 如果是頁面請求,回傳離線頁面
        if (event.request.mode === 'navigate') {
          return caches.match('/offline.html');
        }
      })
  );
});

第六步:驗證 PWA 設定

完成以上步驟後,使用 Chrome DevTools 的 Lighthouse 來驗證你的 PWA 設定:

  1. 開啟 Chrome DevTools(F12)
  2. 切換到 Lighthouse 分頁
  3. 勾選 Progressive Web App 類別
  4. 點擊 Analyze page load

Lighthouse 會給你一份完整的檢查報告,告訴你哪些項目通過、哪些需要修正。目標是讓所有 PWA 相關的項目都顯示綠色。

你也可以在 Application 分頁中查看:

  • Manifest:確認 manifest 被正確載入
  • Service Workers:確認 Service Worker 狀態
  • Cache Storage:確認資源被正確快取

部署注意事項

Cloudflare Pages 上的 PWA

如果你跟我一樣使用 Cloudflare Pages 部署,有幾個要注意的地方:

  • Cloudflare Pages 預設就提供 HTTPS,所以不用擔心安全連線的問題
  • 確保 manifest.jsonsw.js 的 MIME type 正確(通常 Cloudflare 會自動處理)
  • 快取策略要考慮 Cloudflare 自身的 CDN 快取,避免雙重快取造成更新延遲

版本更新策略

每次更新網站內容時,記得更新 Service Worker 中的 CACHE_NAME 版本號:

// 更新版本號觸發新的 install 事件
const CACHE_NAME = 'atlantiskid-v2';

這會觸發 Service Worker 的更新流程,清除舊版快取並載入新資源。

結語

PWA 的設定看起來步驟很多,但其實每個步驟都不複雜。對靜態網站來說,基本的 manifest 加上簡單的快取策略就能提供不錯的體驗。最重要的是 Service Worker 的快取策略要根據你的內容特性來選擇——靜態內容用 Cache First,動態內容用 Network First。

完成 PWA 設定後,你的網站就能出現在使用者手機的「新增到主畫面」選項中,提供更接近原生 App 的使用體驗。對獨立開發者來說,這是一個成本極低但效果顯著的優化。