Service Worker
Service Worker 是浏览器在后台运行的脚本,可以拦截网络请求、缓存资源,实现离线功能和推送通知,是 PWA(Progressive Web App)的核心技术。
参考规范:Service Workers
📚 目录
1. Service Worker 概述
1.1 什么是 Service Worker
Service Worker 是运行在浏览器后台的脚本,独立于网页,可以:
- 拦截网络请求
- 缓存资源
- 实现离线功能
- 接收推送通知
- 后台同步
特点:
- 运行在独立线程
- 不能直接访问 DOM
- 必须通过 HTTPS(localhost 除外)
- 事件驱动
1.2 浏览器支持
// 检查支持
if ('serviceWorker' in navigator) {
// 支持 Service Worker
} else {
console.warn('Service Worker not supported');
}2. 注册和安装
2.1 注册 Service Worker
// 注册 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker registered:', registration);
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
}2.2 Service Worker 文件
// sw.js
// 安装事件
self.addEventListener('install', event => {
console.log('Service Worker installing');
// 等待直到缓存完成
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js'
]);
})
);
});
// 激活事件
self.addEventListener('activate', event => {
console.log('Service Worker activating');
// 清理旧缓存
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== 'v1') {
return caches.delete(cacheName);
}
})
);
})
);
});2.3 作用域
// 注册时指定作用域
navigator.serviceWorker.register('/sw.js', {
scope: '/app/' // 只拦截 /app/ 下的请求
});
// ⚠️ 注意:作用域不能超过 Service Worker 文件所在目录
// /sw.js 的作用域最大是 /3. 生命周期
3.1 生命周期阶段
注册 → 安装 → 激活 → 运行
↓ ↓ ↓ ↓
register install activate fetch
3.2 安装阶段
self.addEventListener('install', event => {
console.log('Installing Service Worker');
// 跳过等待,立即激活
self.skipWaiting();
// 预缓存资源
event.waitUntil(
caches.open('static-v1').then(cache => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js'
]);
})
);
});3.3 激活阶段
self.addEventListener('activate', event => {
console.log('Activating Service Worker');
// 立即控制所有客户端
event.waitUntil(
clients.claim().then(() => {
console.log('Service Worker is controlling clients');
})
);
// 清理旧缓存
event.waitUntil(cleanOldCaches());
});3.4 更新 Service Worker
// 主线程检查更新
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('New Service Worker controlling the page');
window.location.reload();
});
// 手动更新
navigator.serviceWorker.ready.then(registration => {
registration.update();
});4. 缓存策略
4.1 Cache API
// 打开缓存
caches.open('my-cache').then(cache => {
// 添加单个资源
cache.add('/image.jpg');
// 添加多个资源
cache.addAll([
'/',
'/index.html',
'/styles.css'
]);
// 添加响应
cache.put('/api/data', response);
// 匹配缓存
cache.match('/image.jpg').then(response => {
if (response) {
console.log('Found in cache');
}
});
// 删除缓存
cache.delete('/image.jpg');
});4.2 缓存策略
// 1. Cache First(缓存优先)
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response; // 从缓存返回
}
return fetch(event.request); // 网络请求
})
);
});
// 2. Network First(网络优先)
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request); // 网络失败时使用缓存
})
);
});
// 3. Stale While Revalidate(后台更新)
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
const fetchPromise = fetch(event.request).then(networkResponse => {
caches.open('v1').then(cache => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
});
return cachedResponse || fetchPromise;
})
);
});4.3 动态缓存
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).then(response => {
// 克隆响应(响应流只能读取一次)
const responseClone = response.clone();
// 缓存响应
caches.open('dynamic-v1').then(cache => {
cache.put(event.request, responseClone);
});
return response;
}).catch(() => {
// 网络失败,尝试从缓存获取
return caches.match(event.request);
})
);
});5. 网络拦截
5.1 Fetch 事件
self.addEventListener('fetch', event => {
const request = event.request;
// 只处理 GET 请求
if (request.method !== 'GET') {
return;
}
// 只处理同源请求
if (new URL(request.url).origin !== location.origin) {
return;
}
// 拦截并处理请求
event.respondWith(handleRequest(request));
});
async function handleRequest(request) {
// 尝试从缓存获取
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// 网络请求
try {
const response = await fetch(request);
// 缓存响应
const cache = await caches.open('v1');
cache.put(request, response.clone());
return response;
} catch (error) {
// 网络失败,返回离线页面
return caches.match('/offline.html');
}
}5.2 请求修改
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 修改请求 URL
if (url.pathname.startsWith('/api/')) {
url.pathname = '/api/v2' + url.pathname;
const modifiedRequest = new Request(url, event.request);
event.respondWith(fetch(modifiedRequest));
}
});6. 推送通知
6.1 请求通知权限
// 主线程
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
console.log('Notification permission granted');
}
});6.2 推送事件
// sw.js
self.addEventListener('push', event => {
const data = event.data ? event.data.json() : {};
const title = data.title || 'New Notification';
const options = {
body: data.body || 'You have a new message',
icon: '/icon.png',
badge: '/badge.png',
tag: 'notification-tag',
data: data
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});6.3 通知点击
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(
clients.openWindow('/') // 打开窗口
);
});7. 实际应用
7.1 离线应用
// sw.js
const CACHE_NAME = 'offline-v1';
const OFFLINE_URL = '/offline.html';
// 安装时缓存离线页面
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll([OFFLINE_URL]);
})
);
self.skipWaiting();
});
// 拦截请求
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(OFFLINE_URL);
})
);
} else {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
}
});7.2 后台同步
// 主线程
navigator.serviceWorker.ready.then(registration => {
return registration.sync.register('sync-data');
});
// sw.js
self.addEventListener('sync', event => {
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});
async function syncData() {
// 同步数据到服务器
const data = await getPendingData();
await fetch('/api/sync', {
method: 'POST',
body: JSON.stringify(data)
});
}7.3 完整 PWA 示例
// sw.js
const CACHE_NAME = 'pwa-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/manifest.json'
];
// 安装
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(urlsToCache);
})
);
self.skipWaiting();
});
// 激活
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
return self.clients.claim();
});
// 拦截请求
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response;
}
return fetch(event.request).then(response => {
if (!response || response.status !== 200) {
return response;
}
const responseToCache = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});