主题
Service Worker
什么是 Service Worker
Service Worker 是运行在浏览器后台的独立线程,充当浏览器与网络之间的代理服务器。它能拦截网络请求、管理缓存、实现离线访问、推送通知等功能,是构建 PWA(Progressive Web App)的核心技术。
浏览器主线程 Service Worker 线程 网络
───────────── ───────────────────── ─────
(独立线程,无 DOM 访问)
| | |
|── fetch('/api/data') ──────→ | |
| |── 检查缓存 |
| | ├─ 命中 → 直接返回缓存 |
| | └─ 未命中 ─────────────→ |
| | |
| | ←──────── 网络响应 ─────── |
| ←── 返回响应 ────────────── |── 写入缓存 |
| | |Service Worker vs Web Worker
| 对比项 | Service Worker | Web Worker |
|---|---|---|
| 生命周期 | 独立于页面,浏览器管理 | 随页面创建和销毁 |
| 作用域 | 可控制多个页面 | 仅服务于创建它的页面 |
| 网络拦截 | ✅ 可拦截 fetch 请求 | ❌ 不可拦截 |
| 缓存控制 | ✅ Cache API | ❌ 无 |
| 离线支持 | ✅ 核心能力 | ❌ 不支持 |
| 推送通知 | ✅ Push API | ❌ 不支持 |
| DOM 访问 | ❌ | ❌ |
| HTTPS 要求 | ✅ 必须(localhost 除外) | ❌ 无要求 |
| 持久性 | 浏览器空闲时会终止,需要时重新启动 | 页面关闭即销毁 |
| 通信方式 | postMessage / MessageChannel | postMessage |
运行环境约束
Service Worker 的运行限制:
1. 无 DOM 访问
不能直接操作 document / window 对象
只能通过 postMessage 与页面通信
2. HTTPS 限制
必须在 HTTPS 环境下运行(localhost 开发时可豁免)
原因:防止中间人攻击篡改 Service Worker 代码
3. 独立线程
运行在独立的 Worker 上下文(ServiceWorkerGlobalScope)
不阻塞主线程 JS 执行
4. 异步 API
内部不能使用同步 API(如 localStorage / XHR 同步模式)
基于 Promise 驱动(fetch / Cache API / IndexedDB)
5. 空闲终止
浏览器可能在空闲时终止 Service Worker
不能依赖全局变量存储状态(下次启动状态丢失)
持久化状态应使用 IndexedDB / Cache API浏览器支持
Chrome 40+ ✅ 完整支持
Firefox 44+ ✅ 完整支持
Safari 11.1+ ✅ 完整支持(iOS Safari 限制较多)
Edge 17+ ✅ 完整支持
IE ─ ❌ 不支持
注意:
- iOS Safari 对 Service Worker 有存储限制(最多 50MB)
- iOS 上 Service Worker 在非活跃状态下可能被清除
- 可通过 'serviceWorker' in navigator 检测支持情况生命周期
Service Worker 的生命周期独立于网页,是理解其工作原理的关键:
┌──────────────────────────────────────────┐
│ Service Worker 生命周期 │
└──────────────────────────────────────────┘
navigator.serviceWorker.register('/sw.js')
|
▼
┌──────────┐ 下载并解析 sw.js
│ Parsed │─────────────────────┐
└──────────┘ │
| │ 解析失败 → 注册失败
▼ │ 下次访问重试
┌──────────┐ │
│Installing │ ← install 事件 │
│ (安装中) │ 预缓存关键资源 │
└──────────┘ │
| \ │
| \─── 安装失败 ───→ 废弃(redundant)
▼
┌──────────┐
│ Installed │ 等待旧 SW 释放控制权
│ (已安装) │ (或调用 skipWaiting 跳过等待)
└──────────┘
|
▼
┌──────────┐
│Activating│ ← activate 事件
│ (激活中) │ 清理旧缓存
└──────────┘
|
▼
┌──────────┐
│ Activated │ ← 开始控制页面
│ (已激活) │ 拦截 fetch / push / sync 事件
└──────────┘
|
▼
┌──────────┐
│ Idle │ 空闲状态,浏览器可能随时终止
│ (空闲) │ 有事件时重新唤醒
└──────────┘
|
▼ (检测到新版本 sw.js)
┌──────────┐
│ Redundant │ 旧 SW 被新版本替换
│ (废弃) │ 或安装/激活失败
└──────────┘各阶段详解
1. 注册(Register)
触发时机:页面 JS 调用 navigator.serviceWorker.register()
作用:通知浏览器下载并解析 Service Worker 脚本
注意:注册不等于安装,浏览器会检查是否有更新
2. 安装(Install)
触发事件:install
作用:预缓存(precache)应用的核心静态资源
关键 API:event.waitUntil() —— 延长安装阶段直到 Promise 完成
失败处理:如果 precache 任何资源失败,整个安装失败,SW 不会激活
3. 等待(Waiting)
触发条件:已有旧版 SW 正在控制页面
行为:新 SW 安装完成后进入等待状态
跳过等待:调用 self.skipWaiting() 立即进入激活阶段
4. 激活(Activate)
触发事件:activate
作用:清理旧版本缓存、执行数据库迁移等
关键 API:event.waitUntil() + clients.claim()
clients.claim():让新 SW 立即接管所有已打开的页面
5. 控制(Fetch / 空闲)
触发事件:fetch / push / sync / message
作用:拦截网络请求、处理推送、后台同步
注意:第一次注册 SW 后,页面不会被控制(需要刷新或 clients.claim)
6. 更新 / 废弃(Redundant)
触发条件:检测到新版本 sw.js / 安装失败 / 激活失败
行为:旧 SW 被标记为 redundant 并最终被回收注册与安装
注册 Service Worker
js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js', { scope: '/' })
.then((registration) => {
console.log('SW registered, scope:', registration.scope);
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
console.log('New SW activated');
}
});
});
})
.catch((error) => {
console.error('SW registration failed:', error);
});
});
}scope 作用域:
register('/sw.js', { scope: '/' })
scope 决定 SW 能控制哪些页面的请求:
scope: '/' → 控制整个站点
scope: '/app/' → 只控制 /app/ 下的页面
scope: '/blog/' → 只控制 /blog/ 下的页面
默认 scope = sw.js 所在目录
不能设置比 sw.js 位置更高的 scope(除非服务器设置 Service-Worker-Allowed 头)install 事件 —— 预缓存资源
js
const CACHE_NAME = 'app-cache-v1';
const PRECACHE_URLS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.svg',
'/offline.html',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(PRECACHE_URLS);
})
);
});activate 事件 —— 清理旧缓存
js
const CURRENT_CACHES = ['app-cache-v1', 'api-cache-v1'];
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => !CURRENT_CACHES.includes(name))
.map((name) => caches.delete(name))
);
})
);
});skipWaiting 与 clients.claim
js
// sw.js
self.addEventListener('install', (event) => {
self.skipWaiting(); // 跳过等待,立即激活
event.waitUntil(/* precache */);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
clients.claim() // 立即接管所有已打开的页面
);
});skipWaiting + clients.claim 的效果:
不使用:
Tab1(旧 SW 控制)→ 用户刷新页面 → Tab1(新 SW 控制)
新 SW 安装后必须等所有旧 Tab 关闭
使用 skipWaiting + clients.claim:
Tab1(旧 SW 控制)→ 新 SW 安装完成 → Tab1(立即切换到新 SW)
注意风险:
页面加载过程中 SW 可能切换
旧版缓存的 HTML 可能与新版 JS/CSS 不兼容
需要设计版本兼容策略或提示用户刷新Fetch 拦截与缓存策略
Service Worker 通过 fetch 事件拦截页面发出的所有网络请求,并可以根据不同策略决定如何响应。
Cache First(缓存优先 / 离线优先)
优先使用缓存,缓存未命中时回退到网络。适用于不经常变化的静态资源。
请求发起
↓
检查缓存
├─ 命中 → 返回缓存响应 ✅
└─ 未命中
↓
请求网络
├─ 成功 → 写入缓存 + 返回响应 ✅
└─ 失败 → 返回错误 ❌js
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) {
return cached;
}
return fetch(event.request).then((response) => {
const cloned = response.clone();
caches.open('runtime-cache').then((cache) => {
cache.put(event.request, cloned);
});
return response;
});
})
);
});Network First(网络优先)
优先请求网络,网络失败时回退到缓存。适用于需要最新数据的 API 请求。
请求发起
↓
请求网络
├─ 成功 → 更新缓存 + 返回网络响应 ✅
└─ 失败(超时/离线)
↓
检查缓存
├─ 命中 → 返回缓存响应 ✅(可能过期但可用)
└─ 未命中 → 返回离线页面 / 错误 ❌js
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
const cloned = response.clone();
caches.open('api-cache').then((cache) => {
cache.put(event.request, cloned);
});
return response;
})
.catch(() => {
return caches.match(event.request);
})
);
});Stale While Revalidate(缓存 + 后台更新)
立即返回缓存(即使过期),同时后台发起网络请求更新缓存。下次请求时用户将获得最新数据。适用于更新频率适中、对实时性要求不高的资源。
请求发起
↓
检查缓存
├─ 命中 → 立即返回缓存响应 ✅(用户无需等待)
│ 同时后台请求网络
│ └─ 成功 → 更新缓存(下次使用新版本)
│
└─ 未命中 → 请求网络 → 写入缓存 + 返回响应 ✅js
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('swr-cache').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;
});
})
);
});Network Only(仅网络)
完全不使用缓存,所有请求都走网络。适用于非 GET 请求(POST/PUT/DELETE)或实时性要求极高的数据。
请求发起
↓
请求网络
├─ 成功 → 返回网络响应 ✅
└─ 失败 → 返回错误 ❌js
self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request));
});Cache Only(仅缓存)
完全不走网络,只使用缓存。适用于在 install 阶段已预缓存的资源。
请求发起
↓
检查缓存
├─ 命中 → 返回缓存响应 ✅
└─ 未命中 → 返回错误 ❌js
self.addEventListener('fetch', (event) => {
event.respondWith(caches.match(event.request));
});策略对比与选型
策略 响应速度 数据新鲜度 离线能力 典型场景
────────────────── ──────── ────────── ──────── ──────────────
Cache First ⚡ 最快 ⚠️ 可能旧 ✅ 强 字体/图片/CSS/JS
Network First 🐢 较慢 ✅ 最新 ✅ 有 API 数据/动态内容
Stale While Revalidate ⚡ 快 ✅ 较新 ✅ 有 头像/不紧急的 API
Network Only 🐢 取决网络 ✅ 最新 ❌ 无 非幂等请求/实时数据
Cache Only ⚡ 最快 ❌ 固定 ✅ 强 预缓存的 App Shell
实际项目中的混合策略示例:
HTML 入口 → Network First(确保获取最新版本)
带 hash 的静态资源 → Cache First(内容不变,长期缓存)
API 请求 → Network First 或 Stale While Revalidate
App Shell → Cache Only(预缓存的骨架)
CDN 字体/图标 → Cache First(很少更新)PWA 离线应用
PWA(Progressive Web App)通过 Service Worker + manifest.json 实现类原生应用体验。
manifest.json 配置
json
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "A demo progressive web app",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4A90E2",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}html
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#4A90E2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">display 属性值:
fullscreen → 全屏(无系统 UI)
standalone → 独立窗口(像原生 App,无浏览器地址栏)
minimal-ui → 保留少量浏览器控件
browser → 普通浏览器标签页
PWA 安装条件(Chrome):
1. 有效的 manifest.json(含 name/icons/start_url/display)
2. 已注册并激活 Service Worker
3. 通过 HTTPS 提供
4. 用户与页面有一定的交互(engagement heuristic)安装提示(beforeinstallprompt)
js
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
showInstallButton();
});
function onInstallClick() {
if (!deferredPrompt) return;
deferredPrompt.prompt();
deferredPrompt.userChoice.then((result) => {
if (result.outcome === 'accepted') {
console.log('User accepted install');
}
deferredPrompt = null;
hideInstallButton();
});
}
window.addEventListener('appinstalled', () => {
console.log('PWA installed');
deferredPrompt = null;
});离线页面(Offline Fallback)
js
// sw.js
const OFFLINE_URL = '/offline.html';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('offline-cache').then((cache) => {
return cache.add(OFFLINE_URL);
})
);
});
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(OFFLINE_URL);
})
);
}
});App Shell 架构
App Shell 模式:
将应用的「外壳」(导航栏、侧边栏、布局框架)与「内容」分离
┌──────────────────────────────────┐
│ Header / NavBar │ ←── App Shell(预缓存)
├──────────────────────────────────┤
│ Sidebar │ │
│ │ 动态内容区域 │ ←── 内容(Network First)
│ (预缓存) │ (从网络获取) │
│ │ │
├──────────────────────────────────┤
│ Footer │ ←── App Shell(预缓存)
└──────────────────────────────────┘
优势:
1. 首次加载后,App Shell 从缓存中瞬间加载
2. 仅内容区域需要网络请求
3. 离线时仍能展示应用骨架 + 离线提示
4. 用户感知的加载速度极快
缓存策略:
App Shell(HTML 骨架/CSS/JS) → Cache First / Cache Only
动态内容(API 数据) → Network First
静态资源(图片/字体) → Cache First更新策略
Service Worker 更新流程
浏览器如何检测 SW 更新:
1. 每次导航到 SW 作用域内的页面时,浏览器都会尝试重新下载 sw.js
2. 浏览器对 sw.js 进行**逐字节比较**(byte-for-byte comparison)
3. 即使只有 1 个字节不同,也会触发更新流程
4. 浏览器至少每 24 小时检查一次更新(忽略 HTTP 缓存头)
5. 可通过 registration.update() 手动触发检查
更新时间线:
用户访问页面(旧 SW v1 控制中)
↓
浏览器后台下载新 sw.js
↓
字节比较:发现与当前 SW 不同
↓
触发新 SW v2 的 install 事件
↓
新 SW v2 进入 waiting 状态
↓ ← 等待旧 SW v1 释放控制权
↓ (所有由 v1 控制的 Tab 关闭)
↓
新 SW v2 进入 activate 状态
↓
新 SW v2 接管后续页面版本管理
js
// sw.js —— 通过缓存名称管理版本
const VERSION = 'v2';
const CACHE_NAME = `app-cache-${VERSION}`;
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
]);
})
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) => {
return Promise.all(
names
.filter((name) => name.startsWith('app-cache-') && name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
});平滑迁移(新旧版本并存)
新旧 SW 并存期间的处理策略:
方案一:skipWaiting + 提示用户刷新
──────────────────────────────────
新 SW 安装 → skipWaiting 立即激活
→ 通过 postMessage 通知页面
→ 页面显示「有新版本可用,点击刷新」提示
方案二:等待自然切换
──────────────────────
新 SW 安装后等待
→ 用户关闭所有旧 Tab
→ 下次打开时自动使用新 SW
方案三:controllerchange 自动刷新
───────────────────────────────
监听 SW 控制权变化,自动刷新页面js
// 页面代码 —— 方案一:提示用户刷新
navigator.serviceWorker.register('/sw.js').then((registration) => {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
showUpdateNotification();
}
});
});
});
function showUpdateNotification() {
const toast = document.createElement('div');
toast.textContent = '新版本可用,点击刷新';
toast.addEventListener('click', () => {
window.location.reload();
});
document.body.appendChild(toast);
}
// 页面代码 —— 方案三:自动刷新
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (!refreshing) {
refreshing = true;
window.location.reload();
}
});Workbox
Workbox 是 Google 推出的 Service Worker 工具库,封装了常用的缓存策略和最佳实践,大幅简化 SW 开发。
Workbox 核心模块:
workbox-precaching 预缓存管理(带版本哈希)
workbox-routing 请求路由匹配(URL / 正则 / 回调)
workbox-strategies 缓存策略实现(5 种内置策略)
workbox-expiration 缓存过期控制(条目数 / 时间)
workbox-cacheable-response 可缓存响应过滤(按状态码 / Header)
workbox-background-sync 后台同步(离线请求队列)
workbox-broadcast-update 广播缓存更新通知
workbox-window 页面端的 SW 生命周期管理workbox-precaching
js
import { precacheAndRoute } from 'workbox-precaching';
precacheAndRoute(self.__WB_MANIFEST);__WB_MANIFEST 在构建时由 workbox-webpack-plugin 或 workbox-build 注入:
[
{ url: '/index.html', revision: 'a1b2c3' },
{ url: '/styles/main.css', revision: 'd4e5f6' },
{ url: '/scripts/app.js', revision: 'g7h8i9' },
]
带 hash 的文件名(如 app.a1b2c3.js)不需要 revision 字段
Workbox 自动处理版本更新:新版本的资源会在 install 阶段下载,旧版本在 activate 阶段清理workbox-routing + workbox-strategies
js
import { registerRoute } from 'workbox-routing';
import {
CacheFirst,
NetworkFirst,
StaleWhileRevalidate,
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60,
}),
new CacheableResponsePlugin({
statuses: [0, 200],
}),
],
})
);
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 3,
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 5 * 60,
}),
],
})
);
registerRoute(
({ request }) =>
request.destination === 'style' || request.destination === 'script',
new StaleWhileRevalidate({
cacheName: 'static-resources',
})
);Workbox 构建配置(workbox-webpack-plugin)
js
const { InjectManifest } = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new InjectManifest({
swSrc: './src/sw.js',
swDest: 'sw.js',
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
}),
],
};两种 Webpack 插件模式:
GenerateSW:
自动生成完整的 sw.js
适合简单场景,零配置
不支持自定义逻辑
InjectManifest:
将预缓存清单注入到你编写的 sw.js 中
完全控制 SW 逻辑
适合需要自定义缓存策略、推送通知等场景
推荐用于生产环境消息通信
Service Worker 与页面之间无法直接共享变量,需要通过消息机制通信。
postMessage 双向通信
js
// 页面 → SW
navigator.serviceWorker.controller.postMessage({
type: 'CACHE_URLS',
payload: ['/api/articles', '/api/users'],
});
// SW 接收
self.addEventListener('message', (event) => {
if (event.data.type === 'CACHE_URLS') {
caches.open('dynamic-cache').then((cache) => {
cache.addAll(event.data.payload);
});
}
});js
// SW → 页面(向所有受控页面广播)
self.clients.matchAll().then((clients) => {
clients.forEach((client) => {
client.postMessage({
type: 'CACHE_UPDATED',
payload: { url: '/api/data' },
});
});
});
// 页面接收
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data.type === 'CACHE_UPDATED') {
console.log('Cache updated for:', event.data.payload.url);
refreshUI();
}
});MessageChannel(一对一双向通道)
js
// 页面发送带回复通道的消息
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
console.log('SW replied:', event.data);
};
navigator.serviceWorker.controller.postMessage(
{ type: 'GET_CACHE_SIZE' },
[messageChannel.port2]
);
// SW 接收并通过 port 回复
self.addEventListener('message', (event) => {
if (event.data.type === 'GET_CACHE_SIZE') {
getCacheSize().then((size) => {
event.ports[0].postMessage({ size });
});
}
});BroadcastChannel(多 Tab 广播)
js
// SW 中
const channel = new BroadcastChannel('sw-updates');
channel.postMessage({
type: 'NEW_VERSION',
version: 'v2.0.0',
});
// 所有 Tab 页面中
const channel = new BroadcastChannel('sw-updates');
channel.addEventListener('message', (event) => {
if (event.data.type === 'NEW_VERSION') {
showUpdateBanner(event.data.version);
}
});三种通信方式对比:
方式 方向 范围 场景
──────────── ────── ──────── ──────────
postMessage 双向 页面 ↔ SW 通用通信
MessageChannel 双向(1对1) 页面 ↔ SW 需要回复的请求
BroadcastChannel 广播 多 Tab + SW 版本更新通知/数据同步推送通知
Push API + Notification API 基本流程
推送通知完整流程:
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ 应用服务器 │ │ 推送服务 │ │ 浏览器 │
│ (Your │ │ (FCM/APNs/ │ │ (Service │
│ Server) │ │ Web Push) │ │ Worker) │
└──────────┘ └──────────────┘ └──────────┘
| | |
| | ①用户授权通知 |
| | Notification |
| | .requestPermission()
| | |
| | ②订阅推送服务 |
| |←── subscribe() ── |
| |──→ pushSubscription|
| | |
| ③保存 subscription | |
|←─────────────────────────────────── |
| | |
| ④发送推送消息 | |
|──→ web-push API ─→| |
| |──→ push 事件 ────→|
| | |
| | ⑤显示通知 |
| | showNotification()|
| | |订阅推送
js
// 页面代码
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: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription),
});
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = atob(base64);
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
}Service Worker 接收推送并显示通知
js
// sw.js
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
const title = data.title || '新消息';
const options = {
body: data.body || '你有一条新通知',
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
data: { url: data.url || '/' },
actions: [
{ action: 'open', title: '查看' },
{ action: 'dismiss', title: '忽略' },
],
};
event.waitUntil(self.registration.showNotification(title, options));
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'dismiss') return;
event.waitUntil(
clients.matchAll({ type: 'window' }).then((clientList) => {
for (const client of clientList) {
if (client.url === event.notification.data.url && 'focus' in client) {
return client.focus();
}
}
return clients.openWindow(event.notification.data.url);
})
);
});服务端发送推送(Node.js)
js
const webpush = require('web-push');
webpush.setVapidDetails(
'mailto:admin@example.com',
VAPID_PUBLIC_KEY,
VAPID_PRIVATE_KEY
);
async function sendPush(subscription, payload) {
await webpush.sendNotification(subscription, JSON.stringify(payload));
}
sendPush(userSubscription, {
title: '文章更新',
body: '你关注的专栏发布了新文章',
url: '/articles/123',
});面试高频题
1. Service Worker 的生命周期是怎样的?为什么新的 Service Worker 不会立即生效?
Service Worker 的生命周期分为:注册(register)→ 安装(install)→ 等待(waiting)→ 激活(activate)→ 控制(fetch 拦截)→ 废弃(redundant)。新 SW 安装完成后默认进入 waiting 状态而不会立即激活,这是因为旧 SW 可能还在控制页面——如果新 SW 直接接管,页面可能加载了旧版资源却用新版 SW 处理请求,导致不一致。浏览器要求所有由旧 SW 控制的 Tab 全部关闭后,新 SW 才会激活。可以通过 self.skipWaiting() 跳过等待立即激活,配合 clients.claim() 立即接管所有页面,但需要处理好版本兼容问题。
2. Service Worker 有哪些缓存策略?分别适用于什么场景?
五种主要缓存策略:①Cache First(缓存优先):先查缓存,未命中才走网络,适合字体、图片、带 hash 的 JS/CSS 等不常变化的静态资源;②Network First(网络优先):先走网络,失败回退缓存,适合 API 请求、HTML 入口等需要最新数据的场景;③Stale While Revalidate:立即返回缓存,后台更新,适合头像、非实时 API 等对新鲜度要求不高的资源;④Network Only:完全不用缓存,适合非幂等请求(POST/PUT)或实时数据;⑤Cache Only:完全不走网络,适合 install 阶段已预缓存的 App Shell 资源。实际项目通常混合使用多种策略。
3. Service Worker 与 Web Worker 有什么区别?
两者都运行在独立线程、不能访问 DOM,但核心定位不同。Service Worker 是浏览器和网络之间的代理,生命周期独立于页面,可以控制多个页面的网络请求,支持离线缓存、推送通知和后台同步,必须在 HTTPS 环境下运行,浏览器会在空闲时终止并在需要时重新启动。Web Worker 是为主线程分担计算密集型任务的工具线程,随页面创建和销毁,只服务于创建它的页面,不能拦截网络请求,没有 HTTPS 要求。简单说:Service Worker 是"网络代理",Web Worker 是"计算助手"。
4. 如何实现 PWA 的离线访问?App Shell 架构是什么?
离线访问的实现步骤:①注册 Service Worker;②在 install 事件中预缓存关键资源(HTML 骨架、CSS、JS、离线回退页面);③在 fetch 事件中对导航请求使用 Network First 策略,网络失败时返回预缓存的离线页面;④对静态资源使用 Cache First 策略。App Shell 架构是将应用的「外壳」(导航栏、侧边栏、布局框架)与「动态内容」分离——Shell 部分预缓存后从 Cache 瞬间加载,内容区域走网络请求。这样首屏加载极快,离线时也能展示应用骨架加离线提示,再配合 manifest.json 实现可安装的 PWA 体验。
5. Workbox 解决了什么问题?它的核心模块有哪些?
手动编写 Service Worker 需要自己实现缓存策略、版本管理、预缓存清单生成、缓存过期清理等,代码复杂且容易出错。Workbox 将这些最佳实践封装为模块:workbox-precaching 自动管理预缓存(带版本哈希,自动更新旧资源);workbox-routing 提供路由匹配(按 URL、请求类型、正则等分发到不同策略);workbox-strategies 提供五种开箱即用的缓存策略;workbox-expiration 控制缓存过期(按条目数和时间);workbox-background-sync 实现离线请求队列(网络恢复后自动重发)。配合 Webpack/Vite 插件,可以在构建时自动注入预缓存清单,无需手动维护资源列表。
追问思考
- Service Worker 中为什么不能使用
localStorage?它可以使用哪些存储 API?IndexedDB和Cache API分别适合存储什么类型的数据? event.waitUntil()的作用是什么?如果在 install 事件中不使用它会发生什么?它和event.respondWith()有什么区别?- Service Worker 被浏览器终止后重新启动时,全局变量会丢失。如果需要在多次 SW 启动之间保持状态(如请求计数),应该怎么做?
- 在 iOS Safari 上,Service Worker 有哪些已知的限制和坑?为什么说 PWA 在 iOS 上的体验不如 Android?
- 如果 Service Worker 中缓存了错误的资源(如缓存了一个报错页面),如何实现"紧急回滚"?有哪些策略可以避免这类缓存灾难?