Service Worker API
一、为什么需要 Service Worker
1.1 传统 Web 的局限性
传统 Web 应用无法在后台运行,无法在用户没有网络连接时提供体验,无法像原生应用一样推送通知。Service Worker 填补了这个空白。
1.2 Service Worker 的能力
Service Worker 是一种运行在浏览器后台的脚本,独立于网页:
- 拦截网络请求:可以决定如何响应请求
- 缓存管理:完全控制缓存策略
- 后台同步:在网络恢复后执行任务
- 推送通知:接收和显示推送通知
- 离线体验:提供离线或弱网下的内容
二、生命周期
2.1 注册和安装
// 在主线程注册
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker 注册成功:', registration.scope);
})
.catch(error => {
console.error('Service Worker 注册失败:', error);
});
}
// sw.js - Service Worker 文件
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
];
// 安装事件
self.addEventListener('install', event => {
console.log('Service Worker 安装中...');
// 等待缓存完成
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('缓存已打开');
return cache.addAll(urlsToCache);
})
.then(() => {
// 跳过等待,立即激活
return self.skipWaiting();
})
);
});
2.2 激活
// 激活事件
self.addEventListener('activate', event => {
console.log('Service Worker 激活中...');
// 清理旧缓存
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
// 删除不用的缓存
if (cacheName !== CACHE_NAME) {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => {
// 接管所有页面
return self.clients.claim();
})
);
});
2.3 完整的生命周期图
注册 → 安装中 → 安装完成
↓
激活中 → 激活完成
↓
空闲 →(fetch 事件)→ 处理中 → 空闲
三、fetch 事件处理
3.1 基础缓存策略
// 缓存优先
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存命中,直接返回
if (response) {
return response;
}
// 缓存未命中,发起网络请求
return fetch(event.request)
.then(networkResponse => {
// 检查是否有效
if (!networkResponse || networkResponse.status !== 200) {
return networkResponse;
}
// 缓存新资源
const responseToCache = networkResponse.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return networkResponse;
});
})
);
});
3.2 离线优先策略
// 离线优先:先缓存,没有缓存才网络请求
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
return response || fetch(event.request);
})
.catch(() => {
// 网络和缓存都没有,返回离线页面
return caches.match('/offline.html');
})
);
});
3.3 网络优先策略
// 网络优先:先请求,失败时用缓存
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.then(response => {
// 有效响应,缓存后返回
if (response && response.status === 200) {
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
}
return response;
})
.catch(() => {
// 网络失败,使用缓存
return caches.match(event.request);
})
);
});
四、Cache Storage API
4.1 基本操作
// 打开缓存
const cache = await caches.open('v1');
// 添加资源
await cache.add('/styles.css');
await cache.addAll(['/page1.html', '/page2.html']);
// 检查是否包含
const has = await cache.has('/styles.css');
// 获取资源
const response = await cache.match('/styles.css');
// 删除缓存
await cache.delete('/styles.css');
// 获取所有缓存键
const keys = await cache.keys();
4.2 缓存版本管理
const CACHE_VERSION = 'v2';
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;
// 安装时使用新版缓存
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll([
'/',
'/index.html',
'/styles/main.css'
]);
})
);
});
// 激活时清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name.startsWith('app-cache-') && name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
});
五、后台同步
5.1 Background Sync API
// 主线程:请求后台同步
async function sendData(data) {
// 立即显示给用户
showToUser(data);
// 尝试发送,如果失败则排队
try {
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data)
});
} catch (error) {
// 注册后台同步
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-submit');
}
}
// Service Worker:处理后台同步
self.addEventListener('sync', event => {
if (event.tag === 'sync-submit') {
event.waitUntil(syncData());
}
});
async function syncData() {
// 从 IndexedDB 获取待发送的数据
const data = await getDataFromIndexedDB();
if (data) {
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data)
});
await deleteDataFromIndexedDB(data.id);
}
}
六、推送通知
6.1 Web Push 流程
- 用户授权接收通知
- 订阅用户到推送服务
- 推送服务发送推送
- Service Worker 接收并显示
6.2 订阅推送
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'YOUR_PUBLIC_KEY'
});
// 发送到服务器
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription)
});
}
6.3 处理推送
self.addEventListener('push', event => {
if (!event.data) return;
const data = event.data.json();
const options = {
body: data.body,
icon: '/images/icon.png',
badge: '/images/badge.png',
vibrate: [100, 50, 100],
data: {
url: data.url
}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
七、作用域和限制
7.1 作用域
// 注册时指定作用域
navigator.serviceWorker.register('/sw.js', {
scope: '/app/'
});
// Service Worker 只能拦截在自己的作用域内的请求
// scope: '/app/' 只能拦截 '/app/' 下的请求
7.2 限制
- 只能拦截通过 HTTP/HTTPS 发送的请求(localhost 除外)
- 不能访问 localStorage(需要使用 Cache API 和 IndexedDB)
- 异步操作需要使用 Promise
- 不能访问 window 对象
- 不能直接操作 DOM