Service Worker 与 PWA
Service Worker 的工作原理、生命周期及 PWA 应用场景
问题
什么是 Service Worker?它有哪些应用场景?如何用它构建 PWA?
解答
什么是 Service Worker
Service Worker 是运行在浏览器后台的脚本,独立于网页,充当浏览器与网络之间的代理。它可以拦截网络请求、缓存资源、实现离线访问。
特点:
- 运行在独立线程,不阻塞主线程
- 无法直接访问 DOM
- 只能在 HTTPS 环境下运行(localhost 除外)
- 完全异步,基于 Promise
生命周期
// sw.js - Service Worker 文件
// 1. 安装阶段:缓存静态资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html'
]);
})
);
// 跳过等待,立即激活
self.skipWaiting();
});
// 2. 激活阶段:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== 'v1')
.map((name) => caches.delete(name))
);
})
);
// 立即控制所有页面
self.clients.claim();
});
// 3. 拦截请求
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// 缓存命中,返回缓存
if (response) {
return response;
}
// 缓存未命中,发起网络请求
return fetch(event.request);
})
);
});
注册 Service Worker
// main.js - 主页面注册
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('SW 注册成功,作用域:', registration.scope);
} catch (error) {
console.log('SW 注册失败:', error);
}
});
}
缓存策略
// 策略1:缓存优先(适合静态资源)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
});
// 策略2:网络优先(适合 API 请求)
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// 请求成功,更新缓存
const clone = response.clone();
caches.open('v1').then((cache) => {
cache.put(event.request, clone);
});
return response;
})
.catch(() => {
// 网络失败,返回缓存
return caches.match(event.request);
})
);
});
// 策略3:Stale While Revalidate(先返回缓存,后台更新)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('v1').then((cache) => {
return cache.match(event.request).then((cached) => {
const fetchPromise = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
return cached || fetchPromise;
});
})
);
});
PWA 完整示例
// manifest.json - PWA 配置文件
{
"name": "My PWA App",
"short_name": "PWA",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#007bff",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- PWA 必需的 meta 标签 -->
<meta name="theme-color" content="#007bff">
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/icon-192.png">
<title>PWA Demo</title>
</head>
<body>
<h1>PWA 示例</h1>
<script src="/main.js"></script>
</body>
</html>
推送通知
// 请求通知权限并订阅
async function subscribePush() {
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'YOUR_VAPID_PUBLIC_KEY'
});
// 发送订阅信息到服务器
await fetch('/api/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: { 'Content-Type': 'application/json' }
});
}
// sw.js - 接收推送
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icon-192.png'
})
);
});
// 点击通知
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow('/')
);
});
后台同步
// 主页面:注册同步任务
async function syncData() {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-data');
}
// sw.js - 处理同步
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-data') {
event.waitUntil(
// 从 IndexedDB 读取待同步数据,发送到服务器
syncPendingData()
);
}
});
关键点
- 独立线程:Service Worker 运行在独立线程,不能访问 DOM,通过 postMessage 与页面通信
- 生命周期:install → waiting → activate → fetch,更新时新 SW 会等待旧 SW 释放控制权
- HTTPS 必需:出于安全考虑,只能在 HTTPS 或 localhost 下运行
- 缓存策略:根据资源类型选择缓存优先、网络优先或 Stale While Revalidate
- PWA 三要素:HTTPS + manifest.json + Service Worker,满足后可安装到桌面
目录