Service Worker 与 PWA:离线体验与推送通知
Service Worker 与 PWA 简介
渐进式 Web 应用(Progressive Web App,简称 PWA)是使用现代 Web 技术构建的、能够提供类似原生应用体验的网站。其核心依赖于 Service Worker——一个在浏览器后台独立运行的脚本,它赋予了网页离线访问、消息推送、后台同步等“超能力”。本教程将带你从零开始理解 Service Worker 的工作原理,并亲手实现一个具备离线缓存和推送通知功能的 PWA。
先决条件
- 基础的 HTML、CSS 与 JavaScript 知识
- 一个支持 HTTPS 的本地开发环境(Service Worker 强制要求安全上下文,
localhost同样适用) - 现代浏览器(如 Chrome、Edge、Firefox)
Service Worker 核心概念
什么是 Service Worker?
Service Worker 是浏览器与网络之间的可编程代理。它独立于网页运行,即使页面关闭,它仍然可以在后台激活。它不能直接操作 DOM,但可以通过 postMessage 与页面通信。
主要特性:
- 事件驱动:完全基于 Promise,通过
install、activate、fetch等事件响应操作。 - 生命周期独立:安装后长期存在,浏览器会在空闲时自动更新。
- 作用域限制:一个 Service Worker 只能控制其所在路径及子路径下的页面。
生命周期
Service Worker 从注册到控制页面,经历以下阶段:
- 注册(Registration):页面调用
navigator.serviceWorker.register()请求安装。 - 安装(Installing):触发
install事件,通常在此阶段预缓存关键资源。 - 等待激活(Waiting):如果有旧版 Service Worker 仍在运行,新版本会进入等待状态,直到旧版本不再控制任何页面。
- 激活(Activating):触发
activate事件,常用来清理旧缓存。 - 已激活(Activated):此刻起 Service Worker 可以拦截网络请求、响应
fetch事件,并发送推送通知。
注册第一个 Service Worker
在 HTML 页面中使用以下脚本注册 Service Worker 文件(通常命名为 sw.js):
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker 注册成功,作用域:', registration.scope);
})
.catch(err => {
console.error('Service Worker 注册失败:', err);
});
});
}
注意:
/sw.js位于项目根目录,这样它就能控制整个站点的请求。
实现离线体验:缓存策略
PWA 最吸引人的特性之一就是离线可用。通过 Service Worker 拦截请求并返回缓存资源,用户可以断网时继续浏览内容。
预缓存关键资源(install 事件)
在 Service Worker 文件中监听 install 事件,提前缓存 App Shell(应用的骨架资源)。
const CACHE_NAME = 'my-pwa-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('打开缓存');
return cache.addAll(urlsToCache);
})
);
});
event.waitUntil() 确保安装完成前不会终止 Service Worker。如果任何文件缓存失败,则整个安装过程失败。
拦截请求并返回缓存(fetch 事件)
使用“缓存优先,网络回退”策略:优先从缓存中响应,若缓存未命中则从网络获取,同时把新响应存入缓存。
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
// 有缓存直接返回,否则发起网络请求
return cachedResponse || fetch(event.request).then(response => {
// 检查响应的有效性
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应,因为响应流只能读取一次
const responseToCache = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
更新缓存与清理旧版(activate 事件)
当 Service Worker 更新时,旧缓存应该被清除,避免占用存储空间。
const expectedCaches = [CACHE_NAME]; // 当前期待的缓存名称列表
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (!expectedCaches.includes(cacheName)) {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
修改 CACHE_NAME 即可触发新的安装并清除旧缓存。
添加推送通知
推送通知需要服务端的配合,但我们可以先从浏览器端实现订阅与显示逻辑。
获取用户权限并订阅
在页面代码中,获取 PushManager 订阅并发送到服务器。
async function subscribeUserToPush() {
const registration = await navigator.serviceWorker.ready;
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.warn('用户不允许通知');
return;
}
// 需要服务器提供公钥,这里示例直接硬编码(实际应从后端获取)
const applicationServerKey = urlBase64ToUint8Array('你的公钥字符串');
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey
});
console.log('订阅对象:', JSON.stringify(subscription));
// 将此 subscription 对象发送到你的服务器,用于后续推送
await fetch('/save-subscription', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
}
// 工具函数:将 Base64 公钥字符串转为 Uint8Array(VAPID 密钥)
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
在 Service Worker 中监听推送事件
在 sw.js 中添加 push 和 notificationclick 事件监听。
self.addEventListener('push', event => {
let data = {};
if (event.data) {
data = event.data.json();
}
const options = {
body: data.body || '你有一条新消息',
icon: '/images/notification-icon.png',
badge: '/images/badge-icon.png',
data: {
url: data.url || '/' // 点击通知后打开的地址
}
};
event.waitUntil(
self.registration.showNotification(data.title || '新通知', options)
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
const targetUrl = event.notification.data.url;
event.waitUntil(
clients.matchAll({ type: 'window' }).then(windowClients => {
// 如果已有打开的页面且匹配目标 URL,则聚焦;否则打开新窗口
for (let client of windowClients) {
if (client.url === targetUrl && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow(targetUrl);
}
})
);
});
生成 VAPID 密钥
推送通知需要 VAPID 密钥对(公钥用于前端订阅,私钥保存在服务器端用于签名)。你可以使用 web-push 库生成:
npx web-push generate-vapid-keys
输出示例:
Public Key: BEl62iU_YhXeJSxu...
Private Key: cbKJZkfEoDKc...
公钥填入前端订阅代码,私钥保存到后端环境变量。
让 PWA 可安装
为了让用户将站点添加到主屏幕,需要提供一个包含必要图标的 Web App Manifest。
创建 manifest.json
{
"name": "我的 PWA 应用",
"short_name": "PWA",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3f51b5",
"icons": [
{
"src": "/images/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
在 HTML 中引入:
<link rel="manifest" href="/manifest.json">
当用户满足 Chrome 的安装启发式条件(如频繁访问、Service Worker 激活等)时,浏览器会自动提示安装。
调试与常见问题
- 使用 Chrome DevTools:Application > Service Workers 面板可以查看状态、强制更新、跳过等待、模拟离线等。
- 缓存检查:Application > Cache Storage 中可查看所有缓存键值。
- 强制刷新:开发时勾选“Update on reload”可让页签刷新时立即激活新 Service Worker。
- HTTPS 要求:本地开发
localhost不受 HTTPS 限制,但公网部署必须使用 HTTPS。 - Scope 错误:确保 Service Worker 文件位于你想控制的作用域根路径(通常是项目根目录)。
进阶学习方向
- 高级缓存策略:Stale-while-revalidate,网络优先、仅缓存等。
- 后台同步:使用
SyncManager在恢复网络后重发失败请求。 - Workbox 库:Google 提供的一套生产级 Service Worker 工具,简化缓存与路由管理。
- 推送加密与服务器实现:学习如何使用
web-push库从后端发送经过加密的推送消息。
现在,你已经掌握了 Service Worker 的基础,并亲手实现了一个具备离线访问和推送通知能力的 PWA。将这些代码添加到你的项目中,打开浏览器 DevTools 进行实验,迈出打造下一代 Web 体验的第一步吧!