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

面试题手册

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. 网络代理能力// Service Worker 可以拦截所有网络请求self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) );});2. 独立于页面的生命周期安装后持续运行,即使所有页面关闭浏览器可自动重启处理事件适合后台任务(推送通知、后台同步)3. 渐进式 Web 应用核心// 推送通知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. 计算密集型任务// main.jsconst worker = new Worker('worker.js');worker.postMessage({ numbers: [1, 2, 3, 4, 5] });worker.onmessage = event => { console.log('Result:', event.data);};// worker.jsself.onmessage = event => { const { numbers } = event.data; const result = numbers.reduce((a, b) => a + b, 0); self.postMessage(result);};2. 页面级生命周期创建它的页面关闭时自动终止适合处理一次性计算任务无法执行后台持续任务3. 多种类型// 专用 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 适用场景离线应用:缓存资源,提供离线访问网络优化:智能缓存策略,减少网络请求推送通知:接收服务器推送的消息后台同步:网络恢复后自动同步数据PWA 功能:添加到主屏幕、应用壳架构Web Worker 适用场景大数据处理:图片处理、视频编解码复杂计算:数学运算、数据分析实时数据处理:WebSocket 数据解析文件处理:大文件读取、压缩避免 UI 阻塞:耗时操作不卡顿界面代码示例对比Service Worker 示例// 注册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 示例// 创建 Workerconst worker = new Worker('/worker.js');// 发送消息worker.postMessage({ action: 'calculate', data: [1, 2, 3] });// 接收消息worker.onmessage = event => { console.log('Result:', event.data);};// worker.jsself.onmessage = event => { const result = performCalculation(event.data); self.postMessage(result);};总结Service Worker:网络代理专家,负责离线体验、后台任务Web Worker:计算性能专家,负责耗时运算、避免阻塞两者关系:互补而非替代,可在 PWA 中同时使用
阅读 0·3月7日 12:06

Service Worker 的安全注意事项有哪些?

Service Worker 安全注意事项详解Service Worker 作为浏览器后台运行的代理服务器,具有强大的能力,同时也带来了一些安全风险。了解这些安全问题对于开发安全的 Web 应用至关重要。1. HTTPS 要求为什么必须使用 HTTPS// Service Worker 只能在 HTTPS 环境下注册// 例外:localhost 允许 HTTPif ('serviceWorker' in navigator) { // 检查是否是安全上下文 if (window.isSecureContext) { navigator.serviceWorker.register('/sw.js'); } else { console.error('Service Worker 需要 HTTPS 环境'); }}安全风险:HTTP 环境下 Service Worker 可能被中间人攻击篡改攻击者可注入恶意 Service Worker 拦截所有网络请求用户的敏感数据可能被窃取检测安全上下文// 检查当前环境是否安全function checkSecureContext() { if (!window.isSecureContext) { console.warn('当前不是安全上下文,Service Worker 功能受限'); return false; } return true;}// 或者检查协议function isSecureProtocol() { return location.protocol === 'https:' || location.hostname === 'localhost';}2. 作用域限制作用域安全// Service Worker 只能控制其作用域内的页面// 注册时指定作用域navigator.serviceWorker.register('/sw.js', { scope: '/app/' // 只能控制 /app/ 下的页面});// 尝试访问作用域外的资源会失败// /app/page.html ✅ 可以控制// /other/page.html ❌ 无法控制路径遍历防护// ❌ 危险:不验证路径可能导致安全问题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 的影响// 设置 CSP 防止 XSS 攻击// 在 HTTP 响应头中设置:// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'// Service Worker 脚本本身需要符合 CSP// 内联脚本可能被阻止安全的 Service Worker 脚本加载// ✅ 推荐:加载外部脚本navigator.serviceWorker.register('/sw.js');// ❌ 避免:使用内联 Service Workerconst swCode = ` self.addEventListener('fetch', ...);`;const blob = new Blob([swCode], { type: 'application/javascript' });const url = URL.createObjectURL(blob);navigator.serviceWorker.register(url); // 可能违反 CSP4. 缓存安全敏感数据缓存// ❌ 危险:缓存敏感信息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); }) );});缓存清理安全// ✅ 安全的缓存清理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)防护防止恶意脚本注入// ❌ 危险:直接使用用户输入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)// 虽然 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. 权限控制最小权限原则// ✅ 只请求必要的权限// 推送通知权限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. 更新安全安全的更新机制// ✅ 验证 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. 数据泄露防护防止缓存泄露// ✅ 安全的缓存策略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 头[ ] 定期审计缓存内容[ ] 实施访问控制[ ] 记录安全日志安全测试建议// 安全测试清单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 安全要点:必须使用 HTTPS:防止中间人攻击合理设置作用域:限制控制能力不缓存敏感数据:防止数据泄露验证用户输入:防止 XSS 攻击实施 CSP:增强内容安全定期更新:修复安全漏洞最小权限:只请求必要权限
阅读 0·3月7日 12:06

如何在 Service Worker 中实现推送通知功能?

Service Worker 推送通知实现详解推送通知是 Service Worker 的重要功能之一,允许服务器向用户发送消息,即使用户没有打开网站。推送通知架构┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ 服务器 │────▶│ 推送服务 │────▶│ 浏览器 ││ (Web App) │ │(FCM/APNs等) │ │(Service Worker)└─────────────┘ └─────────────┘ └─────────────┘实现步骤1. 请求通知权限// main.jsasync 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. 订阅推送服务// main.jsasync 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 接收推送// 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. 处理通知点击// 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 库)// server.jsconst 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. 定时推送(定期后台同步)// 主线程请求定期同步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. 富媒体通知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. 取消订阅// main.jsasync 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 | ✅ | ❌ | ❌ | ✅ |最佳实践尊重用户:不要频繁发送通知,提供取消订阅选项及时更新:定期清理无效的订阅个性化内容:根据用户偏好发送相关通知优雅降级:不支持推送的浏览器提供替代方案HTTPS 必需:推送功能必须在 HTTPS 环境下运行
阅读 0·3月7日 12:06

Service Worker 的更新机制是怎样的?

Service Worker 更新机制详解Service Worker 的更新机制是其重要特性之一,理解它对于维护应用的稳定性和用户体验至关重要。更新触发条件当浏览器检测到 Service Worker 文件发生变化时(字节差异),会触发更新流程。// 浏览器会自动检查更新// 以下情况会触发检查:// 1. 用户访问页面时// 2. 调用 registration.update()// 3. 页面刷新(在特定条件下)更新生命周期1. 检测更新// 手动触发更新检查navigator.serviceWorker.register('/sw.js').then(registration => { // 检查更新 registration.update(); // 监听更新发现 registration.addEventListener('updatefound', () => { const newWorker = registration.installing; console.log('发现新版本 Service Worker'); });});2. 新版本安装// sw.jsconst 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 会进入等待状态,直到旧版本控制的所有页面关闭。// 主线程监听状态变化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. 激活阶段// sw.jsself.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();});用户提示更新方案一:立即更新(推荐用于开发环境)// 主线程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(); }});方案二:优雅更新(推荐用于生产环境)// 监听 controller 变化navigator.serviceWorker.addEventListener('controllerchange', () => { // Service Worker 已切换,建议用户刷新页面 window.location.reload();});方案三:定期自动更新// 每 60 分钟检查一次更新setInterval(() => { navigator.serviceWorker.ready.then(registration => { registration.update(); });}, 60 * 60 * 1000);更新通知 UI 示例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. 版本控制// 使用版本号管理缓存const CACHE_VERSION = 'v2.1.0';const CACHE_NAME = `app-cache-${CACHE_VERSION}`;2. 增量更新// 只更新变化的资源const urlsToCache = [ '/', '/index.html?v=2', // 添加版本号 '/app.js?v=2.1', '/styles.css?v=2.1'];3. 更新策略选择| 策略 | 适用场景 | 用户体验 ||------|----------|----------|| 立即更新 | 开发环境、紧急修复 | 强制刷新 || 优雅更新 | 生产环境 | 用户可控 || 静默更新 | 非关键更新 | 无感知 |4. 避免更新陷阱// ❌ 错误:缓存 Service Worker 本身// Service Worker 文件不应该被缓存// ✅ 正确:确保 Service Worker 文件不被缓存// 在服务器配置中设置:// Cache-Control: no-cache, no-store, must-revalidate调试技巧// 查看当前 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 Workernavigator.serviceWorker.getRegistration().then(reg => { reg.unregister();});总结自动检测:浏览器自动检测 Service Worker 文件变化安装等待:新版本安装后默认进入等待状态激活接管:旧页面关闭后新版本激活缓存清理:activate 阶段清理旧版本缓存用户提示:提供友好的更新提示机制
阅读 0·3月7日 12:06

Service Worker 的生命周期包含哪些阶段?

Service Worker 生命周期详解Service Worker 的生命周期是理解其工作原理的核心,包含以下关键阶段:1. 注册阶段(Registration)// 在主线程中注册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 事件适合进行静态资源的预缓存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 开始控制页面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 替换最佳实践版本管理:使用版本号管理缓存(如 'v1', 'v2')优雅升级:通过页面刷新触发新 Service Worker 激活缓存清理:在 activate 阶段清理过期缓存错误处理:每个阶段都要添加适当的错误处理
阅读 0·3月7日 12:05

Service Worker 如何实现离线访问功能?

Service Worker 离线访问实现详解离线访问是 Service Worker 最核心的功能之一,让 Web 应用在无网络环境下仍能正常工作。核心原理Service Worker 作为网络代理,拦截所有 HTTP 请求,根据缓存策略决定从缓存返回还是请求网络。实现步骤1. 注册 Service Worker// main.jsif ('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. 预缓存核心资源// sw.jsconst 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. 拦截请求并返回缓存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. 清理旧缓存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();});离线页面设计<!-- 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. 网络优先 + 缓存回退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. 缓存优先 + 后台更新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; }) );});检测网络状态// 主线程中检测window.addEventListener('online', () => { console.log('网络已连接');});window.addEventListener('offline', () => { console.log('网络已断开');});// 检查当前状态if (navigator.onLine) { console.log('在线');} else { console.log('离线');}最佳实践核心资源优先:确保 HTML、CSS、JS 等核心资源被缓存优雅降级:网络失败时提供友好的离线页面缓存更新:定期更新缓存,避免用户长期看到旧版本缓存清理:及时清理过期缓存,避免存储溢出测试验证:使用 Chrome DevTools 的 Network 面板模拟离线环境
阅读 0·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. 网络面板调试// 查看 Service Worker 拦截的请求// DevTools → Network → 查看 Size 列// (from ServiceWorker) 表示从缓存返回// (from disk cache) 表示从磁盘缓存返回// (from memory cache) 表示从内存缓存返回3. Console 面板// Service Worker 中的 console.log 会显示在 DevTools Console// 注意:需要勾选 "Preserve log" 以保留刷新前的日志// 查看当前 Service Worker 状态navigator.serviceWorker.ready.then(registration => { console.log('Service Worker 状态:', registration);});常用调试技巧1. 强制更新 Service Worker// 方法1:手动更新navigator.serviceWorker.getRegistration().then(reg => { reg.update();});// 方法2:硬刷新(Ctrl+Shift+R 或 Cmd+Shift+R)// 会绕过 Service Worker,安装新版本// 方法3:DevTools Application 面板点击 Update2. 查看缓存内容// 查看所有缓存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. 清除缓存// 清除所有缓存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. 模拟离线环境// 方法1:DevTools Network 面板// 选择 "Offline" 或设置自定义网络条件// 方法2:代码中检测window.addEventListener('offline', () => { console.log('进入离线模式');});// 方法3:手动触发离线状态// navigator.connection 可以查看网络状态console.log('网络状态:', navigator.connection);5. 调试 Fetch 事件// 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,可以启用详细日志:// 启用 Workbox 调试workbox.setConfig({ debug: true});// 查看 Workbox 日志workbox.core.setLogLevel(workbox.core.LOG_LEVELS.debug);3. 断点调试// 在 Service Worker 代码中设置 debuggerself.addEventListener('fetch', event => { debugger; // DevTools 会在这里暂停 event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) );});4. 使用 console.table 查看缓存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 未注册// 检查浏览器支持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 问题// 检查是否 HTTPS(localhost 除外)if (location.protocol !== 'https:' && location.hostname !== 'localhost') { console.error('Service Worker 需要 HTTPS 环境');}3. 缓存未更新// 确保每次更新 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. 跨域资源缓存问题// 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 测试 |调试最佳实践使用 Chrome DevTools:充分利用 Application 面板添加详细日志:开发和生产环境使用不同日志级别版本控制:每次更新更改缓存名称测试离线:定期测试离线功能监控错误:使用错误追踪服务监控 Service Worker 错误
阅读 0·3月7日 12:05

Service Worker 如何实现跨域资源的缓存?

Service Worker 跨域资源缓存详解跨域资源缓存是 Service Worker 中的常见问题,由于浏览器的同源策略(Same-Origin Policy),跨域请求的缓存需要特殊处理。跨域请求的基本概念什么是跨域请求// 同源请求(Same-Origin)// 当前页面: https://example.com/page.htmlfetch('/api/data'); // ✅ 同源fetch('https://example.com/api'); // ✅ 同源// 跨域请求(Cross-Origin)fetch('https://api.other.com/data'); // ❌ 跨域(不同域名)fetch('https://cdn.example.com/file'); // ❌ 跨域(不同子域)CORS 和 Opaque Response// 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 模式(推荐)// 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 响应// 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 代理// 如果第三方 API 不支持 CORS,可以搭建代理服务器// sw.jsself.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:预缓存跨域资源// sw.jsconst 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// sw.jsconst 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 资源// sw.jsconst 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:处理跨域图片// sw.jsconst 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 响应的存储成本// 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. 缓存清理策略// 定期清理跨域缓存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. 错误处理// 跨域请求的错误处理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 }); }}最佳实践优先使用 CORS:如果服务器支持 CORS,优先使用 cors 模式限制 Opaque 响应数量:Opaque 响应占用更多存储空间合理设置缓存策略:跨域资源变化频繁时需要设置过期时间提供降级方案:跨域资源加载失败时提供替代内容监控存储使用:定期检查缓存大小,避免超出配额
阅读 0·3月7日 12:05

Service Worker 中的 Cache Storage API 如何使用?

Cache Storage API 是 Service Worker 的核心 API,用于管理请求/响应对的缓存,实现离线访问和性能优化。基本概念Cache Storage API 提供了类似数据库的接口来存储和检索网络请求和响应:// Cache Storage 是 caches 全局对象console.log(caches); // CacheStorage 对象// 主要方法caches.open(cacheName) // 打开/创建缓存caches.match(request) // 跨缓存匹配请求caches.keys() // 获取所有缓存名称caches.delete(cacheName) // 删除指定缓存核心方法详解1. 打开和创建缓存// 打开缓存(如果不存在则创建)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. 添加资源到缓存// 方法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. 从缓存中检索// 方法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. 删除缓存中的资源// 删除特定请求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. 查看缓存内容// 列出所有缓存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:预缓存静态资源// sw.jsconst 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:动态缓存运行时资源// sw.jsconst 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 响应// sw.jsconst 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:缓存清理策略// sw.jsconst 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 只能使用一次// ❌ 错误: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. 存储限制// 检查存储配额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. 缓存键匹配规则// URL 必须完全匹配(包括查询参数)await cache.match('/api/data'); // ✅ 匹配 /api/dataawait 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 头});最佳实践版本控制:使用版本号管理缓存,便于更新定期清理:在 activate 事件中清理过期缓存存储限制:注意浏览器的存储配额限制错误处理:添加适当的错误处理和降级方案性能优化:避免缓存过多数据,定期清理
阅读 0·3月6日 22:58

Service Worker 中的 Background Sync 是什么?如何使用?

Service Worker Background Sync 详解Background Sync(后台同步)是 Service Worker 的重要功能,允许在网络恢复后自动执行延迟的任务,特别适用于离线场景下的数据同步。核心概念Background Sync 解决了什么问题:用户在离线状态下提交表单或操作网络恢复后自动同步数据到服务器无需用户手动重试操作使用场景表单提交:离线时保存表单,联网后自动提交消息发送:离线时保存消息,联网后自动发送数据同步:离线操作的数据在联网后同步日志上传:离线日志在网络恢复后批量上传实现步骤1. 注册同步事件// main.jsasync 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 处理同步// 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 辅助函数// 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); });}完整示例:离线表单提交// form-handler.jsclass 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)// 请求定期同步权限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 | ✅ | ❌ | ❌ | ✅ |最佳实践数据持久化:使用 IndexedDB 存储待同步数据错误处理:同步失败时抛出错误以触发重试幂等性设计:确保重复执行不会产生副作用用户反馈:同步完成后通知用户降级方案:为不支持的浏览器提供替代方案// 降级方案示例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('您处于离线状态,请联网后重试'); } }}
阅读 0·3月6日 22:01