Service Worker与PWA
约 1495 字大约 5 分钟
service-workerpwa
2025-08-01
概述
Service Worker 是运行在浏览器后台的独立线程,充当 Web 应用与网络之间的代理。它可以拦截和缓存网络请求、实现离线访问、推送通知和后台同步等功能,是构建 Progressive Web App(PWA)的核心技术。
Service Worker 生命周期
注册与安装
// main.js — 注册 Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/' // 控制范围
});
console.log('SW registered:', registration.scope);
// 监听更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
// 新版本已激活,提示用户刷新
showUpdateNotification();
}
});
});
} catch (error) {
console.error('SW registration failed:', error);
}
});
}// sw.js — Service Worker
const CACHE_NAME = 'app-v1';
const PRECACHE_ASSETS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
'/offline.html'
];
// Install 事件:预缓存关键资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(PRECACHE_ASSETS))
.then(() => self.skipWaiting()) // 立即激活,不等待
);
});
// Activate 事件:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
}).then(() => self.clients.claim()) // 立即控制所有页面
);
});缓存策略
Cache First(缓存优先)
适用于不常变化的静态资源。
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image' ||
event.request.url.includes('/static/')) {
event.respondWith(
caches.match(event.request)
.then(cached => {
if (cached) return cached; // 缓存命中
return fetch(event.request).then(response => {
const clone = response.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, clone));
return response;
});
})
);
}
});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(() => {
// 网络失败,使用缓存
return caches.match(event.request);
})
);
}
});Stale While Revalidate
兼顾速度和新鲜度:立即返回缓存,后台静默更新。
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then(cache => {
return cache.match(event.request).then(cached => {
// 后台发起网络请求更新缓存
const fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
// 立即返回缓存(如果有),否则等待网络
return cached || fetchPromise;
});
})
);
});综合路由策略
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// HTML 页面:Network First
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.catch(() => caches.match('/offline.html'))
);
return;
}
// API 请求:Network First + 缓存回退
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
return;
}
// 静态资源:Cache First
if (url.pathname.match(/\.(js|css|png|jpg|svg|woff2)$/)) {
event.respondWith(cacheFirst(event.request));
return;
}
// 其他:Stale While Revalidate
event.respondWith(staleWhileRevalidate(event.request));
});Web App Manifest
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "An awesome PWA",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#007bff",
"orientation": "portrait",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/icons/maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"screenshots": [
{
"src": "/screenshots/desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
]
}<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#007bff">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="/icons/icon-192.png">Push Notifications
// 前端:订阅推送
async function subscribePush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
// 发送 subscription 到后端
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
}
// sw.js — 处理推送
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
data: { url: data.url },
actions: [
{ action: 'open', title: '查看' },
{ action: 'dismiss', title: '忽略' }
]
})
);
});
// 处理通知点击
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open' || !event.action) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});Offline Fallback
// 离线页面的优雅降级
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match('/offline.html');
})
);
}
});<!-- offline.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>离线</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: system-ui;
background: #f5f5f5;
}
.offline-msg {
text-align: center;
padding: 2rem;
}
</style>
</head>
<body>
<div class="offline-msg">
<h1>您当前处于离线状态</h1>
<p>请检查网络连接后重试</p>
<button onclick="window.location.reload()">重试</button>
</div>
</body>
</html>Workbox
Google 的 Workbox 库封装了 Service Worker 的常用功能,大幅简化开发。
// 使用 workbox-webpack-plugin
const { GenerateSW } = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new GenerateSW({
clientsClaim: true,
skipWaiting: true,
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 100, maxAgeSeconds: 3600 }
}
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: { maxEntries: 200, maxAgeSeconds: 30 * 24 * 3600 }
}
}
]
})
]
};// 直接使用 Workbox 库
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// 预缓存(由构建工具生成清单)
precacheAndRoute(self.__WB_MANIFEST);
// API 请求
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
plugins: [new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 300 })]
})
);
// 字体文件
registerRoute(
({ request }) => request.destination === 'font',
new CacheFirst({
cacheName: 'font-cache',
plugins: [new ExpirationPlugin({ maxEntries: 10, maxAgeSeconds: 365 * 24 * 3600 })]
})
);PWA 安装体验
// 监听安装提示
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); // 阻止默认的安装横幅
deferredPrompt = e;
showInstallButton(); // 显示自定义安装按钮
});
document.getElementById('install-btn').addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`User ${outcome === 'accepted' ? 'accepted' : 'dismissed'} install`);
deferredPrompt = null;
});
// 检测是否已安装
window.addEventListener('appinstalled', () => {
console.log('PWA installed successfully');
hideInstallButton();
});总结
Service Worker 是 PWA 的核心技术,通过拦截网络请求和管理缓存实现离线访问和性能优化。选择合适的缓存策略(Cache First、Network First、Stale While Revalidate)是关键:静态资源用缓存优先,API 数据用网络优先,通用资源用过期刷新。结合 Web App Manifest 实现安装体验,Push API 实现推送通知。Workbox 库大幅简化了 Service Worker 的开发工作,是生产级 PWA 的推荐选择。
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于