progressive-web-app
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseProgressive Web Apps (PWAs)
Progressive Web Apps(PWA)
Overview
概述
A Progressive Web App is a web application that uses modern browser capabilities to deliver a fast, reliable, and installable experience — even on unreliable networks. The three required pillars are:
- HTTPS — Required in production for service workers to register (localhost is exempt for development).
- Web App Manifest () — Makes the app installable and defines its appearance on device home screens.
manifest.json - Service Worker () — A background script that intercepts network requests, manages caches, and enables offline functionality.
sw.js
渐进式Web应用(PWA)是一种利用现代浏览器功能的Web应用,可提供快速、可靠且可安装的体验——即使在网络不稳定的环境下也能正常使用。其三大核心要素为:
- HTTPS —— 生产环境下注册service worker必须使用HTTPS(开发环境下localhost除外)。
- Web App Manifest()—— 让应用可安装,并定义其在设备主屏幕上的显示样式。
manifest.json - Service Worker()—— 一个后台脚本,用于拦截网络请求、管理缓存并实现离线功能。
sw.js
When to Use This Skill
何时使用此技能
- Use when the user wants their web app to work offline or on unreliable networks.
- Use when building a mobile-first web project where users should be able to install the app to their home screen.
- Use when the user asks about caching strategies, service workers, or improving web app performance and resilience.
- Use when the user mentions Workbox, web app manifests, background sync, or push notifications for the web.
- Use when the user asks "can my website be installed like an app?" or "how do I make my site work offline?" — even if they don't use the word PWA.
- 当用户希望其Web应用支持离线使用或在网络不稳定的环境下运行时。
- 当构建移动端优先的Web项目,且希望用户能够将应用安装到主屏幕时。
- 当用户询问缓存策略、service worker,或希望提升Web应用的性能和韧性时。
- 当用户提及Workbox、Web App Manifest、后台同步或Web推送通知时。
- 当用户询问“我的网站能否像应用一样安装?”或“如何让我的网站支持离线使用?”时——即使他们没有提到PWA这个词。
Deliverables Checklist
交付物检查清单
Every PWA implementation must include these files at minimum:
- — Links manifest, registers service worker
index.html - — Full app metadata and icon set
manifest.json - — Service worker with install, activate, and fetch handlers
sw.js - — Main app logic with SW registration and install prompt handling
app.js - — Fallback page shown when navigation fails offline (required — missing file will cause install to fail)
offline.html
每个PWA实现至少必须包含以下文件:
- —— 关联manifest,注册service worker
index.html - —— 完整的应用元数据和图标集
manifest.json - —— 包含install、activate和fetch处理器的service worker
sw.js - —— 包含SW注册和安装提示处理的主应用逻辑
app.js - —— 离线导航失败时显示的备用页面(必填——缺少此文件会导致安装失败)
offline.html
Step 1: Web App Manifest (manifest.json
)
manifest.json步骤1:Web App Manifest(manifest.json
)
manifest.jsonDefines how the app appears when installed. Must be linked from via .
<head><link rel="manifest">json
{
"name": "My Awesome PWA",
"short_name": "MyPWA",
"description": "A fast, offline-capable Progressive Web App.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#0055ff",
"icons": [
{
"src": "/assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/assets/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
]
}Key fields:
- :
displayhides browser UI;standaloneshows minimal controls;minimal-uiis standard tab.browser - on icons enables adaptive icons on Android (safe zone matters — keep content in center 80%).
purpose: "maskable" - is optional but required for Chrome's enhanced install dialog on desktop.
screenshots
定义应用安装后的显示样式。必须通过中的进行关联。
<head><link rel="manifest">json
{
"name": "My Awesome PWA",
"short_name": "MyPWA",
"description": "A fast, offline-capable Progressive Web App.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#0055ff",
"icons": [
{
"src": "/assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/assets/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
]
}关键字段:
- :
display会隐藏浏览器UI;standalone显示极简控制栏;minimal-ui为标准标签页模式。browser - 图标上的支持Android自适应图标(注意安全区域——内容需保持在中心80%范围内)。
purpose: "maskable" - 为可选字段,但桌面端Chrome的增强安装对话框需要此字段。
screenshots
Step 2: HTML Shell (index.html
)
index.html步骤2:HTML外壳(index.html
)
index.htmlhtml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Awesome PWA</title>
<!-- PWA manifest -->
<link rel="manifest" href="/manifest.json">
<!-- Theme color for browser chrome -->
<meta name="theme-color" content="#0055ff">
<!-- iOS-specific (Safari doesn't fully use manifest) -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="MyPWA">
<link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png">
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div id="app">
<header><h1>My PWA</h1></header>
<main id="content">Loading...</main>
<!-- Optional: install button, hidden by default -->
<button id="install-btn" hidden>Install App</button>
</div>
<script src="/app.js"></script>
</body>
</html>html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Awesome PWA</title>
<!-- PWA manifest -->
<link rel="manifest" href="/manifest.json">
<!-- 浏览器主题色 -->
<meta name="theme-color" content="#0055ff">
<!-- iOS专属配置(Safari未完全支持manifest) -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="MyPWA">
<link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png">
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div id="app">
<header><h1>My PWA</h1></header>
<main id="content">Loading...</main>
<!-- 可选:安装按钮,默认隐藏 -->
<button id="install-btn" hidden>Install App</button>
</div>
<script src="/app.js"></script>
</body>
</html>Step 3: Service Worker Registration & Install Prompt (app.js
)
app.js步骤3:Service Worker注册与安装提示(app.js
)
app.jsjavascript
// ─── Service Worker Registration ───────────────────────────────────────────
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('[App] SW registered, scope:', registration.scope);
} catch (err) {
console.error('[App] SW registration failed:', err);
}
});
}
// ─── Install Prompt (Add to Home Screen) ───────────────────────────────────
let deferredPrompt;
const installBtn = document.getElementById('install-btn'); // may be null if omitted
// Capture the browser's install prompt — it fires before the browser's own UI
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); // Stop automatic mini-infobar on mobile
deferredPrompt = e;
if (installBtn) installBtn.hidden = false; // Show your custom install button
});
if (installBtn) {
installBtn.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('[App] Install outcome:', outcome);
deferredPrompt = null;
installBtn.hidden = true;
});
}
// Fires when the app is installed (via browser or your button)
window.addEventListener('appinstalled', () => {
console.log('[App] PWA installed successfully');
installBtn.hidden = true;
});javascript
// ─── Service Worker 注册 ───────────────────────────────────────────
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('[App] SW registered, scope:', registration.scope);
} catch (err) {
console.error('[App] SW registration failed:', err);
}
});
}
// ─── 安装提示(添加到主屏幕) ───────────────────────────────────
let deferredPrompt;
const installBtn = document.getElementById('install-btn'); // 若省略则可能为null
// 捕获浏览器的安装提示——该事件在浏览器自身UI弹出前触发
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); // 阻止移动端自动显示迷你信息栏
deferredPrompt = e;
if (installBtn) installBtn.hidden = false; // 显示自定义安装按钮
});
if (installBtn) {
installBtn.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('[App] Install outcome:', outcome);
deferredPrompt = null;
installBtn.hidden = true;
});
}
// 应用安装完成时触发(通过浏览器或自定义按钮安装)
window.addEventListener('appinstalled', () => {
console.log('[App] PWA installed successfully');
installBtn.hidden = true;
});Step 4: Service Worker (sw.js
)
sw.js步骤4:Service Worker(sw.js
)
sw.jsCache Versioning (critical — always increment on deploy)
缓存版本控制(至关重要——部署时务必递增版本号)
javascript
const CACHE_VERSION = 'v1';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`;
// Files to pre-cache during install (the "App Shell")
const APP_SHELL = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/assets/icons/icon-192x192.png',
'/offline.html', // Fallback page shown when network is unavailable
];javascript
const CACHE_VERSION = 'v1';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `dynamic-${CACHE_VERSION}`;
// 安装期间预缓存的文件(“应用外壳”)
const APP_SHELL = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/assets/icons/icon-192x192.png',
'/offline.html', // 网络不可用时显示的备用页面
];Install — Pre-cache the App Shell
安装——预缓存应用外壳
javascript
self.addEventListener('install', (event) => {
console.log('[SW] Installing...');
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => {
console.log('[SW] Pre-caching app shell');
return cache.addAll(APP_SHELL);
})
);
// Activate immediately without waiting for old SW to die
self.skipWaiting();
});javascript
self.addEventListener('install', (event) => {
console.log('[SW] Installing...');
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => {
console.log('[SW] Pre-caching app shell');
return cache.addAll(APP_SHELL);
})
);
// 立即激活,无需等待旧SW终止
self.skipWaiting();
});Activate — Clean Up Old Caches
激活——清理旧缓存
javascript
self.addEventListener('activate', (event) => {
console.log('[SW] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== STATIC_CACHE && name !== DYNAMIC_CACHE)
.map((name) => {
console.log('[SW] Deleting old cache:', name);
return caches.delete(name);
})
);
})
);
// Take control of all pages immediately
self.clients.claim();
});javascript
self.addEventListener('activate', (event) => {
console.log('[SW] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== STATIC_CACHE && name !== DYNAMIC_CACHE)
.map((name) => {
console.log('[SW] Deleting old cache:', name);
return caches.delete(name);
})
);
})
);
// 立即接管所有页面
self.clients.claim();
});Fetch — Caching Strategies
Fetch——缓存策略
Choose the right strategy per resource type:
javascript
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Only handle GET requests from our own origin
if (request.method !== 'GET' || url.origin !== location.origin) return;
// Strategy A: Cache-First (for static assets — fast, tolerates stale)
if (url.pathname.match(/\.(css|js|png|jpg|svg|woff2)$/)) {
event.respondWith(cacheFirst(request));
return;
}
// Strategy B: Network-First (for HTML pages — fresh, falls back to cache)
if (request.headers.get('Accept')?.includes('text/html')) {
event.respondWith(networkFirst(request));
return;
}
// Strategy C: Stale-While-Revalidate (for API data — fast and eventually fresh)
if (url.pathname.startsWith('/api/')) {
event.respondWith(staleWhileRevalidate(request));
return;
}
});
// ─── Strategy Implementations ──────────────────────────────────────────────
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
const cache = await caches.open(STATIC_CACHE);
cache.put(request, response.clone());
return response;
} catch {
// Nothing useful to fall back to for assets
return new Response('Asset unavailable offline', { status: 503 });
}
}
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, response.clone());
return response;
} catch {
const cached = await caches.match(request);
return cached || caches.match('/offline.html');
}
}
async function staleWhileRevalidate(request) {
const cache = await caches.open(DYNAMIC_CACHE);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}根据资源类型选择合适的策略:
javascript
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// 仅处理同源的GET请求
if (request.method !== 'GET' || url.origin !== location.origin) return;
// 策略A:缓存优先(适用于静态资源——速度快,可接受过期内容)
if (url.pathname.match(/\.(css|js|png|jpg|svg|woff2)$/)) {
event.respondWith(cacheFirst(request));
return;
}
// 策略B:网络优先(适用于HTML页面——内容新鲜,缓存作为备用)
if (request.headers.get('Accept')?.includes('text/html')) {
event.respondWith(networkFirst(request));
return;
}
// 策略C:缓存回源(适用于API数据——速度快且最终会更新为最新内容)
if (url.pathname.startsWith('/api/')) {
event.respondWith(staleWhileRevalidate(request));
return;
}
});
// ─── 策略实现 ──────────────────────────────────────────────
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
const cache = await caches.open(STATIC_CACHE);
cache.put(request, response.clone());
return response;
} catch {
// 资源离线时无可用备用内容
return new Response('Asset unavailable offline', { status: 503 });
}
}
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, response.clone());
return response;
} catch {
const cached = await caches.match(request);
return cached || caches.match('/offline.html');
}
}
async function staleWhileRevalidate(request) {
const cache = await caches.open(DYNAMIC_CACHE);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}Edge Cases & Platform Notes
边缘情况与平台说明
iOS / Safari Quirks
iOS / Safari 特性
- Safari supports manifests and service workers but does not support — users must install via the Share → "Add to Home Screen" menu manually.
beforeinstallprompt - Use the meta tags (shown in
apple-mobile-web-app-*above) for proper iOS integration.index.html - Safari may clear service worker caches after ~7 days of inactivity (Intelligent Tracking Prevention).
- Safari支持manifest和service worker,但不支持——用户必须通过分享→“添加到主屏幕”菜单手动安装。
beforeinstallprompt - 使用元标签(如上述
apple-mobile-web-app-*中所示)实现iOS的正确集成。index.html - Safari可能会在应用闲置约7天后清除service worker缓存(智能跟踪预防机制)。
HTTPS Requirement
HTTPS要求
- Service workers only register on origins.
https://is the only exception for development.http://localhost - Use a tool like or
mkcertif you need HTTPS locally with a custom hostname.ngrok
- Service worker仅能在源上注册。开发环境下
https://是唯一例外。http://localhost - 如果需要在本地自定义主机名上使用HTTPS,可以使用或
mkcert等工具。ngrok
Cache-Busting on Deploy
部署时的缓存更新
- Always increment in
CACHE_VERSIONwhen deploying new assets. This ensures activate clears old caches and users get fresh files.sw.js - A common pattern is to inject the version automatically via your build tool (e.g., Vite, Webpack).
- 部署新资源时,务必递增中的
sw.js。这样可以确保激活阶段清理旧缓存,用户获取到最新文件。CACHE_VERSION - 常见做法是通过构建工具(如Vite、Webpack)自动注入版本号。
Opaque Responses (cross-origin requests)
不透明响应(跨域请求)
- Requests to external origins (e.g., CDN fonts, third-party APIs) return "opaque" responses that cannot be inspected. Cache them with caution — a failed opaque response still gets a status.
200 - Prefer for cross-origin resources, or use a library like Workbox which handles this safely.
staleWhileRevalidate
- 对外部源的请求(如CDN字体、第三方API)会返回“不透明”响应,无法被检查。缓存此类响应时需谨慎——失败的不透明响应仍会返回状态码。
200 - 对于跨域资源,优先使用策略,或使用Workbox等库来安全处理此类情况。
staleWhileRevalidate
Workbox (Optional: Production Shortcut)
Workbox(可选:生产环境快捷工具)
For production apps, consider Workbox (Google's PWA library) instead of hand-rolling strategies. It handles edge cases, cache expiry, and versioning automatically.
javascript
// With Workbox (via CDN for simplicity — use npm + bundler in production)
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');
const { registerRoute } = workbox.routing;
const { CacheFirst, NetworkFirst, StaleWhileRevalidate } = workbox.strategies;
const { precacheAndRoute } = workbox.precaching;
precacheAndRoute(self.__WB_MANIFEST || []); // Injected by build plugin
registerRoute(({ request }) => request.destination === 'image', new CacheFirst());
registerRoute(({ request }) => request.mode === 'navigate', new NetworkFirst());
registerRoute(({ request }) => request.destination === 'script', new StaleWhileRevalidate());对于生产环境应用,考虑使用Workbox(谷歌的PWA库)替代手动编写策略。它会自动处理边缘情况、缓存过期和版本控制。
javascript
// 使用Workbox(为简化使用CDN——生产环境建议使用npm + 打包工具)
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');
const { registerRoute } = workbox.routing;
const { CacheFirst, NetworkFirst, StaleWhileRevalidate } = workbox.strategies;
const { precacheAndRoute } = workbox.precaching;
precacheAndRoute(self.__WB_MANIFEST || []); // 由构建插件注入
registerRoute(({ request }) => request.destination === 'image', new CacheFirst());
registerRoute(({ request }) => request.mode === 'navigate', new NetworkFirst());
registerRoute(({ request }) => request.destination === 'script', new StaleWhileRevalidate());Checklist Before Shipping
上线前检查清单
- Site is served over HTTPS
- has
manifest.json,name,short_name,start_url,display(192 + 512)icons - Icons have
purpose: "any maskable" - registers without errors in DevTools → Application → Service Workers
sw.js - App shell loads from cache when network is throttled to "Offline" in DevTools
- fallback is cached and served when navigation fails offline
offline.html - Lighthouse PWA audit passes (Chrome DevTools → Lighthouse tab)
- Tested on iOS Safari (manual install flow) and Android Chrome (install prompt)
- 网站通过HTTPS提供服务
- 包含
manifest.json、name、short_name、start_url、图标(192×192和512×512)display - 图标设置了
purpose: "any maskable" - 在DevTools→Application→Service Workers中注册无错误
sw.js - 当网络被限制为“离线”时,应用外壳可从缓存加载
- 备用页面已被缓存,且在离线导航失败时正常显示
offline.html - Lighthouse PWA审核通过(Chrome DevTools→Lighthouse标签)
- 已在iOS Safari(手动安装流程)和Android Chrome(安装提示)上进行测试