前言
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 開啟時隱藏瀏覽器的網址列,看起來像原生 Apptheme_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 有幾種常見的快取策略:
- Cache First(快取優先):先查快取,沒有才走網路。適合靜態資源。
- Network First(網路優先):先走網路,失敗才用快取。適合需要即時性的內容。
- 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 設定:
- 開啟 Chrome DevTools(F12)
- 切換到 Lighthouse 分頁
- 勾選 Progressive Web App 類別
- 點擊 Analyze page load
Lighthouse 會給你一份完整的檢查報告,告訴你哪些項目通過、哪些需要修正。目標是讓所有 PWA 相關的項目都顯示綠色。
你也可以在 Application 分頁中查看:
- Manifest:確認 manifest 被正確載入
- Service Workers:確認 Service Worker 狀態
- Cache Storage:確認資源被正確快取
部署注意事項
Cloudflare Pages 上的 PWA
如果你跟我一樣使用 Cloudflare Pages 部署,有幾個要注意的地方:
- Cloudflare Pages 預設就提供 HTTPS,所以不用擔心安全連線的問題
- 確保
manifest.json和sw.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 的使用體驗。對獨立開發者來說,這是一個成本極低但效果顯著的優化。