乐闻世界logo
搜索文章和话题

Service Worker

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