前言
Progressive Web App(PWA)已成为提升 Web 应用体验的关键技术,而 Service Worker 是其核心引擎。它能拦截网络请求、管理缓存、实现离线访问,甚至支持后台同步与推送通知。然而,Service Worker 的生命周期复杂,缓存策略选择不当容易导致资源过期或版本混乱。本文将从零开始,带你构建一个完整的 PWA,涵盖注册、安装、激活、拦截请求、缓存更新等全流程,并分享生产环境下的最佳实践。
1. 项目初始化
创建一个简单的 HTML 页面,包含一个图标和清单文件。
mkdir pwa-demo && cd pwa-demo
创建 index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PWA 演示</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<h1>Hello, PWA!</h1>
<img src="icon.png" alt="icon" width="100">
<script src="app.js"></script>
</body>
</html>
创建 manifest.json:
{
"name": "PWA Demo",
"short_name": "PWADemo",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
> 注意:图标文件需准备 192×192 和 512×512 的 PNG 图片,可临时用占位图代替。
2. 注册 Service Worker
创建 sw.js 文件,这是 Service Worker 的脚本。在 app.js 中注册它。
// app.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW 注册成功:', registration.scope);
})
.catch(error => {
console.log('SW 注册失败:', error);
});
} else {
console.log('当前浏览器不支持 Service Worker');
}
> 注意:sw.js 必须放在站点根目录(或同域下),且 scope 默认为脚本所在目录。建议放在根目录以控制整个站点。
3. 生命周期与核心事件
Service Worker 有三个关键事件:install、activate 和 fetch。
3.1 install 事件:预缓存静态资源
在 sw.js 中监听 install 事件,通常在此处预缓存应用外壳(App Shell)所需的静态资源。
// sw.js
const CACHE_NAME = 'pwa-demo-v1';
const urlsToCache = [
'/',
'/index.html',
'/icon.png',
'/app.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('缓存已打开');
return cache.addAll(urlsToCache);
})
.catch(err => console.error('缓存失败:', err))
);
});
> 注意:cache.addAll() 是原子操作,任一请求失败则整个缓存失败。建议先单独缓存关键资源,或使用 try-catch。
3.2 activate 事件:清理旧缓存
当新 Service Worker 激活时,应删除旧版本的缓存,避免磁盘空间浪费。
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
3.3 fetch 事件:拦截请求并返回缓存
这是核心部分,决定缓存策略。常用策略有:
- Cache First:优先从缓存获取,失败则请求网络。
- Network First:优先网络,失败则回退缓存。
- Stale While Revalidate:同时返回缓存和网络更新。
以下实现 Cache First 策略:
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response; // 缓存命中
}
return fetch(event.request); // 请求网络
})
.catch(() => {
// 离线且无缓存时返回默认页
return caches.match('/offline.html');
})
);
});
> 注意:对于 API 请求(如 /api/data),通常使用 Network First 或 Stale While Revalidate 以保证数据新鲜。可针对不同 URL 模式使用不同策略。
4. 更新 Service Worker
当 sw.js 内容变化(字节级别),浏览器会检测到并触发更新。但新 SW 不会立即接管,需等待旧 SW 控制的所有页面关闭。可手动调用 self.skipWaiting() 和 clients.claim() 实现立即更新。
// 在 install 事件中
self.addEventListener('install', event => {
self.skipWaiting(); // 立即激活新 SW
// ... 缓存逻辑
});
// 在 activate 事件中
self.addEventListener('activate', event => {
clients.claim(); // 立即控制所有客户端
// ... 清理缓存
});
> 最佳实践:通常不推荐立即激活,而是通知用户有新版本,让用户手动刷新。可通过监听 controllerchange 事件实现。
5. 后台同步与推送通知
5.1 后台同步
当用户离线时提交的表单,可在网络恢复后自动同步。
// 注册同步事件
navigator.serviceWorker.ready.then(registration => {
registration.sync.register('sync-form-data');
});
// sw.js 中监听
self.addEventListener('sync', event => {
if (event.tag === 'sync-form-data') {
event.waitUntil(syncFormData());
}
});
async function syncFormData() {
// 从 IndexedDB 读取待同步数据并发送请求
const data = await getPendingData();
await fetch('/api/submit', { method: 'POST', body: data });
}
5.2 推送通知
需要服务端配合,这里只展示客户端接收。
// 请求权限
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
// 订阅推送
}
});
// sw.js 中监听推送事件
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/icon.png'
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
6. 常见陷阱与调试
- HTTPS 要求:Service Worker 只能在 HTTPS 或 localhost 下工作。
- 作用域限制:
sw.js只能控制其所在目录及子目录下的页面。 - 缓存更新:修改
sw.js后浏览器可能不更新,需在开发者工具中勾选“Update on reload”。 - 调试工具:Chrome DevTools 的 Application 面板可查看 SW 状态、缓存内容及清除缓存。
7. 完整示例代码
以下是整合后的 sw.js:
const CACHE_NAME = 'pwa-demo-v2';
const urlsToCache = [
'/',
'/index.html',
'/icon.png',
'/app.js',
'/offline.html'
];
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('activate', event => {
clients.claim();
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
.catch(() => caches.match('/offline.html'))
);
});
总结
通过本文,你已掌握 Service Worker 的核心概念与实现:
- 注册与生命周期管理
- 缓存策略(Cache First、Network First 等)
- 更新机制与立即激活技巧
- 后台同步与推送通知入门
- 常见陷阱与调试方法
下一步,可以探索 Workbox 库简化 Service Worker 编写,或结合 IndexedDB 实现更复杂的离线数据管理。PWA 的未来还包括 Web 包、流式安装等新特性,值得持续关注。
希望本文能为你的 PWA 实践提供清晰指引。如有疑问,欢迎在评论区交流。