PWA 实战:从零打造离线可用的 Web 应用
什么是 PWA?
PWA(Progressive Web App)是一种结合了 Web 和原生 App 优势的技术方案。它让 Web 应用能够:
- 📱 添加到桌面:像原生 App 一样安装
- 🔌 离线可用:没有网络也能访问
- 🔔 推送通知:接收后台消息推送
- ⚡ 秒开体验:缓存策略优化加载速度
核心三要素:
manifest.json- 应用配置文件Service Worker- 离线与缓存核心- HTTPS - 安全传输(开发环境 localhost 除外)
manifest.json 配置
这是 PWA 的「身份证」,告诉浏览器如何展示你的应用:
{
"name": "戒色打卡",
"short_name": "打卡",
"description": "自律给我自由 - 习惯追踪器",
"theme_color": "#6366f1",
"background_color": "#030712",
"display": "standalone",
"orientation": "portrait",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
关键配置说明
display: "standalone"- 隐藏浏览器地址栏,像原生 Apptheme_color- 顶部状态栏颜色purpose: "maskable"- 支持 Android 自适应图标
⚠️ 图标要求:
- 至少提供 192x192 和 512x512 两个尺寸
- 推荐 PNG 格式,支持透明背景
- maskable 图标需要安全区域设计(中心 80%)
Service Worker 核心
Service Worker 是 PWA 的「大脑」,运行在独立线程,拦截所有网络请求。
1. 注册 Service Worker
在 HTML 中注册:
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('✅ Service Worker 注册成功');
})
.catch((error) => {
console.log('❌ 注册失败:', error);
});
});
}
</script>
2. Service Worker 生命周期
// 安装事件 - 预缓存静态资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/manifest.json',
'/favicon.svg'
]);
})
);
self.skipWaiting(); // 跳过等待,立即激活
});
// 激活事件 - 清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
self.clients.claim(); // 立即控制所有页面
});
缓存策略详解
选择正确的缓存策略是 PWA 性能的关键:
策略一:缓存优先(Cache First)
适用于不常变化的静态资源:
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
});
策略二:网络优先(Network First)
适用于需要实时性的 API 请求:
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then((response) => {
// 缓存成功响应
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, clone);
});
return response;
})
.catch(() => caches.match(event.request)) // 离线用缓存
);
}
});
策略三:Stale While Revalidate
先返回缓存,后台更新:
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
const fetchPromise = fetch(event.request).then((response) => {
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone());
});
return response;
});
return cached || fetchPromise;
})
);
});
✅ 最佳实践:
- 静态资源(JS/CSS/图片)→ 缓存优先
- API 数据 → 网络优先 + 离线缓存
- HTML 页面 → Stale While Revalidate
推送通知
PWA 支持后台推送,即使应用未打开也能收到通知:
// 监听推送事件
self.addEventListener('push', (event) => {
const data = event.data?.json() || {};
event.waitUntil(
self.registration.showNotification(data.title || '新消息', {
body: data.body || '您有一条新通知',
icon: '/icon-192.png',
badge: '/favicon-32x32.png',
tag: 'notification-' + Date.now(),
vibrate: [200, 100, 200],
data: { url: data.url || '/' }
})
);
});
// 点击通知跳转
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
部署与测试
1. 本地测试
Chrome DevTools 提供了完整的 PWA 调试工具:
- 打开 DevTools → Application 面板
- 查看 Manifest、Service Workers、Cache Storage
- 使用 Lighthouse 跑 PWA 审计
2. 常见问题
⚠️ Service Worker 不更新?
修改 CACHE_NAME 版本号,或者:
- DevTools → Application → Service Workers → Update
- 勾选「Update on reload」开发模式
- 清除浏览器缓存后刷新
3. Lighthouse 审计
运行 Lighthouse 审计确保 PWA 标准:
- ✅ installable - 可安装
- ✅ offline - 离线可用
- ✅ splash-screen - 启动画面
- ✅ themed-omnibox - 地址栏主题色
总结
PWA 让 Web 应用拥有了原生 App 的体验,核心是:
- manifest.json 定义应用元数据
- Service Worker 实现离线与缓存
- 缓存策略 根据资源类型选择
- 推送通知 提升用户粘性
🎉 在我的项目 no-fap-tracker-web 中,已完整实现 PWA 支持,欢迎参考源码!
参考资料: