Service Worker
Service Worker 是一种运行在浏览器背后的脚本,充当网站和浏览器之间的代理服务器。它能够在浏览器背景中运行,即使用户没有访问网页也是如此。Service Worker 的引入使得开发者能够创建更加丰富和可靠的用户体验,特别是离线体验和网络性能优化方面。

查看更多相关内容
Service Worker 与 Web Worker 有什么区别?## Service Worker vs Web Worker 对比
Service Worker 和 Web Worker 都是运行在浏览器后台的 JavaScript 线程,但它们的设计目标和应用场景完全不同。
## 核心区别对比表
| 特性 | Service Worker | Web Worker |
|------|----------------|------------|
| **主要用途** | 网络代理、离线缓存、推送通知 | 执行复杂计算、避免阻塞主线程 |
| **生命周期** | 独立于页面,可长期运行 | 与页面绑定,页面关闭即终止 |
| **DOM 访问** | ❌ 无法访问 | ❌ 无法访问 |
| **网络拦截** | ✅ 可以拦截所有网络请求 | ❌ 无法拦截 |
| **事件驱动** | ✅ 基于事件(fetch, push, sync) | ✅ 基于消息传递 |
| **安装方式** | 需要注册,有独立生命周期 | 直接实例化 Worker 对象 |
| **持久化** | ✅ 浏览器可自动重启 | ❌ 页面关闭即销毁 |
| **通信方式** | `postMessage` + `clients` API | `postMessage` |
## Service Worker 特点
### 1. 网络代理能力
```javascript
// Service Worker 可以拦截所有网络请求
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
```
### 2. 独立于页面的生命周期
- 安装后持续运行,即使所有页面关闭
- 浏览器可自动重启处理事件
- 适合后台任务(推送通知、后台同步)
### 3. 渐进式 Web 应用核心
```javascript
// 推送通知
self.addEventListener('push', event => {
event.waitUntil(
self.registration.showNotification('新消息', {
body: event.data.text()
})
);
});
// 后台同步
self.addEventListener('sync', event => {
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});
```
## Web Worker 特点
### 1. 计算密集型任务
```javascript
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ numbers: [1, 2, 3, 4, 5] });
worker.onmessage = event => {
console.log('Result:', event.data);
};
// worker.js
self.onmessage = event => {
const { numbers } = event.data;
const result = numbers.reduce((a, b) => a + b, 0);
self.postMessage(result);
};
```
### 2. 页面级生命周期
- 创建它的页面关闭时自动终止
- 适合处理一次性计算任务
- 无法执行后台持续任务
### 3. 多种类型
```javascript
// 专用 Worker(Dedicated Worker)
const worker = new Worker('worker.js');
// 共享 Worker(Shared Worker)- 多页面共享
const sharedWorker = new SharedWorker('shared-worker.js');
// Service Worker(特殊类型)
navigator.serviceWorker.register('sw.js');
```
## 使用场景对比
### Service Worker 适用场景
1. **离线应用**:缓存资源,提供离线访问
2. **网络优化**:智能缓存策略,减少网络请求
3. **推送通知**:接收服务器推送的消息
4. **后台同步**:网络恢复后自动同步数据
5. **PWA 功能**:添加到主屏幕、应用壳架构
### Web Worker 适用场景
1. **大数据处理**:图片处理、视频编解码
2. **复杂计算**:数学运算、数据分析
3. **实时数据处理**:WebSocket 数据解析
4. **文件处理**:大文件读取、压缩
5. **避免 UI 阻塞**:耗时操作不卡顿界面
## 代码示例对比
### Service Worker 示例
```javascript
// 注册
navigator.serviceWorker.register('/sw.js');
// sw.js - 拦截请求
self.addEventListener('fetch', event => {
event.respondWith(caches.match(event.request));
});
// 与主线程通信(通过 clients)
self.clients.matchAll().then(clients => {
clients.forEach(client => client.postMessage('update'));
});
```
### Web Worker 示例
```javascript
// 创建 Worker
const worker = new Worker('/worker.js');
// 发送消息
worker.postMessage({ action: 'calculate', data: [1, 2, 3] });
// 接收消息
worker.onmessage = event => {
console.log('Result:', event.data);
};
// worker.js
self.onmessage = event => {
const result = performCalculation(event.data);
self.postMessage(result);
};
```
## 总结
- **Service Worker**:网络代理专家,负责离线体验、后台任务
- **Web Worker**:计算性能专家,负责耗时运算、避免阻塞
- **两者关系**:互补而非替代,可在 PWA 中同时使用
服务端 · 3月7日 12:06
Service Worker 的安全注意事项有哪些?## Service Worker 安全注意事项详解
Service Worker 作为浏览器后台运行的代理服务器,具有强大的能力,同时也带来了一些安全风险。了解这些安全问题对于开发安全的 Web 应用至关重要。
## 1. HTTPS 要求
### 为什么必须使用 HTTPS
```javascript
// Service Worker 只能在 HTTPS 环境下注册
// 例外:localhost 允许 HTTP
if ('serviceWorker' in navigator) {
// 检查是否是安全上下文
if (window.isSecureContext) {
navigator.serviceWorker.register('/sw.js');
} else {
console.error('Service Worker 需要 HTTPS 环境');
}
}
```
**安全风险:**
- HTTP 环境下 Service Worker 可能被中间人攻击篡改
- 攻击者可注入恶意 Service Worker 拦截所有网络请求
- 用户的敏感数据可能被窃取
### 检测安全上下文
```javascript
// 检查当前环境是否安全
function checkSecureContext() {
if (!window.isSecureContext) {
console.warn('当前不是安全上下文,Service Worker 功能受限');
return false;
}
return true;
}
// 或者检查协议
function isSecureProtocol() {
return location.protocol === 'https:' || location.hostname === 'localhost';
}
```
## 2. 作用域限制
### 作用域安全
```javascript
// Service Worker 只能控制其作用域内的页面
// 注册时指定作用域
navigator.serviceWorker.register('/sw.js', {
scope: '/app/' // 只能控制 /app/ 下的页面
});
// 尝试访问作用域外的资源会失败
// /app/page.html ✅ 可以控制
// /other/page.html ❌ 无法控制
```
### 路径遍历防护
```javascript
// ❌ 危险:不验证路径可能导致安全问题
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 恶意请求可能包含 ../ 等路径遍历字符
caches.match(url.pathname); // 危险!
});
// ✅ 安全:验证和清理路径
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
const pathname = url.pathname;
// 验证路径不包含遍历字符
if (pathname.includes('..') || pathname.includes('//')) {
event.respondWith(new Response('Invalid path', { status: 400 }));
return;
}
// 只允许访问白名单路径
const allowedPaths = ['/api/', '/assets/', '/static/'];
const isAllowed = allowedPaths.some(path => pathname.startsWith(path));
if (!isAllowed) {
event.respondWith(new Response('Forbidden', { status: 403 }));
return;
}
event.respondWith(caches.match(event.request));
});
```
## 3. 内容安全策略(CSP)
### CSP 对 Service Worker 的影响
```javascript
// 设置 CSP 防止 XSS 攻击
// 在 HTTP 响应头中设置:
// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
// Service Worker 脚本本身需要符合 CSP
// 内联脚本可能被阻止
```
### 安全的 Service Worker 脚本加载
```javascript
// ✅ 推荐:加载外部脚本
navigator.serviceWorker.register('/sw.js');
// ❌ 避免:使用内联 Service Worker
const swCode = `
self.addEventListener('fetch', ...);
`;
const blob = new Blob([swCode], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
navigator.serviceWorker.register(url); // 可能违反 CSP
```
## 4. 缓存安全
### 敏感数据缓存
```javascript
// ❌ 危险:缓存敏感信息
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/user/profile')) {
event.respondWith(
fetch(event.request).then(response => {
// 缓存包含个人信息的响应
caches.open('api-cache').then(cache => {
cache.put(event.request, response.clone()); // 危险!
});
return response;
})
);
}
});
// ✅ 安全:不缓存敏感数据
const SENSITIVE_PATHS = [
'/api/auth/',
'/api/user/profile',
'/api/payment/'
];
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 检查是否是敏感路径
const isSensitive = SENSITIVE_PATHS.some(path =>
url.pathname.includes(path)
);
if (isSensitive) {
// 敏感请求直接走网络,不缓存
event.respondWith(fetch(event.request));
return;
}
// 非敏感请求可以使用缓存
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
```
### 缓存清理安全
```javascript
// ✅ 安全的缓存清理
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
// 只删除当前 Service Worker 创建的缓存
// 避免删除其他应用的缓存
if (cacheName.startsWith('my-app-')) {
return caches.delete(cacheName);
}
})
);
})
);
});
```
## 5. 跨站脚本攻击(XSS)防护
### 防止恶意脚本注入
```javascript
// ❌ 危险:直接使用用户输入
self.addEventListener('message', event => {
const userInput = event.data.message;
// 直接执行用户输入可能导致 XSS
eval(userInput); // 极度危险!
});
// ✅ 安全:验证和清理输入
self.addEventListener('message', event => {
const data = event.data;
// 验证消息来源
if (event.origin !== 'https://trusted-domain.com') {
console.error('Untrusted origin:', event.origin);
return;
}
// 验证数据类型
if (typeof data.action !== 'string') {
return;
}
// 使用白名单验证操作
const allowedActions = ['skipWaiting', 'claimClients'];
if (!allowedActions.includes(data.action)) {
console.error('Invalid action:', data.action);
return;
}
// 安全执行
switch (data.action) {
case 'skipWaiting':
self.skipWaiting();
break;
case 'claimClients':
self.clients.claim();
break;
}
});
```
## 6. 中间人攻击防护
### 证书固定(Certificate Pinning)
```javascript
// 虽然 Service Worker 无法直接实现证书固定
// 但可以通过检查响应头来增强安全性
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).then(response => {
// 检查安全响应头
const headers = response.headers;
// 检查 HSTS 头
if (!headers.get('Strict-Transport-Security')) {
console.warn('Missing HSTS header');
}
// 检查 CSP 头
if (!headers.get('Content-Security-Policy')) {
console.warn('Missing CSP header');
}
return response;
})
);
});
```
## 7. 权限控制
### 最小权限原则
```javascript
// ✅ 只请求必要的权限
// 推送通知权限
async function requestPushPermission() {
const permission = await Notification.requestPermission();
return permission === 'granted';
}
// 后台同步权限
async function requestSyncPermission() {
const registration = await navigator.serviceWorker.ready;
if ('sync' in registration) {
// 检查权限状态
const status = await navigator.permissions.query({
name: 'periodic-background-sync'
});
if (status.state === 'granted') {
return true;
}
}
return false;
}
```
## 8. 更新安全
### 安全的更新机制
```javascript
// ✅ 验证 Service Worker 更新
navigator.serviceWorker.register('/sw.js').then(registration => {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed') {
// 验证新版本的完整性
// 可以通过计算哈希值来验证
verifyServiceWorkerIntegrity(newWorker).then(isValid => {
if (isValid) {
// 提示用户更新
showUpdateNotification(newWorker);
} else {
console.error('Service Worker 完整性验证失败');
}
});
}
});
});
});
// 验证 Service Worker 完整性(示例)
async function verifyServiceWorkerIntegrity(worker) {
try {
const response = await fetch('/sw.js');
const scriptContent = await response.text();
// 这里可以添加哈希验证逻辑
// 比如与服务器返回的哈希值对比
return true;
} catch (error) {
console.error('验证失败:', error);
return false;
}
}
```
## 9. 数据泄露防护
### 防止缓存泄露
```javascript
// ✅ 安全的缓存策略
const PUBLIC_RESOURCES = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/images/'
];
const PRIVATE_RESOURCES = [
'/api/user/',
'/api/orders/',
'/dashboard/'
];
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 检查是否是公共资源
const isPublic = PUBLIC_RESOURCES.some(path =>
url.pathname.startsWith(path)
);
// 检查是否是私有资源
const isPrivate = PRIVATE_RESOURCES.some(path =>
url.pathname.startsWith(path)
);
if (isPrivate) {
// 私有资源:网络优先,不缓存
event.respondWith(
fetch(event.request).catch(() => {
return new Response('Network error', { status: 503 });
})
);
} else if (isPublic) {
// 公共资源:可以使用缓存
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
} else {
// 未知资源:默认网络请求
event.respondWith(fetch(event.request));
}
});
```
## 10. 安全最佳实践清单
### 开发阶段
- [ ] 确保所有环境使用 HTTPS(除 localhost)
- [ ] 合理设置 Service Worker 作用域
- [ ] 不缓存敏感数据(用户信息、支付数据等)
- [ ] 验证所有用户输入
- [ ] 实施 CSP 策略
- [ ] 定期更新 Service Worker
### 生产环境
- [ ] 监控 Service Worker 异常行为
- [ ] 实施 Subresource Integrity (SRI)
- [ ] 配置 HSTS 头
- [ ] 定期审计缓存内容
- [ ] 实施访问控制
- [ ] 记录安全日志
## 安全测试建议
```javascript
// 安全测试清单
const securityTests = {
// 1. 检查 HTTPS
checkHTTPS: () => location.protocol === 'https:',
// 2. 检查作用域
checkScope: async () => {
const registration = await navigator.serviceWorker.ready;
console.log('Service Worker scope:', registration.scope);
return registration.scope;
},
// 3. 检查缓存内容
checkCacheContents: async () => {
const cacheNames = await caches.keys();
for (const name of cacheNames) {
const cache = await caches.open(name);
const requests = await cache.keys();
console.log(`Cache "${name}" contains ${requests.length} items`);
}
},
// 4. 检查权限
checkPermissions: async () => {
const permissions = await navigator.permissions.query({
name: 'notifications'
});
console.log('Notification permission:', permissions.state);
}
};
```
## 总结
Service Worker 安全要点:
1. **必须使用 HTTPS**:防止中间人攻击
2. **合理设置作用域**:限制控制能力
3. **不缓存敏感数据**:防止数据泄露
4. **验证用户输入**:防止 XSS 攻击
5. **实施 CSP**:增强内容安全
6. **定期更新**:修复安全漏洞
7. **最小权限**:只请求必要权限
服务端 · 3月7日 12:06
如何在 Service Worker 中实现推送通知功能?## Service Worker 推送通知实现详解
推送通知是 Service Worker 的重要功能之一,允许服务器向用户发送消息,即使用户没有打开网站。
## 推送通知架构
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 服务器 │────▶│ 推送服务 │────▶│ 浏览器 │
│ (Web App) │ │(FCM/APNs等) │ │(Service Worker)
└─────────────┘ └─────────────┘ └─────────────┘
```
## 实现步骤
### 1. 请求通知权限
```javascript
// main.js
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
console.log('通知权限已授予');
await subscribeUserToPush();
} else if (permission === 'denied') {
console.log('通知权限被拒绝');
} else {
console.log('通知权限待处理');
}
}
// 检查当前权限状态
console.log('当前权限:', Notification.permission);
// 'default' | 'granted' | 'denied'
```
### 2. 订阅推送服务
```javascript
// main.js
async function subscribeUserToPush() {
const registration = await navigator.serviceWorker.ready;
// 获取或创建订阅
let subscription = await registration.pushManager.getSubscription();
if (!subscription) {
// 从服务器获取 VAPID 公钥
const vapidPublicKey = await fetch('/api/vapid-public-key').then(r => r.text());
// 转换为 Uint8Array
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
// 创建订阅
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, // 必须设置为 true
applicationServerKey: convertedVapidKey
});
console.log('推送订阅成功:', subscription);
}
// 将订阅信息发送到服务器
await fetch('/api/save-subscription', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
return subscription;
}
// 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;
}
```
### 3. Service Worker 接收推送
```javascript
// sw.js
// 监听推送事件
self.addEventListener('push', event => {
console.log('收到推送消息:', event);
let data = {};
if (event.data) {
data = event.data.json();
}
const options = {
body: data.body || '您有一条新消息',
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
image: data.image || '/images/notification-banner.jpg',
tag: data.tag || 'default',
requireInteraction: false, // 是否保持通知直到用户交互
actions: [
{
action: 'open',
title: '打开',
icon: '/icons/open.png'
},
{
action: 'dismiss',
title: '忽略',
icon: '/icons/close.png'
}
],
data: {
url: data.url || '/',
timestamp: Date.now()
}
};
event.waitUntil(
self.registration.showNotification(data.title || '新通知', options)
);
});
```
### 4. 处理通知点击
```javascript
// sw.js
// 监听通知点击事件
self.addEventListener('notificationclick', event => {
console.log('通知被点击:', event);
event.notification.close();
const { action, notification } = event;
const data = notification.data || {};
if (action === 'dismiss') {
// 用户点击忽略按钮
return;
}
// 默认行为或点击打开按钮
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then(clientList => {
const urlToOpen = data.url || '/';
// 检查是否已有窗口打开
for (const client of clientList) {
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// 没有则打开新窗口
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// 监听通知关闭事件
self.addEventListener('notificationclose', event => {
console.log('通知被关闭:', event);
// 可以在这里记录统计数据
});
```
## 服务器端发送推送
### Node.js 示例(使用 web-push 库)
```javascript
// server.js
const webpush = require('web-push');
// 设置 VAPID 密钥
const vapidKeys = {
publicKey: 'YOUR_PUBLIC_KEY',
privateKey: 'YOUR_PRIVATE_KEY'
};
webpush.setVapidDetails(
'mailto:your-email@example.com',
vapidKeys.publicKey,
vapidKeys.privateKey
);
// 存储订阅信息(生产环境应使用数据库)
let subscriptions = [];
// 保存订阅
app.post('/api/save-subscription', (req, res) => {
const subscription = req.body;
subscriptions.push(subscription);
res.json({ success: true });
});
// 发送推送
app.post('/api/send-push', async (req, res) => {
const { title, body, url } = req.body;
const payload = JSON.stringify({
title,
body,
url,
icon: '/icons/icon-192x192.png'
});
const results = await Promise.allSettled(
subscriptions.map(sub => webpush.sendNotification(sub, payload))
);
// 清理无效订阅
results.forEach((result, index) => {
if (result.status === 'rejected' && result.reason.statusCode === 410) {
subscriptions.splice(index, 1);
}
});
res.json({ success: true, sent: results.length });
});
```
## 高级功能
### 1. 定时推送(定期后台同步)
```javascript
// 主线程请求定期同步
navigator.serviceWorker.ready.then(registration => {
registration.periodicSync.register('daily-news', {
minInterval: 24 * 60 * 60 * 1000 // 24小时
});
});
// sw.js 监听
self.addEventListener('periodicsync', event => {
if (event.tag === 'daily-news') {
event.waitUntil(showDailyNewsNotification());
}
});
```
### 2. 富媒体通知
```javascript
const options = {
body: '点击查看详情',
icon: '/icon.png',
image: '/banner.jpg', // 大图
badge: '/badge.png', // 小图标
vibrate: [200, 100, 200], // 振动模式
sound: '/notification.mp3', // 声音(部分浏览器支持)
dir: 'ltr', // 文字方向
lang: 'zh-CN',
timestamp: Date.now(),
requireInteraction: true,
actions: [
{ action: 'reply', title: '回复', icon: '/reply.png' },
{ action: 'archive', title: '归档', icon: '/archive.png' }
]
};
```
### 3. 取消订阅
```javascript
// main.js
async function unsubscribeFromPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
// 通知服务器删除订阅
await fetch('/api/remove-subscription', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ endpoint: subscription.endpoint })
});
console.log('已取消推送订阅');
}
}
```
## 浏览器兼容性
| 功能 | Chrome | Firefox | Safari | Edge |
|------|--------|---------|--------|------|
| Push API | ✅ | ✅ | ✅ (16.4+) | ✅ |
| Notification | ✅ | ✅ | ✅ | ✅ |
| Actions | ✅ | ✅ | ❌ | ✅ |
| Badge | ✅ | ❌ | ❌ | ✅ |
## 最佳实践
1. **尊重用户**:不要频繁发送通知,提供取消订阅选项
2. **及时更新**:定期清理无效的订阅
3. **个性化内容**:根据用户偏好发送相关通知
4. **优雅降级**:不支持推送的浏览器提供替代方案
5. **HTTPS 必需**:推送功能必须在 HTTPS 环境下运行
服务端 · 3月7日 12:06
Service Worker 的更新机制是怎样的?## Service Worker 更新机制详解
Service Worker 的更新机制是其重要特性之一,理解它对于维护应用的稳定性和用户体验至关重要。
## 更新触发条件
当浏览器检测到 Service Worker 文件发生变化时(字节差异),会触发更新流程。
```javascript
// 浏览器会自动检查更新
// 以下情况会触发检查:
// 1. 用户访问页面时
// 2. 调用 registration.update()
// 3. 页面刷新(在特定条件下)
```
## 更新生命周期
### 1. 检测更新
```javascript
// 手动触发更新检查
navigator.serviceWorker.register('/sw.js').then(registration => {
// 检查更新
registration.update();
// 监听更新发现
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
console.log('发现新版本 Service Worker');
});
});
```
### 2. 新版本安装
```javascript
// sw.js
const CACHE_NAME = 'app-v2'; // 更新缓存版本
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll([
'/',
'/index.html',
'/app.js',
'/styles.css'
]);
})
);
// 立即激活,跳过等待阶段
self.skipWaiting();
});
```
### 3. 等待阶段(Waiting)
默认情况下,新版本的 Service Worker 会进入等待状态,直到旧版本控制的所有页面关闭。
```javascript
// 主线程监听状态变化
navigator.serviceWorker.register('/sw.js').then(registration => {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
switch (newWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// 有新版本等待激活
console.log('新版本已安装,等待激活');
showUpdateNotification(newWorker);
}
break;
case 'activated':
console.log('新版本已激活');
break;
}
});
});
});
```
### 4. 激活阶段
```javascript
// sw.js
self.addEventListener('activate', event => {
event.waitUntil(
// 清理旧缓存
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => {
console.log('删除旧缓存:', name);
return caches.delete(name);
})
);
})
);
// 立即接管所有页面
self.clients.claim();
});
```
## 用户提示更新
### 方案一:立即更新(推荐用于开发环境)
```javascript
// 主线程
let newWorker = null;
navigator.serviceWorker.register('/sw.js').then(registration => {
registration.addEventListener('updatefound', () => {
newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 显示更新提示
showUpdateBar();
}
});
});
});
// 用户点击更新按钮
function applyUpdate() {
if (newWorker) {
newWorker.postMessage({ action: 'skipWaiting' });
}
}
// sw.js 中监听
self.addEventListener('message', event => {
if (event.data.action === 'skipWaiting') {
self.skipWaiting();
}
});
```
### 方案二:优雅更新(推荐用于生产环境)
```javascript
// 监听 controller 变化
navigator.serviceWorker.addEventListener('controllerchange', () => {
// Service Worker 已切换,建议用户刷新页面
window.location.reload();
});
```
### 方案三:定期自动更新
```javascript
// 每 60 分钟检查一次更新
setInterval(() => {
navigator.serviceWorker.ready.then(registration => {
registration.update();
});
}, 60 * 60 * 1000);
```
## 更新通知 UI 示例
```javascript
function showUpdateBar() {
const updateBar = document.createElement('div');
updateBar.className = 'update-bar';
updateBar.innerHTML = `
<span>发现新版本,是否更新?</span>
<button id="update-btn">立即更新</button>
<button id="dismiss-btn">稍后</button>
`;
document.body.appendChild(updateBar);
document.getElementById('update-btn').addEventListener('click', () => {
applyUpdate();
updateBar.remove();
});
document.getElementById('dismiss-btn').addEventListener('click', () => {
updateBar.remove();
});
}
```
## 最佳实践
### 1. 版本控制
```javascript
// 使用版本号管理缓存
const CACHE_VERSION = 'v2.1.0';
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;
```
### 2. 增量更新
```javascript
// 只更新变化的资源
const urlsToCache = [
'/',
'/index.html?v=2', // 添加版本号
'/app.js?v=2.1',
'/styles.css?v=2.1'
];
```
### 3. 更新策略选择
| 策略 | 适用场景 | 用户体验 |
|------|----------|----------|
| 立即更新 | 开发环境、紧急修复 | 强制刷新 |
| 优雅更新 | 生产环境 | 用户可控 |
| 静默更新 | 非关键更新 | 无感知 |
### 4. 避免更新陷阱
```javascript
// ❌ 错误:缓存 Service Worker 本身
// Service Worker 文件不应该被缓存
// ✅ 正确:确保 Service Worker 文件不被缓存
// 在服务器配置中设置:
// Cache-Control: no-cache, no-store, must-revalidate
```
## 调试技巧
```javascript
// 查看当前 Service Worker 状态
navigator.serviceWorker.ready.then(registration => {
console.log('Active:', registration.active);
console.log('Installing:', registration.installing);
console.log('Waiting:', registration.waiting);
});
// 强制更新
navigator.serviceWorker.getRegistration().then(reg => {
reg.update();
});
// 注销 Service Worker
navigator.serviceWorker.getRegistration().then(reg => {
reg.unregister();
});
```
## 总结
1. **自动检测**:浏览器自动检测 Service Worker 文件变化
2. **安装等待**:新版本安装后默认进入等待状态
3. **激活接管**:旧页面关闭后新版本激活
4. **缓存清理**:activate 阶段清理旧版本缓存
5. **用户提示**:提供友好的更新提示机制
服务端 · 3月7日 12:06
Service Worker 的生命周期包含哪些阶段?## Service Worker 生命周期详解
Service Worker 的生命周期是理解其工作原理的核心,包含以下关键阶段:
## 1. 注册阶段(Registration)
```javascript
// 在主线程中注册
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered:', registration);
})
.catch(error => {
console.log('SW registration failed:', error);
});
}
```
## 2. 安装阶段(Installation)
- 浏览器下载 Service Worker 脚本
- 触发 `install` 事件
- 适合进行静态资源的预缓存
```javascript
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/',
'/styles.css',
'/app.js'
]);
})
);
// 立即激活新的 Service Worker
self.skipWaiting();
});
```
## 3. 等待阶段(Waiting)
- 新的 Service Worker 安装完成后进入等待状态
- 等待旧的 Service Worker 控制的所有页面关闭
- 可以通过 `skipWaiting()` 跳过等待
## 4. 激活阶段(Activation)
- 旧的 Service Worker 被替换后触发 `activate` 事件
- 适合清理旧缓存
- 此时 Service Worker 开始控制页面
```javascript
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== 'v1')
.map(name => caches.delete(name))
);
})
);
// 立即接管页面
self.clients.claim();
});
```
## 5. 运行阶段(Running/Idle)
- 监听 `fetch`、`push`、`sync` 等事件
- 处理网络请求和后台任务
- 浏览器可能随时终止 Service Worker 以节省资源
## 6. 终止阶段(Termination)
- 浏览器自动回收资源
- 下次事件触发时重新启动
- 保持状态不丢失(但全局变量会重置)
## 状态转换图
```
注册 → 下载 → 安装中 → 安装完成 → 等待中 → 激活中 → 激活完成 → 空闲/运行
↓
被新 SW 替换
```
## 最佳实践
1. **版本管理**:使用版本号管理缓存(如 'v1', 'v2')
2. **优雅升级**:通过页面刷新触发新 Service Worker 激活
3. **缓存清理**:在 activate 阶段清理过期缓存
4. **错误处理**:每个阶段都要添加适当的错误处理
服务端 · 3月7日 12:05
Service Worker 如何实现离线访问功能?## Service Worker 离线访问实现详解
离线访问是 Service Worker 最核心的功能之一,让 Web 应用在无网络环境下仍能正常工作。
## 核心原理
Service Worker 作为网络代理,拦截所有 HTTP 请求,根据缓存策略决定从缓存返回还是请求网络。
## 实现步骤
### 1. 注册 Service Worker
```javascript
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered:', registration.scope);
})
.catch(error => {
console.log('SW registration failed:', error);
});
});
}
```
### 2. 预缓存核心资源
```javascript
// sw.js
const CACHE_NAME = 'offline-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/icons/icon-192x192.png',
'/offline.html' // 离线 fallback 页面
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Cache opened');
return cache.addAll(urlsToCache);
})
.catch(err => console.error('Cache failed:', err))
);
self.skipWaiting();
});
```
### 3. 拦截请求并返回缓存
```javascript
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;
})
.catch(() => {
// 网络失败,返回离线页面
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
});
})
);
});
```
### 4. 清理旧缓存
```javascript
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
self.clients.claim();
});
```
## 离线页面设计
```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;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
background: #f5f5f5;
}
.offline-container {
text-align: center;
padding: 40px;
}
.offline-icon {
font-size: 64px;
margin-bottom: 20px;
}
h1 { color: #333; }
p { color: #666; }
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="offline-container">
<div class="offline-icon">📡</div>
<h1>您当前处于离线状态</h1>
<p>请检查网络连接后重试</p>
<button onclick="location.reload()">重新加载</button>
</div>
</body>
</html>
```
## 高级离线策略
### 1. 网络优先 + 缓存回退
```javascript
self.addEventListener('fetch', event => {
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(() => caches.match(event.request))
);
});
```
### 2. 缓存优先 + 后台更新
```javascript
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
const fetchPromise = fetch(event.request).then(networkResponse => {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
});
return response || fetchPromise;
})
);
});
```
## 检测网络状态
```javascript
// 主线程中检测
window.addEventListener('online', () => {
console.log('网络已连接');
});
window.addEventListener('offline', () => {
console.log('网络已断开');
});
// 检查当前状态
if (navigator.onLine) {
console.log('在线');
} else {
console.log('离线');
}
```
## 最佳实践
1. **核心资源优先**:确保 HTML、CSS、JS 等核心资源被缓存
2. **优雅降级**:网络失败时提供友好的离线页面
3. **缓存更新**:定期更新缓存,避免用户长期看到旧版本
4. **缓存清理**:及时清理过期缓存,避免存储溢出
5. **测试验证**:使用 Chrome DevTools 的 Network 面板模拟离线环境
服务端 · 3月7日 12:05
Service Worker 调试有哪些常用方法和工具?## Service Worker 调试方法与工具详解
Service Worker 运行在浏览器后台,调试相对复杂。掌握正确的调试方法和工具对于开发至关重要。
## Chrome DevTools 调试
### 1. Application 面板
Chrome DevTools 的 Application 面板是调试 Service Worker 的主要工具。
```
DevTools → Application → Service Workers
```
**主要功能:**
- 查看已注册的 Service Worker
- 查看当前状态(activated, installing, waiting 等)
- 手动触发更新(Update)
- 注销 Service Worker(Unregister)
- 模拟离线环境(Offline checkbox)
- 绕过网络(Bypass for network)
### 2. 网络面板调试
```javascript
// 查看 Service Worker 拦截的请求
// DevTools → Network → 查看 Size 列
// (from ServiceWorker) 表示从缓存返回
// (from disk cache) 表示从磁盘缓存返回
// (from memory cache) 表示从内存缓存返回
```
### 3. Console 面板
```javascript
// Service Worker 中的 console.log 会显示在 DevTools Console
// 注意:需要勾选 "Preserve log" 以保留刷新前的日志
// 查看当前 Service Worker 状态
navigator.serviceWorker.ready.then(registration => {
console.log('Service Worker 状态:', registration);
});
```
## 常用调试技巧
### 1. 强制更新 Service Worker
```javascript
// 方法1:手动更新
navigator.serviceWorker.getRegistration().then(reg => {
reg.update();
});
// 方法2:硬刷新(Ctrl+Shift+R 或 Cmd+Shift+R)
// 会绕过 Service Worker,安装新版本
// 方法3:DevTools Application 面板点击 Update
```
### 2. 查看缓存内容
```javascript
// 查看所有缓存
async function inspectCaches() {
const cacheNames = await caches.keys();
console.log('缓存列表:', cacheNames);
for (const name of cacheNames) {
const cache = await caches.open(name);
const requests = await cache.keys();
console.log(`缓存 ${name} 内容:`, requests.map(r => r.url));
}
}
inspectCaches();
```
### 3. 清除缓存
```javascript
// 清除所有缓存
async function clearAllCaches() {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
console.log('所有缓存已清除');
}
// 清除特定缓存
async function clearCache(cacheName) {
await caches.delete(cacheName);
console.log(`缓存 ${cacheName} 已清除`);
}
```
### 4. 模拟离线环境
```javascript
// 方法1:DevTools Network 面板
// 选择 "Offline" 或设置自定义网络条件
// 方法2:代码中检测
window.addEventListener('offline', () => {
console.log('进入离线模式');
});
// 方法3:手动触发离线状态
// navigator.connection 可以查看网络状态
console.log('网络状态:', navigator.connection);
```
### 5. 调试 Fetch 事件
```javascript
// sw.js 中添加详细日志
self.addEventListener('fetch', event => {
console.log('Fetch 请求:', {
url: event.request.url,
method: event.request.method,
mode: event.request.mode,
destination: event.request.destination
});
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
console.log('缓存命中:', event.request.url);
return response;
}
console.log('缓存未命中,请求网络:', event.request.url);
return fetch(event.request);
})
);
});
```
## 高级调试技术
### 1. 使用 Chrome 的 Service Worker 内部页面
```
chrome://serviceworker-internals/
```
可以查看所有 Service Worker 的详细信息,包括:
- 注册信息
- 控制台日志
- 网络请求
- 存储使用情况
### 2. 使用 Workbox 调试
如果使用 Workbox,可以启用详细日志:
```javascript
// 启用 Workbox 调试
workbox.setConfig({
debug: true
});
// 查看 Workbox 日志
workbox.core.setLogLevel(workbox.core.LOG_LEVELS.debug);
```
### 3. 断点调试
```javascript
// 在 Service Worker 代码中设置 debugger
self.addEventListener('fetch', event => {
debugger; // DevTools 会在这里暂停
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
```
### 4. 使用 console.table 查看缓存
```javascript
async function logCacheContents() {
const cache = await caches.open('my-cache');
const requests = await cache.keys();
const tableData = await Promise.all(
requests.map(async request => {
const response = await cache.match(request);
return {
URL: request.url,
Status: response.status,
Type: response.headers.get('content-type'),
Size: response.headers.get('content-length')
};
})
);
console.table(tableData);
}
```
## 常见问题排查
### 1. Service Worker 未注册
```javascript
// 检查浏览器支持
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('注册成功:', reg))
.catch(err => console.error('注册失败:', err));
} else {
console.error('浏览器不支持 Service Worker');
}
```
### 2. HTTPS 问题
```javascript
// 检查是否 HTTPS(localhost 除外)
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
console.error('Service Worker 需要 HTTPS 环境');
}
```
### 3. 缓存未更新
```javascript
// 确保每次更新 Service Worker 时更改缓存名称
const CACHE_NAME = 'my-app-v2'; // 更新版本号
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
});
```
### 4. 跨域资源缓存问题
```javascript
// CORS 资源需要特殊处理
self.addEventListener('fetch', event => {
if (event.request.mode === 'cors') {
// 跨域请求使用 no-cors 模式缓存
event.respondWith(
fetch(event.request, { mode: 'no-cors' })
.then(response => {
return caches.open('cors-cache').then(cache => {
cache.put(event.request, response.clone());
return response;
});
})
);
}
});
```
## 调试清单
### 开发阶段检查项
- [ ] Service Worker 成功注册
- [ ] Install 事件正常触发
- [ ] 静态资源正确缓存
- [ ] Fetch 事件正确拦截
- [ ] 缓存策略按预期工作
- [ ] Activate 事件清理旧缓存
- [ ] 离线功能正常
### 生产环境检查项
- [ ] Service Worker 文件不被缓存
- [ ] 缓存版本控制正确
- [ ] 更新机制正常工作
- [ ] 错误处理完善
- [ ] 降级方案有效
## 推荐工具
| 工具 | 用途 |
|------|------|
| Chrome DevTools | 主要调试工具 |
| Workbox | Service Worker 库,带调试功能 |
| Lighthouse | PWA 审核和性能分析 |
| PWA Builder | 验证 PWA 配置 |
| Web Server for Chrome | 本地 HTTPS 测试 |
## 调试最佳实践
1. **使用 Chrome DevTools**:充分利用 Application 面板
2. **添加详细日志**:开发和生产环境使用不同日志级别
3. **版本控制**:每次更新更改缓存名称
4. **测试离线**:定期测试离线功能
5. **监控错误**:使用错误追踪服务监控 Service Worker 错误
服务端 · 3月7日 12:05
Service Worker 如何实现跨域资源的缓存?## Service Worker 跨域资源缓存详解
跨域资源缓存是 Service Worker 中的常见问题,由于浏览器的同源策略(Same-Origin Policy),跨域请求的缓存需要特殊处理。
## 跨域请求的基本概念
### 什么是跨域请求
```javascript
// 同源请求(Same-Origin)
// 当前页面: https://example.com/page.html
fetch('/api/data'); // ✅ 同源
fetch('https://example.com/api'); // ✅ 同源
// 跨域请求(Cross-Origin)
fetch('https://api.other.com/data'); // ❌ 跨域(不同域名)
fetch('https://cdn.example.com/file'); // ❌ 跨域(不同子域)
```
### CORS 和 Opaque Response
```javascript
// CORS 请求 - 服务器允许跨域
fetch('https://api.example.com/data', {
mode: 'cors' // 默认模式
});
// 响应包含完整的 headers 和 body
// No-CORS 请求 - 服务器不允许跨域
fetch('https://cdn.example.com/image.jpg', {
mode: 'no-cors'
});
// 响应是 "opaque",无法读取内容,但可以缓存
```
## 跨域资源缓存方案
### 方案1:使用 CORS 模式(推荐)
```javascript
// sw.js
// 服务器需要设置 CORS 头:Access-Control-Allow-Origin: *
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 处理跨域 API 请求
if (url.hostname === 'api.example.com') {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response;
}
// 使用 cors 模式请求
return fetch(event.request, { mode: 'cors' })
.then(networkResponse => {
// 检查响应是否成功
if (!networkResponse || networkResponse.status !== 200) {
return networkResponse;
}
// 缓存响应
const responseToCache = networkResponse.clone();
caches.open('api-cache').then(cache => {
cache.put(event.request, responseToCache);
});
return networkResponse;
})
.catch(error => {
console.error('跨域请求失败:', error);
// 返回离线降级内容
return new Response(JSON.stringify({
error: 'Network error',
offline: true
}), {
headers: { 'Content-Type': 'application/json' }
});
});
})
);
}
});
```
### 方案2:使用 No-CORS 模式缓存 Opaque 响应
```javascript
// sw.js
// 适用于 CDN 资源、图片等不需要读取内容的资源
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 处理 CDN 图片资源
if (url.hostname === 'cdn.example.com') {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response;
}
// 使用 no-cors 模式
return fetch(event.request, { mode: 'no-cors' })
.then(networkResponse => {
// Opaque 响应可以缓存,但无法读取状态和内容
const responseToCache = networkResponse.clone();
caches.open('cdn-cache').then(cache => {
cache.put(event.request, responseToCache);
});
return networkResponse;
});
})
);
}
});
```
**Opaque 响应的特点:**
- 状态码始终为 0
- 无法读取响应头
- 无法读取响应体
- 占用缓存配额(约是实际大小的 7 倍)
### 方案3:使用 CORS 代理
```javascript
// 如果第三方 API 不支持 CORS,可以搭建代理服务器
// sw.js
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 代理跨域请求
if (url.pathname.startsWith('/proxy/')) {
const targetUrl = url.pathname.replace('/proxy/', '');
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response;
}
// 通过同域代理转发请求
return fetch(`/api/proxy?url=${encodeURIComponent(targetUrl)}`)
.then(networkResponse => {
const responseToCache = networkResponse.clone();
caches.open('proxy-cache').then(cache => {
cache.put(event.request, responseToCache);
});
return networkResponse;
});
})
);
}
});
```
### 方案4:预缓存跨域资源
```javascript
// sw.js
const CROSS_ORIGIN_CACHE = 'cross-origin-v1';
// 需要预缓存的跨域资源
const crossOriginResources = [
'https://fonts.googleapis.com/css?family=Roboto',
'https://fonts.gstatic.com/s/roboto/v20/font.woff2',
'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CROSS_ORIGIN_CACHE).then(cache => {
// 使用 addAll 预缓存(自动处理 CORS)
return cache.addAll(crossOriginResources);
})
);
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 匹配跨域资源
if (crossOriginResources.some(resource => event.request.url.includes(resource))) {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
}
});
```
## 实际应用示例
### 示例1:缓存 Google Fonts
```javascript
// sw.js
const FONT_CACHE = 'fonts-v1';
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Google Fonts CSS(CORS 支持)
if (url.hostname === 'fonts.googleapis.com') {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(fetchResponse => {
return caches.open(FONT_CACHE).then(cache => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
}
// Google Fonts 字体文件(Opaque 响应)
if (url.hostname === 'fonts.gstatic.com') {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request, { mode: 'no-cors' })
.then(fetchResponse => {
return caches.open(FONT_CACHE).then(cache => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
}
});
```
### 示例2:缓存 CDN 资源
```javascript
// sw.js
const CDN_CACHE = 'cdn-v1';
const CDN_HOSTS = [
'cdn.jsdelivr.net',
'unpkg.com',
'cdnjs.cloudflare.com'
];
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 检查是否是 CDN 资源
if (CDN_HOSTS.includes(url.hostname)) {
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
return response;
}
// CDN 资源使用 no-cors 模式
return fetch(event.request, { mode: 'no-cors' })
.then(fetchResponse => {
// 只缓存成功的响应
if (fetchResponse.type === 'opaque' || fetchResponse.ok) {
const responseToCache = fetchResponse.clone();
caches.open(CDN_CACHE).then(cache => {
cache.put(event.request, responseToCache);
});
}
return fetchResponse;
})
.catch(() => {
// 离线时返回降级
return new Response('CDN resource unavailable offline');
});
})
);
}
});
```
### 示例3:处理跨域图片
```javascript
// sw.js
const IMAGE_CACHE = 'images-v1';
self.addEventListener('fetch', event => {
const request = event.request;
// 处理图片请求
if (request.destination === 'image') {
event.respondWith(
caches.match(request).then(response => {
if (response) {
return response;
}
// 判断是否是跨域请求
const url = new URL(request.url);
const isCrossOrigin = url.origin !== location.origin;
const fetchOptions = isCrossOrigin ? { mode: 'no-cors' } : {};
return fetch(request, fetchOptions).then(fetchResponse => {
// 检查响应是否有效
if (!fetchResponse || fetchResponse.status === 0) {
// Opaque 响应或失败
return fetchResponse;
}
const responseToCache = fetchResponse.clone();
caches.open(IMAGE_CACHE).then(cache => {
cache.put(request, responseToCache);
});
return fetchResponse;
}).catch(() => {
// 返回占位图
return caches.match('/images/placeholder.png');
});
})
);
}
});
```
## 跨域缓存的注意事项
### 1. Opaque 响应的存储成本
```javascript
// Opaque 响应的存储成本约为实际大小的 7 倍
// 需要谨慎使用,避免超出存储配额
async function checkOpaqueResponseSize() {
const cache = await caches.open('cdn-cache');
const requests = await cache.keys();
let totalSize = 0;
for (const request of requests) {
const response = await cache.match(request);
if (response.type === 'opaque') {
// Opaque 响应无法获取实际大小
// 需要预估或限制数量
totalSize += 1024 * 1024; // 假设每个 1MB
}
}
console.log(`预估 Opaque 响应总大小: ${totalSize / 1024 / 1024} MB`);
}
```
### 2. 缓存清理策略
```javascript
// 定期清理跨域缓存
async function cleanCrossOriginCache() {
const cache = await caches.open('cdn-cache');
const requests = await cache.keys();
// 只保留最近访问的 50 个资源
if (requests.length > 50) {
const toDelete = requests.slice(0, requests.length - 50);
await Promise.all(
toDelete.map(request => cache.delete(request))
);
}
}
// 在 activate 事件中执行清理
self.addEventListener('activate', event => {
event.waitUntil(cleanCrossOriginCache());
});
```
### 3. 错误处理
```javascript
// 跨域请求的错误处理
async function fetchWithFallback(request, isCrossOrigin) {
try {
const fetchOptions = isCrossOrigin ? { mode: 'no-cors' } : {};
const response = await fetch(request, fetchOptions);
// Opaque 响应 status 为 0,需要特殊处理
if (response.type === 'opaque' || response.ok) {
return response;
}
throw new Error(`HTTP ${response.status}`);
} catch (error) {
console.error('Fetch failed:', error);
// 返回缓存或降级响应
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// 返回默认响应
return new Response('Resource unavailable', { status: 503 });
}
}
```
## 最佳实践
1. **优先使用 CORS**:如果服务器支持 CORS,优先使用 cors 模式
2. **限制 Opaque 响应数量**:Opaque 响应占用更多存储空间
3. **合理设置缓存策略**:跨域资源变化频繁时需要设置过期时间
4. **提供降级方案**:跨域资源加载失败时提供替代内容
5. **监控存储使用**:定期检查缓存大小,避免超出配额
服务端 · 3月7日 12:05
Service Worker 中的 Cache Storage API 如何使用?Cache Storage API 是 Service Worker 的核心 API,用于管理请求/响应对的缓存,实现离线访问和性能优化。
## 基本概念
Cache Storage API 提供了类似数据库的接口来存储和检索网络请求和响应:
```javascript
// Cache Storage 是 caches 全局对象
console.log(caches); // CacheStorage 对象
// 主要方法
caches.open(cacheName) // 打开/创建缓存
caches.match(request) // 跨缓存匹配请求
caches.keys() // 获取所有缓存名称
caches.delete(cacheName) // 删除指定缓存
```
## 核心方法详解
### 1. 打开和创建缓存
```javascript
// 打开缓存(如果不存在则创建)
async function openCache() {
const cache = await caches.open('my-cache-v1');
console.log('缓存已打开:', cache);
return cache;
}
// 使用版本号管理缓存
const CACHE_VERSION = 'v2';
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;
```
### 2. 添加资源到缓存
```javascript
// 方法1:使用 add() - 获取并缓存单个请求
async function addToCache(url) {
const cache = await caches.open('my-cache');
await cache.add(url); // 自动 fetch 并缓存
console.log(`已缓存: ${url}`);
}
// 方法2:使用 addAll() - 批量缓存
async function addMultipleToCache(urls) {
const cache = await caches.open('my-cache');
await cache.addAll(urls);
console.log(`已批量缓存 ${urls.length} 个资源`);
}
// 使用示例
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/images/logo.png'
];
addMultipleToCache(urlsToCache);
// 方法3:使用 put() - 手动存储请求/响应对
async function putInCache(request, response) {
const cache = await caches.open('my-cache');
await cache.put(request, response);
}
// 示例:缓存自定义响应
const request = new Request('/custom-data');
const response = new Response(JSON.stringify({ data: 'value' }), {
headers: { 'Content-Type': 'application/json' }
});
putInCache(request, response);
```
### 3. 从缓存中检索
```javascript
// 方法1:match() - 匹配单个请求
async function getFromCache(url) {
const cache = await caches.open('my-cache');
const response = await cache.match(url);
if (response) {
console.log('缓存命中:', url);
return response;
}
console.log('缓存未命中:', url);
return null;
}
// 方法2:跨所有缓存匹配
caches.match('/api/data').then(response => {
if (response) {
// 在所有缓存中找到匹配
return response;
}
});
// 方法3:matchAll() - 获取所有匹配
async function getAllMatches(url) {
const cache = await caches.open('my-cache');
const responses = await cache.matchAll(url);
return responses;
}
```
### 4. 删除缓存中的资源
```javascript
// 删除特定请求
async function deleteFromCache(url) {
const cache = await caches.open('my-cache');
const deleted = await cache.delete(url);
console.log(deleted ? '删除成功' : '资源不存在');
}
// 删除整个缓存
async function deleteCache(cacheName) {
const deleted = await caches.delete(cacheName);
console.log(deleted ? '缓存已删除' : '缓存不存在');
}
// 清理旧缓存(常用在 activate 事件)
async function cleanOldCaches(currentCacheName) {
const cacheNames = await caches.keys();
const oldCaches = cacheNames.filter(name => name !== currentCacheName);
await Promise.all(
oldCaches.map(name => caches.delete(name))
);
console.log(`已清理 ${oldCaches.length} 个旧缓存`);
}
```
### 5. 查看缓存内容
```javascript
// 列出所有缓存
async function listAllCaches() {
const cacheNames = await caches.keys();
console.log('所有缓存:', cacheNames);
return cacheNames;
}
// 查看特定缓存中的所有请求
async function listCacheContents(cacheName) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
console.log(`缓存 "${cacheName}" 内容:`);
requests.forEach(request => {
console.log(' -', request.url);
});
return requests;
}
```
## 实际应用场景
### 场景1:预缓存静态资源
```javascript
// sw.js
const STATIC_CACHE = 'static-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/manifest.json',
'/icons/icon-192x192.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting())
);
});
```
### 场景2:动态缓存运行时资源
```javascript
// sw.js
const DYNAMIC_CACHE = 'dynamic-v1';
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
// 返回缓存或请求网络
return response || fetch(event.request).then(fetchResponse => {
// 动态缓存新资源
return caches.open(DYNAMIC_CACHE).then(cache => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
});
```
### 场景3:缓存 API 响应
```javascript
// sw.js
const API_CACHE = 'api-v1';
const API_MAX_AGE = 5 * 60 * 1000; // 5分钟
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/')) {
event.respondWith(
caches.open(API_CACHE).then(cache => {
return cache.match(event.request).then(cachedResponse => {
// 检查缓存是否过期
if (cachedResponse) {
const cachedTime = new Date(
cachedResponse.headers.get('sw-cached-time')
).getTime();
if (Date.now() - cachedTime < API_MAX_AGE) {
return cachedResponse;
}
}
// 请求网络并缓存
return fetch(event.request).then(networkResponse => {
const clonedResponse = networkResponse.clone();
// 添加自定义缓存时间头
const headers = new Headers(clonedResponse.headers);
headers.set('sw-cached-time', new Date().toISOString());
const modifiedResponse = new Response(
clonedResponse.body,
{ ...clonedResponse, headers }
);
cache.put(event.request, modifiedResponse);
return networkResponse;
});
});
})
);
}
});
```
### 场景4:缓存清理策略
```javascript
// sw.js
const CACHE_MAX_ITEMS = 50; // 最大缓存数量
async function trimCache(cacheName, maxItems) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
if (requests.length > maxItems) {
// 删除最旧的缓存
const toDelete = requests.slice(0, requests.length - maxItems);
await Promise.all(
toDelete.map(request => cache.delete(request))
);
console.log(`已清理 ${toDelete.length} 个旧缓存项`);
}
}
// 定期清理
self.addEventListener('activate', event => {
event.waitUntil(
trimCache('dynamic-v1', CACHE_MAX_ITEMS)
);
});
```
## 注意事项
### 1. Response 只能使用一次
```javascript
// ❌ 错误:Response 只能读取一次
const response = await fetch('/api/data');
await cache.put(request, response); // 存储到缓存
return response; // 已经消耗,无法返回
// ✅ 正确:使用 clone()
const response = await fetch('/api/data');
await cache.put(request, response.clone()); // 存储副本
return response; // 返回原始响应
```
### 2. 存储限制
```javascript
// 检查存储配额
async function checkStorageQuota() {
if ('storage' in navigator && 'estimate' in navigator.storage) {
const estimate = await navigator.storage.estimate();
console.log(`已使用: ${estimate.usage} bytes`);
console.log(`配额: ${estimate.quota} bytes`);
console.log(`使用率: ${(estimate.usage / estimate.quota * 100).toFixed(2)}%`);
}
}
```
### 3. 缓存键匹配规则
```javascript
// URL 必须完全匹配(包括查询参数)
await cache.match('/api/data'); // ✅ 匹配 /api/data
await cache.match('/api/data?id=1'); // ❌ 不匹配 /api/data
// 使用 ignoreSearch 选项忽略查询参数
await cache.match('/api/data?id=1', {
ignoreSearch: true // 忽略查询参数
});
// 其他匹配选项
await cache.match(request, {
ignoreSearch: true, // 忽略查询参数
ignoreMethod: true, // 忽略 HTTP 方法
ignoreVary: true // 忽略 Vary 头
});
```
## 最佳实践
1. **版本控制**:使用版本号管理缓存,便于更新
2. **定期清理**:在 activate 事件中清理过期缓存
3. **存储限制**:注意浏览器的存储配额限制
4. **错误处理**:添加适当的错误处理和降级方案
5. **性能优化**:避免缓存过多数据,定期清理
服务端 · 3月6日 22:58
Service Worker 中的 Background Sync 是什么?如何使用?## Service Worker Background Sync 详解
Background Sync(后台同步)是 Service Worker 的重要功能,允许在网络恢复后自动执行延迟的任务,特别适用于离线场景下的数据同步。
## 核心概念
Background Sync 解决了什么问题:
- 用户在离线状态下提交表单或操作
- 网络恢复后自动同步数据到服务器
- 无需用户手动重试操作
## 使用场景
1. **表单提交**:离线时保存表单,联网后自动提交
2. **消息发送**:离线时保存消息,联网后自动发送
3. **数据同步**:离线操作的数据在联网后同步
4. **日志上传**:离线日志在网络恢复后批量上传
## 实现步骤
### 1. 注册同步事件
```javascript
// main.js
async function registerBackgroundSync(tag) {
const registration = await navigator.serviceWorker.ready;
try {
await registration.sync.register(tag);
console.log(`后台同步已注册: ${tag}`);
} catch (error) {
console.error('后台同步注册失败:', error);
}
}
// 使用示例
async function submitForm(data) {
// 先保存到本地数据库
await saveToIndexedDB('pending-forms', data);
// 注册后台同步
await registerBackgroundSync('sync-forms');
// 如果在线,立即尝试发送
if (navigator.onLine) {
try {
await sendToServer(data);
await removeFromIndexedDB('pending-forms', data.id);
} catch (error) {
console.log('发送失败,将在网络恢复后重试');
}
}
}
```
### 2. Service Worker 处理同步
```javascript
// sw.js
// 监听 sync 事件
self.addEventListener('sync', event => {
console.log('后台同步触发:', event.tag);
if (event.tag === 'sync-forms') {
event.waitUntil(syncPendingForms());
} else if (event.tag === 'sync-messages') {
event.waitUntil(syncPendingMessages());
}
});
// 同步待提交的表单
async function syncPendingForms() {
const pendingForms = await getFromIndexedDB('pending-forms');
for (const form of pendingForms) {
try {
await fetch('/api/submit-form', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form)
});
// 同步成功,从待处理列表移除
await removeFromIndexedDB('pending-forms', form.id);
// 通知用户
self.registration.showNotification('同步成功', {
body: '您的表单已成功提交',
icon: '/icons/success.png'
});
} catch (error) {
console.error('表单同步失败:', error);
// 失败后会自动重试
throw error;
}
}
}
// 同步待发送的消息
async function syncPendingMessages() {
const messages = await getFromIndexedDB('pending-messages');
const syncPromises = messages.map(async message => {
try {
await fetch('/api/send-message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(message)
});
await removeFromIndexedDB('pending-messages', message.id);
} catch (error) {
console.error('消息同步失败:', error);
throw error;
}
});
return Promise.all(syncPromises);
}
```
### 3. IndexedDB 辅助函数
```javascript
// db.js - IndexedDB 操作封装
const DB_NAME = 'background-sync-db';
const DB_VERSION = 1;
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = event => {
const db = event.target.result;
// 创建存储对象
if (!db.objectStoreNames.contains('pending-forms')) {
db.createObjectStore('pending-forms', { keyPath: 'id' });
}
if (!db.objectStoreNames.contains('pending-messages')) {
db.createObjectStore('pending-messages', { keyPath: 'id' });
}
};
});
}
async function saveToIndexedDB(storeName, data) {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function getFromIndexedDB(storeName) {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function removeFromIndexedDB(storeName, id) {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
```
## 完整示例:离线表单提交
```javascript
// form-handler.js
class OfflineFormHandler {
constructor(formId) {
this.form = document.getElementById(formId);
this.setupEventListeners();
}
setupEventListeners() {
this.form.addEventListener('submit', async (e) => {
e.preventDefault();
await this.handleSubmit();
});
}
async handleSubmit() {
const formData = new FormData(this.form);
const data = {
id: Date.now().toString(),
timestamp: Date.now(),
...Object.fromEntries(formData)
};
// 显示提交中状态
this.showStatus('保存中...');
try {
// 保存到本地
await saveToIndexedDB('pending-forms', data);
// 注册后台同步
await this.registerSync('sync-forms');
// 尝试立即发送
if (navigator.onLine) {
await this.sendImmediately(data);
} else {
this.showStatus('已离线保存,联网后自动同步');
}
this.form.reset();
} catch (error) {
this.showStatus('保存失败: ' + error.message);
}
}
async registerSync(tag) {
const registration = await navigator.serviceWorker.ready;
if ('sync' in registration) {
await registration.sync.register(tag);
} else {
// 降级方案:立即发送或提示用户
console.log('浏览器不支持后台同步');
}
}
async sendImmediately(data) {
try {
await fetch('/api/submit-form', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
await removeFromIndexedDB('pending-forms', data.id);
this.showStatus('提交成功!');
} catch (error) {
this.showStatus('提交失败,将在后台重试');
}
}
showStatus(message) {
const statusEl = document.getElementById('form-status');
statusEl.textContent = message;
setTimeout(() => {
statusEl.textContent = '';
}, 3000);
}
}
// 初始化
const formHandler = new OfflineFormHandler('my-form');
```
## 定期后台同步(Periodic Background Sync)
```javascript
// 请求定期同步权限
async function requestPeriodicSync() {
const registration = await navigator.serviceWorker.ready;
try {
await registration.periodicSync.register('daily-sync', {
minInterval: 24 * 60 * 60 * 1000 // 最少 24 小时
});
console.log('定期同步已注册');
} catch (error) {
console.error('定期同步注册失败:', error);
}
}
// sw.js 处理定期同步
self.addEventListener('periodicsync', event => {
if (event.tag === 'daily-sync') {
event.waitUntil(performDailySync());
}
});
async function performDailySync() {
// 执行每日同步任务
const data = await fetch('/api/daily-data').then(r => r.json());
await caches.open('daily-cache').then(cache => {
cache.put('/daily-data', new Response(JSON.stringify(data)));
});
}
```
## 浏览器兼容性
| 功能 | Chrome | Firefox | Safari | Edge |
|------|--------|---------|--------|------|
| Background Sync | ✅ | ❌ | ❌ | ✅ |
| Periodic Sync | ✅ | ❌ | ❌ | ✅ |
## 最佳实践
1. **数据持久化**:使用 IndexedDB 存储待同步数据
2. **错误处理**:同步失败时抛出错误以触发重试
3. **幂等性设计**:确保重复执行不会产生副作用
4. **用户反馈**:同步完成后通知用户
5. **降级方案**:为不支持的浏览器提供替代方案
```javascript
// 降级方案示例
async function syncWithFallback() {
const registration = await navigator.serviceWorker.ready;
if ('sync' in registration) {
// 使用后台同步
await registration.sync.register('sync-data');
} else {
// 降级:立即尝试或提示用户
if (navigator.onLine) {
await syncData();
} else {
alert('您处于离线状态,请联网后重试');
}
}
}
```
服务端 · 3月6日 22:01