Service Worker原理与应用
graph TD A[Web应用] -->|请求| B{Service Worker} B -->|拦截请求| C[缓存策略] B -->|无匹配缓存| D[网络请求] C -->|返回缓存| A D -->|返回资源并缓存| C E[浏览器] -->|注册| B B -->|控制| A style B fill:#f9d77e,stroke:#333,stroke-width:2px style C fill:#a8e6cf,stroke:#333,stroke-width:1px style D fill:#ffd3b6,stroke:#333,stroke-width:1px
1. Service Worker概述
Service Worker是一种运行在浏览器背景中的脚本,它独立于网页,为现代Web应用提供了丰富的离线体验、定期的后台同步以及推送通知等功能。作为PWA(Progressive Web App,渐进式Web应用)的核心技术,Service Worker充当了Web应用程序、浏览器与网络之间的代理服务器,拦截和修改网络请求,精细控制缓存资源。
1.1 主要特性
- 运行在独立线程:Service Worker在工作线程(Worker)环境中运行,不会阻塞主线程
- 完全异步:不支持同步XHR和localStorage等同步API操作
- 不能直接访问DOM:无法直接操作页面DOM,需通过postMessage与页面通信
- 安全限制:仅在HTTPS环境下工作(开发环境下localhost例外)
- 生命周期独立于页面:即使用户关闭了网站,Service Worker仍可在后台运行
- 支持离线运行:通过缓存机制,可以在离线环境下提供Web应用服务
2. Service Worker的生命周期
Service Worker拥有完全独立于Web页面的生命周期,理解这一生命周期对于有效开发和调试Service Worker至关重要。
stateDiagram-v2 [*] --> 下载 下载 --> 安装: install事件 安装 --> 等待: 安装成功 安装 --> 废弃: 安装失败 等待 --> 激活: activate事件 激活 --> 空闲: 激活成功 空闲 --> 终止: 一段时间无操作 终止 --> 空闲: 有新请求 空闲 --> 处理请求: fetch事件 处理请求 --> 空闲: 请求完成 激活 --> 更新: 检测到新版本 更新 --> 安装: 安装新版本 废弃 --> [*]
2.1 注册阶段
Service Worker的使用始于注册过程,通常在应用的主JavaScript文件中进行:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker注册成功,作用域为:', registration.scope);
})
.catch(error => {
console.error('Service Worker注册失败:', error);
});
}
注册代码会告诉浏览器Service Worker脚本的位置。
2.2 下载与解析
浏览器会下载、解析并执行Service Worker文件。如果这个过程中出现任何错误(如语法错误),注册将会失败,Service Worker将不会被安装。
2.3 安装阶段
下载并执行成功后,Service Worker会触发install
事件。这是Service Worker生命周期中仅发生一次的事件,通常用于缓存应用所需的静态资源:
self.addEventListener('install', event => {
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js',
'/images/logo.png'
]);
})
);
});
event.waitUntil()
方法接受一个Promise,它会延长事件的生命周期直到Promise被解决。这确保Service Worker不会在安装完成前就被终止。
2.4 激活阶段
安装成功后,Service Worker进入激活阶段,触发activate
事件。这个阶段通常用于清理旧版本的缓存:
self.addEventListener('activate', event => {
const cacheWhitelist = ['v1'];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
新安装的Service Worker可能会延迟激活,直到使用旧版本的页面都已关闭。这确保了只有一个版本的Service Worker在运行。
2.5 控制页面
激活后,Service Worker将能够控制在其作用域内的页面,监听fetch
事件并拦截网络请求:
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
// 如果在缓存中找到了匹配的请求,返回它
// caches.match()方法在Cache对象中查找与请求匹配的缓存响应
// 它会检查URL和请求方法等是否匹配缓存中的条目
if (response) {
return response;
}
// 否则发起网络请求
return fetch(event.request);
})
);
});
2.6 更新机制
当用户再次访问网站时,浏览器会检查服务器上的Service Worker文件是否有更新。如有更新(字节不同),会安装新版本,但不会立即激活
sequenceDiagram participant 浏览器 participant 旧SW as 旧Service Worker participant 新SW as 新Service Worker participant 网络 浏览器->>网络: 检查SW文件更新 网络-->>浏览器: 返回新版本SW 浏览器->>新SW: 安装(install事件) 新SW-->>浏览器: 安装完成 Note over 浏览器: 此时旧SW仍在控制页面 浏览器->>旧SW: 继续处理请求 Note over 浏览器,新SW: 等待所有使用旧SW的页面关闭 浏览器->>新SW: 激活(activate事件) 新SW-->>浏览器: 激活完成 浏览器->>新SW: 后续请求由新SW处理
3. Service Worker的通信机制
Service Worker虽然不能直接操作DOM,但可以通过以下方式与Web页面通信:
3.1 使用postMessage通信
Service Worker与页面之间可以通过postMessage
API进行双向通信:
从页面向Service Worker发送消息:
navigator.serviceWorker.controller.postMessage({
type: 'COMMAND',
payload: 'Hello from page!'
});
从Service Worker接收消息:
self.addEventListener('message', event => {
console.log('收到来自页面的消息:', event.data);
});
从Service Worker向页面发送消息:
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage({
type: 'UPDATE',
payload: 'Hello from Service Worker!'
});
});
});
在页面中接收消息:
navigator.serviceWorker.addEventListener('message', event => {
console.log('收到来自Service Worker的消息:', event.data);
});
4. 缓存策略
缓存是Service Worker实现离线功能的关键。根据应用需求的不同,可以采用不同的缓存策略:
graph TD subgraph CacheOnly["缓存优先 (Cache Only)"] A1[请求] --> B1[缓存] B1 --> C1[返回] end subgraph NetworkOnly["网络优先 (Network Only)"] A2[请求] --> B2[网络] B2 --> C2[返回] end style CacheOnly fill:#e6f7ff,stroke:#1890ff,stroke-width:2px style NetworkOnly fill:#fff7e6,stroke:#fa8c16,stroke-width:2px
graph TD subgraph CacheFirst["缓存优先,网络备用 (CacheFirst)"] A3[请求] --> B3{缓存?} B3 -->|有| C3[返回缓存] B3 -->|无| D3[网络请求] D3 --> E3[返回并缓存] end subgraph NetworkFirst["网络优先,缓存备用 (NetworkFirst)"] A4[请求] --> B4[网络请求] B4 -->|成功| C4[返回并缓存] B4 -->|失败| D4[查找缓存] D4 --> E4[返回缓存] end subgraph StaleWhileRevalidate["使用旧缓存并更新(Stale While Revalidate)"] A5[请求] --> B5{缓存?} B5 -->|有| C5[返回缓存] B5 -->|无| D5[网络请求] C5 --> E5[同时更新缓存] D5 --> E5 E5 --> F5[更新完成] end style CacheFirst fill:#f6ffed,stroke:#52c41a,stroke-width:2px style NetworkFirst fill:#fff2e8,stroke:#fa541c,stroke-width:2px style StaleWhileRevalidate fill:#f9f0ff,stroke:#722ed1,stroke-width:2px
4.1 Cache Only (仅缓存)
所有请求直接从缓存中获取,完全不使用网络。适用于不会改变的静态资源。
self.addEventListener('fetch', event => {
event.respondWith(caches.match(event.request));
});
4.2 Network Only (仅网络)
所有请求必须从网络获取,完全不使用缓存。适用于需要最新数据的API请求。
self.addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});
4.3 Cache First (缓存优先,网络备用)
先尝试从缓存中获取资源,如果没有找到再从网络获取。适用于性能优先且资源很少更新的场景。
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(response => {
// 将从网络获取的资源存入缓存
return caches.open('v1').then(cache => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
4.4 Network First (网络优先,缓存备用)
先尝试从网络获取最新资源,如果网络不可用则从缓存获取。适用于需要最新数据但又要提供离线体验的场景。
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
});
4.5 Stale While Revalidate (使用旧缓存同时更新)
先返回缓存的资源(无论是否过期),同时在后台发起网络请求获取最新资源并更新缓存。适用于需要快速响应且内容可以稍微滞后的场景。
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('v1').then(cache => {
return cache.match(event.request).then(response => {
const fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
})
);
});
5. 使用Workbox简化开发
Workbox是Google提供的一套库,用于简化Service Worker的开发过程。它提供了一系列工具,可以轻松实现各种缓存策略和Service Worker功能。
5.1 Workbox概述
Workbox是一个由多个库组成的集合,每个库负责Service Worker的不同方面:
- workbox-routing:处理路由匹配
- workbox-strategies:实现缓存策略
- workbox-precaching:预缓存资源
- workbox-expiration:管理缓存过期
- workbox-background-sync:支持后台同步
- workbox-window:简化Service Worker注册和更新
graph TD A[Workbox核心] --> B[workbox-routing] A --> C[workbox-strategies] A --> D[workbox-precaching] A --> E[workbox-expiration] A --> F[workbox-background-sync] A --> G[workbox-window] B --> H[路由匹配] C --> I[缓存策略实现] D --> J[资源预缓存] E --> K[缓存过期管理] F --> L[后台同步] G --> M[Service Worker注册和更新] style A fill:#f9d77e,stroke:#333,stroke-width:2px style B,C,D,E,F,G fill:#a8e6cf,stroke:#333,stroke-width:1px
5.2 使用Workbox实现缓存策略
使用Workbox可以简化缓存策略的实现:
// 导入Workbox
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');
// 预缓存重要资源
workbox.precaching.precacheAndRoute([
{ url: '/', revision: '1' },
{ url: '/index.html', revision: '1' },
{ url: '/styles.css', revision: '1' },
{ url: '/app.js', revision: '1' }
]);
// 为图片应用Cache First策略
workbox.routing.registerRoute(
({ request }) => request.destination === 'image',
new workbox.strategies.CacheFirst({
cacheName: 'images',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30天
}),
],
})
);
// 为API请求应用Network First策略
workbox.routing.registerRoute(
new RegExp('/api/'),
new workbox.strategies.NetworkFirst({
cacheName: 'api-responses',
networkTimeoutSeconds: 3,
})
);
// 为静态资源应用Stale While Revalidate策略
workbox.routing.registerRoute(
({ request }) => request.destination === 'script' ||
request.destination === 'style',
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'static-resources',
})
);
6. Service Worker高级功能
除了缓存和离线功能外,Service Worker还支持许多高级功能:
6.1 后台同步 (Background Sync)
允许在用户重新联网时执行操作,例如发送之前离线时未能发送的表单数据:
self.addEventListener('sync', event => {
if (event.tag === 'outbox') {
event.waitUntil(sendOutboxMessages());
}
});
sequenceDiagram participant 用户 participant 页面 participant SW as Service Worker participant 服务器 用户->>页面: 提交表单 页面->>SW: 注册sync任务(outbox) Note over 用户,服务器: 用户离线 页面->>SW: 存储表单数据 Note over 用户,服务器: 用户重新联网 SW->>SW: 触发sync事件 SW->>服务器: 发送存储的表单数据 服务器-->>SW: 响应成功 SW->>页面: 通知提交成功
6.2 推送通知 (Push Notifications)
允许Web应用即使在未打开的情况下也能接收推送消息:
self.addEventListener('push', event => {
const notificationData = event.data.json();
self.registration.showNotification(notificationData.title, {
body: notificationData.body,
icon: notificationData.icon
});
});
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(
clients.openWindow('https://example.com')
);
});
sequenceDiagram participant 推送服务 participant SW as Service Worker participant 浏览器 participant 用户 推送服务->>SW: 发送推送消息 SW->>SW: 触发push事件 SW->>浏览器: 显示通知 浏览器->>用户: 展示通知给用户 用户->>浏览器: 点击通知 浏览器->>SW: 触发notificationclick事件 SW->>浏览器: 打开相关页面
6.3 导航预加载 (Navigation Preload)
加速导航请求,在Service Worker启动的同时并行地发起网络请求:
self.addEventListener('activate', event => {
event.waitUntil(
self.registration.navigationPreload.enable()
);
});
self.addEventListener('fetch', event => {
event.respondWith(async function() {
// 尝试获取预加载的响应
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
// 如果没有预加载响应,回退到正常的网络请求
return fetch(event.request);
}());
});
sequenceDiagram participant 用户 participant 浏览器 participant SW as Service Worker participant 网络 用户->>浏览器: 点击链接或输入URL 浏览器->>SW: 启动Service Worker 浏览器->>网络: 同时发起预加载请求 par 并行处理 SW->>SW: 启动并运行 and 网络-->>浏览器: 返回预加载响应 end 浏览器->>SW: 触发fetch事件(带有预加载响应) SW->>浏览器: 使用预加载响应
7. 实战应用案例
7.1 实现离线功能的PWA
下面是一个实现了完整离线功能的PWA应用的Service Worker示例:
const CACHE_NAME = 'my-app-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/app.js',
'/styles.css',
'/images/logo.png',
'/images/offline.png',
'/offline.html'
];
// 安装阶段:预缓存静态资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting()) // 强制新安装的Service Worker激活
);
});
// 激活阶段:清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
}).then(() => self.clients.claim()) // 控制未受控制的客户端
);
});
// 拦截请求
self.addEventListener('fetch', event => {
// 处理导航请求
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.catch(() => caches.match('/offline.html'))
);
return;
}
// 对API请求使用"网络优先,缓存备用"策略
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then(response => {
// 将有效响应存入缓存
if (response.ok) {
const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseClone);
});
}
return response;
})
.catch(() => caches.match(event.request))
);
return;
}
// 对其他请求使用"缓存优先,网络备用"策略
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request)
.then(response => {
// 将有效响应存入缓存
if (response.ok && response.type === 'basic') {
const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseClone);
});
}
return response;
})
.catch(error => {
// 如果请求的是图片,返回备用离线图片
if (event.request.destination === 'image') {
return caches.match('/images/offline.png');
}
throw error;
});
})
);
});
// 处理后台同步
self.addEventListener('sync', event => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
// 处理推送通知
self.addEventListener('push', event => {
const data = event.data.json();
self.registration.showNotification(data.title, {
body: data.body,
icon: '/images/notification-icon.png',
badge: '/images/notification-badge.png',
data: data.data
});
});
// 处理通知点击
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.notification.data && event.notification.data.url) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
8. Service Worker调试技巧
8.1 使用Chrome DevTools
Chrome浏览器提供了强大的Service Worker调试工具:
- 打开DevTools (F12)
- 切换到Application标签
- 在左侧面板找到Service Workers
graph LR A[Chrome DevTools] --> B[Application标签] B --> C[Service Workers面板] C --> D[状态查看] C --> E[强制更新] C --> F[模拟离线] C --> G[Update on reload] C --> H[错误日志] C --> I[卸载] style C fill:#f9d77e,stroke:#333,stroke-width:2px style D,E,F,G,H,I fill:#a8e6cf,stroke:#333,stroke-width:1px
这里可以进行以下操作:
- 查看Service Worker的状态
- 强制更新Service Worker
- 模拟离线状态
- 切换"Update on reload"选项(每次刷新页面时强制更新Service Worker)
- 查看Service Worker的错误日志
- 卸载Service Worker
8.2 常见调试问题及解决方案
Service Worker无法注册:
- 确保使用HTTPS或localhost
- 检查Service Worker文件路径是否正确
- 确保Service Worker作用域适当
缓存不生效:
- 检查缓存名称是否一致
- 确认fetch事件处理器正确拦截请求
- 验证路径匹配规则
Service Worker不更新:
- 确保新Service Worker文件内容有变化
- 检查更新流程中可能阻塞的waitUntil调用
- 可以在DevTools中尝试"Update on reload"选项
无法清除旧缓存:
- 确保在activate事件中正确处理清理逻辑
- 检查缓存键名匹配
9. 最佳实践与性能优化
9.1 Service Worker最佳实践
不要在Service Worker中缓存过大的资源:每个浏览器对缓存大小有限制
避免在install事件中缓存大量资源:可能导致安装失败
实现适当的更新策略:使用版本控制和合理的缓存过期策略
处理跨域请求:注意CORS和不透明响应(opaque responses)的处理
提供回退机制:当网络和缓存都失败时,提供适当的回退页面
注意作用域:Service Worker只能控制在其作用域下的页面
9.2 性能优化
使用导航预加载:减少Service Worker启动延迟对导航请求的影响
选择合适的缓存策略:不同类型的资源应使用不同的缓存策略
缓存管理:定期清理过期缓存以释放存储空间
最小化Service Worker文件大小:减少解析和执行时间
避免不必要的网络请求:合理使用缓存可以减少网络负载
10. 未来发展
Service Worker技术仍在不断发展,未来可能会有更多功能:
mindmap root((Service Worker未来)) 后台处理 更复杂的任务执行 更长的运行时间 Web API整合 Web Share Web Payments Web Bluetooth 缓存控制 更精细的策略 更智能的存储管理 跨浏览器支持 统一实现标准 更一致的行为 原生平台整合 更接近原生体验 更深度的系统集成
更强大的后台处理能力:执行更复杂的后台任务
与其他Web API的深度整合:如Web Share、Web Payments等API
更精细的缓存控制:更灵活的缓存策略和存储管理
更好的跨浏览器支持:各浏览器实现更加统一
与原生平台的更深度整合:提供更接近原生体验的功能
参考资料
- MDN Web Docs - Service Worker API
- Google Developers - Service Workers: an Introduction
- web.dev - Service workers and the Cache Storage API
- Workbox 官方文档
- Jake Archibald's Offline Cookbook
Q&A
基础概念类问题
什么是Service Worker?它与Web Worker有什么区别?
- Service Worker是一种特殊的Web Worker,作为网页与网络之间的代理服务器,能够拦截、修改网络请求并缓存资源
- 区别:
- 功能范围:Service Worker可以拦截网络请求、持久存在、有生命周期事件,而普通Web Worker只是执行JavaScript计算任务
- 持久性:Service Worker在页面关闭后仍可存在,而Web Worker随页面关闭而终止
- 上下文环境:Service Worker运行在自己的全局上下文中,与页面JavaScript环境完全分离
- 通信方式:Service Worker使用postMessage与页面通信,还可以通过Fetch API拦截请求
- 注册方式:Service Worker需要通过特定的注册流程,而Web Worker通过构造函数创建
为什么Service Worker只能在HTTPS环境下运行?
- 由于Service Worker可以拦截和修改网络请求,在HTTP环境下容易被中间人攻击利用,攻击者可能注入恶意代码
- 安全性考虑:
- 防止恶意代码篡改网络通信和用户数据
- 保护用户隐私,避免敏感信息被窃取
- 确保Service Worker代码的完整性和可信度
- 符合现代Web安全最佳实践
- 例外情况:localhost(本地开发环境)可以不使用HTTPS,便于开发调试
Service Worker的生命周期有哪些阶段?每个阶段的作用是什么?
- 注册(Registration):应用告知浏览器Service Worker的位置
- 下载(Download):浏览器下载Service Worker脚本文件
- 安装(Install):
- 触发install事件
- 适合预缓存静态资源(HTML、CSS、JS、图片等应用外壳资源)
- 通过event.waitUntil()确保安装完成
- 等待(Waiting):
- 确保同一时间只有一个版本在运行,避免版本冲突
- 等待旧版本Service Worker控制的页面全部关闭
- 可以通过skipWaiting()跳过等待阶段
- 激活(Activate):
- 触发activate事件
- 适合清理旧缓存和旧版本资源
- 可以通过clients.claim()立即控制未被控制的客户端
- 运行/空闲(Running/Idle):处理fetch、push等事件
- 终止(Terminated):浏览器可能在空闲时终止Service Worker以节省资源
- 更新(Update):当检测到新版本时重新开始生命周期
Service Worker作用域(scope)是什么?如何配置?
- 作用域决定了Service Worker可以控制的页面范围,定义了其"管辖权限"
- 默认为Service Worker文件所在的路径及其子路径
- 配置方法:
- 通过register方法的第二个参数配置:
navigator.serviceWorker.register('/sw.js', {scope: '/app/'})
- 作用域必须是Service Worker所在路径的子路径或相同路径
- 要控制更高级别的路径,需要设置Service-Worker-Allowed HTTP头
- 通过register方法的第二个参数配置:
- 实际应用:
- 可以为不同部分的应用使用不同的Service Worker
- 例如:主应用使用一个Service Worker,而管理后台使用另一个
缓存策略类问题
请解释Service Worker的5种缓存策略及其适用场景
Cache Only(仅缓存):
- 实现:直接从缓存返回资源,不发起网络请求
- 适用场景:静态资源,如应用外壳(App Shell)、字体、图标等永远不变或很少变化的资源
- 优势:响应速度最快,无网络依赖
- 风险:如果资源未预缓存,将无法获取
Network Only(仅网络):
- 实现:始终从网络获取资源,完全不使用缓存
- 适用场景:需要实时数据的API请求、支付交易、用户特定内容等
- 优势:始终获取最新数据
- 风险:离线时完全无法工作
Cache First(缓存优先,网络备用):
- 实现:先检查缓存,有则使用;无则从网络获取并缓存
- 适用场景:不常变化的资源,如CSS、JavaScript库、字体、图标等
- 优势:大幅提升性能,减少网络请求
- 风险:更新频率低,用户可能看到过时内容
Network First(网络优先,缓存备用):
- 实现:先尝试从网络获取,失败则从缓存获取
- 适用场景:频繁更新但又要提供离线体验的内容,如新闻文章、博客、产品信息
- 优势:在线时获取最新内容,离线时仍可访问
- 风险:网络较慢时用户体验不佳
Stale While Revalidate(使用旧缓存同时更新):
- 实现:立即返回缓存内容(无论是否过期),同时在后台更新缓存
- 适用场景:既需要快速响应又需要最新内容的场景,如社交媒体feed、评论系统
- 优势:结合了速度和新鲜度,用户体验最佳
- 风险:用户可能先看到旧内容,后台更新失败时不会通知用户
如何处理Service Worker中的缓存过期问题?
使用缓存版本号进行标记:
- 为每个缓存创建唯一的版本标识符(如'cache-v1'、'cache-v2')
- 当需要更新缓存策略时,增加版本号
- 在activate事件中清理旧版本缓存
在activate事件中清理旧版本缓存:
javascriptself.addEventListener('activate', event => { const currentCaches = ['cache-v2']; // 当前使用的缓存版本 event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (!currentCaches.includes(cacheName)) { return caches.delete(cacheName); // 删除旧版本缓存 } }) ); }) ); });
使用Workbox的ExpirationPlugin:
- 设置缓存时间限制:
maxAgeSeconds
(资源最大存活时间) - 设置缓存数量限制:
maxEntries
(缓存项目数量上限) - 自动清理过期资源:
javascriptworkbox.routing.registerRoute( /\.(?:png|jpg|jpeg|svg|gif)$/, new workbox.strategies.CacheFirst({ cacheName: 'images', plugins: [ new workbox.expiration.ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60, // 30天 }), ], }) );
- 设置缓存时间限制:
基于HTTP缓存头的策略:
- 尊重资源的Cache-Control和Expires头
- 实现条件请求(使用ETag或Last-Modified)
如何处理大型文件(如视频)的缓存策略?
避免直接缓存大文件:
- Cache API有存储限制(通常为浏览器总存储空间的一定比例)
- 大文件缓存可能导致其他重要资源被挤出缓存
使用range requests处理分段请求:
- 实现对视频等大文件的部分请求处理
- 只缓存用户实际观看的视频片段
javascriptself.addEventListener('fetch', event => { const url = new URL(event.request.url); if (url.pathname.endsWith('.mp4') && event.request.headers.has('range')) { // 处理范围请求逻辑 } });
考虑使用IndexedDB代替Cache API存储大文件:
- IndexedDB通常有更大的存储限制
- 可以实现更精细的存储管理
- 支持结构化存储和查询
javascript// 存储视频到IndexedDB function storeVideo(videoId, videoBlob) { const dbPromise = indexedDB.open('video-store', 1); // 实现存储逻辑 }
实现智能缓存策略:
- 只缓存视频的元数据和初始片段
- 基于用户行为预测和缓存(如常看的视频)
- 实现缓存优先级(重要视频保留更长时间)
- 监控设备存储状态,在存储紧张时清理低优先级缓存
实现与调试类问题
Service Worker如何实现离线访问功能?请详述完整流程
注册Service Worker:
javascriptif ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('SW注册成功:', registration.scope); }) .catch(error => { console.log('SW注册失败:', error); }); }); }
install事件中缓存核心资源:
javascriptconst CACHE_NAME = 'offline-v1'; const OFFLINE_URLS = [ '/', '/index.html', '/styles/main.css', '/scripts/main.js', '/images/logo.png', '/offline.html' // 离线回退页面 ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('缓存核心资源'); return cache.addAll(OFFLINE_URLS); }) .then(() => self.skipWaiting()) // 立即激活 ); });
fetch事件中拦截请求,返回缓存内容:
javascriptself.addEventListener('fetch', event => { // 网络优先策略 event.respondWith( fetch(event.request) .catch(() => { // 网络请求失败,尝试从缓存获取 return caches.match(event.request) .then(response => { if (response) { return response; // 返回缓存的资源 } // 如果是HTML请求,返回离线页面 if (event.request.mode === 'navigate') { return caches.match('/offline.html'); } // 其他资源无法提供 return new Response('网络不可用', { status: 503, statusText: 'Service Unavailable' }); }); }) ); });
提供离线回退页面:
- 创建专门的offline.html页面,告知用户当前处于离线状态
- 确保该页面所需的所有资源(CSS、图片等)都已缓存
- 在页面中提供有用信息和可能的离线功能
激活并更新缓存:
javascriptself.addEventListener('activate', event => { // 清理旧缓存 event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== CACHE_NAME) { return caches.delete(cacheName); } }) ); }).then(() => self.clients.claim()) // 接管现有客户端 ); });
Service Worker的更新机制是什么?如何强制更新Service Worker?
更新触发机制:
- 浏览器检测到Service Worker文件字节差异时触发更新
- 页面导航时会检查Service Worker更新(至少24小时检查一次)
- 通过registration.update()手动触发更新检查
更新流程:
- 浏览器下载新版本Service Worker
- 新版本进入安装阶段(触发install事件)
- 安装成功后进入等待状态(waiting)
- 当所有使用旧版本的页面关闭后,新版本激活
使用
skipWaiting()
跳过等待状态:javascriptself.addEventListener('install', event => { event.waitUntil( // 缓存资源后立即跳过等待 caches.open('v2').then(cache => { return cache.addAll(resources); }).then(() => { return self.skipWaiting(); // 强制激活 }) ); });
使用
clients.claim()
接管现有客户端:javascriptself.addEventListener('activate', event => { event.waitUntil( // 清理旧缓存 caches.keys().then(/* 清理逻辑 */) .then(() => { return self.clients.claim(); // 接管所有客户端 }) ); });
通过版本号变更或添加查询参数触发更新检测:
javascript// 添加时间戳或版本号强制更新 navigator.serviceWorker.register('/sw.js?v=' + Date.now())
实现更新通知和刷新机制:
javascript// 在页面中检测Service Worker更新 navigator.serviceWorker.addEventListener('controllerchange', () => { // Service Worker已更新并接管页面 if (refreshing) return; refreshing = true; window.location.reload(); // 刷新页面应用更新 });
如何调试Service Worker?有哪些常用工具和方法?
Chrome DevTools的Application面板:
- 查看Service Worker状态和生命周期
- 启用"Update on reload"自动更新
- 模拟离线状态测试离线功能
- 查看缓存存储内容
- 查看Service Worker日志和错误
Firefox的调试工具:
- 在"调试"面板中的"Service Workers"选项
- 查看注册的Service Worker和状态
- 检查缓存存储
"Update on reload"选项:
- 在Chrome DevTools中启用此选项
- 每次刷新页面时强制更新Service Worker
- 加速开发和测试周期
清除Service Worker的方法:
javascript// 编程方式注销Service Worker navigator.serviceWorker.getRegistrations().then(registrations => { for(let registration of registrations) { registration.unregister(); } });
使用Lighthouse审计:
- 评估PWA功能和性能
- 检查Service Worker实现是否符合最佳实践
Service Worker调试技巧:
- 使用self.skipWaiting()和clients.claim()加速更新
- 添加详细日志便于调试:javascript
console.log('[ServiceWorker] 安装中...');
- 使用Chrome DevTools的"Preserve log"选项保留日志
- 使用workbox-cli等工具生成Service Worker
如何处理Service Worker中的跨域请求?
理解不透明响应(opaque responses):
- 当请求跨域资源且没有CORS头时返回
- 状态码为0,无法读取内容
- 缓存大小可能比实际资源大
CORS策略配置:
- 在服务器端添加适当的CORS头:
Access-Control-Allow-Origin: *
- 或针对特定域名:
Access-Control-Allow-Origin: https://your-app.com
- 在服务器端添加适当的CORS头:
使用
mode: 'cors'
配置fetch请求:javascriptfetch('https://api.example.com/data', { mode: 'cors', // 明确请求CORS响应 credentials: 'same-origin' // 控制是否发送cookies })
处理opaque responses的缓存限制:
- 避免缓存不透明响应,或谨慎处理
- 使用no-cors模式获取跨域资源:javascript
fetch('https://third-party.com/image.jpg', { mode: 'no-cors' // 允许请求,但返回不透明响应 })
- 考虑使用代理服务器中转请求,避免跨域问题
预检请求(Preflight)处理:
- 复杂请求会触发OPTIONS预检请求
- 确保服务器正确响应OPTIONS请求:
Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: Content-Type
高级应用类问题
描述如何使用Service Worker实现推送通知功能
获取用户授权:
javascript// 请求通知权限 function requestNotificationPermission() { return Notification.requestPermission().then(permission => { if (permission !== 'granted') { throw new Error('通知权限被拒绝'); } return permission; }); }
订阅推送服务:
javascript// 获取推送订阅 function subscribeToPushNotifications(serviceWorkerReg) { const applicationServerKey = urlBase64ToUint8Array( 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U' ); return serviceWorkerReg.pushManager.subscribe({ userVisibleOnly: true, // 承诺通知对用户可见 applicationServerKey: applicationServerKey }) .then(subscription => { // 将订阅信息发送到服务器 return fetch('/api/save-subscription', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(subscription) }); }); }
监听push事件:
javascript// 在Service Worker中处理推送消息 self.addEventListener('push', event => { let notificationData = {}; try { notificationData = event.data.json(); } catch (e) { notificationData = { title: '新消息', body: event.data ? event.data.text() : '无内容' }; } // 确保Service Worker保持活动状态直到通知显示完成 event.waitUntil( self.registration.showNotification(notificationData.title, { body: notificationData.body, icon: '/images/notification-icon.png', badge: '/images/badge-icon.png', data: notificationData.data || {}, actions: notificationData.actions || [] }) ); });
显示通知并处理点击事件:
javascript// 处理通知点击 self.addEventListener('notificationclick', event => { event.notification.close(); // 提取通知数据 const notificationData = event.notification.data; let urlToOpen = new URL('/', self.location.origin).href; if (notificationData && notificationData.url) { urlToOpen = notificationData.url; } // 处理特定操作按钮点击 if (event.action === 'view-details') { urlToOpen = notificationData.detailsUrl || urlToOpen; } // 打开或聚焦窗口 event.waitUntil( self.clients.matchAll({ type: 'window', includeUncontrolled: true }) .then(clientList => { // 检查是否已有打开的窗口 for (let client of clientList) { if (client.url === urlToOpen && 'focus' in client) { return client.focus(); } } // 如果没有打开的窗口,则打开新窗口 if (self.clients.openWindow) { return self.clients.openWindow(urlToOpen); } }) ); });
Service Worker与PWA的关系是什么?PWA的核心特性有哪些?
Service Worker是PWA的核心技术之一:
- 提供PWA的离线功能和网络弹性
- 实现后台同步和推送通知
- 控制缓存策略,提升性能
- 作为PWA与网络之间的代理
PWA核心特性:
可安装(Installable):
- 通过Web App Manifest实现
- 添加到主屏幕,类似原生应用
- 全屏或沉浸式体验
- 自定义启动画面和图标
离线工作(Offline Capability):
- 通过Service Worker缓存关键资源
- 提供离线页面和功能
- 实现网络弹性,在弱网络环境下仍可使用
推送通知(Push Notifications):
- 通过Push API和Notification API实现
- 即使用户未打开应用也能接收通知
- 提高用户参与度和留存率
后台同步(Background Sync):
- 在网络恢复时自动同步数据
- 确保用户操作最终完成
- 提升弱网络环境下的用户体验
渐进增强(Progressive Enhancement):
- 在支持PWA功能的浏览器中提供增强体验
- 在不支持的浏览器中仍能基本工作
- 随着用户浏览器升级,体验自动提升
与Web App Manifest的配合使用:
json{ "name": "我的PWA应用", "short_name": "PWA应用", "start_url": "/index.html", "display": "standalone", "background_color": "#ffffff", "theme_color": "#4285f4", "icons": [ { "src": "/images/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/images/icon-512.png", "sizes": "512x512", "type": "image/png" } ] }
PWA的技术组成:
- Service Worker:离线功能、缓存、推送通知
- Web App Manifest:安装体验、外观配置
- HTTPS:安全通信
- 响应式设计:适应不同设备
- App Shell架构:快速加载核心UI
Workbox库的作用是什么?它如何简化Service Worker开发?
Workbox是Google开发的Service Worker库:
- 一套用于构建PWA的JavaScript库
- 简化Service Worker开发的复杂性
- 遵循性能最佳实践
- 模块化设计,可按需引入功能
提供预定义的缓存策略:
javascript// 使用Workbox的缓存优先策略 workbox.routing.registerRoute( /\.(?:png|jpg|jpeg|svg|gif)$/, new workbox.strategies.CacheFirst({ cacheName: 'images', plugins: [ new workbox.expiration.ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60, // 30天 }), ], }) ); // 使用网络优先策略 workbox.routing.registerRoute( /\/api\/.*$/, new workbox.strategies.NetworkFirst({ cacheName: 'api-responses', networkTimeoutSeconds: 3, }) ); // 使用Stale While Revalidate策略 workbox.routing.registerRoute( /\.(?:js|css)$/, new workbox.strategies.StaleWhileRevalidate({ cacheName: 'static-resources', }) );
简化路由匹配和请求处理:
- 支持正则表达式、字符串、自定义函数匹配
- 提供直观的API处理不同类型的请求
javascript// 精确URL匹配 workbox.routing.registerRoute( '/index.html', new workbox.strategies.NetworkFirst() ); // 自定义匹配函数 workbox.routing.registerRoute( ({url, request, event}) => { return url.pathname.startsWith('/special/'); }, new workbox.strategies.NetworkFirst() );
提供缓存过期、后台同步等模块:
- workbox-expiration:管理缓存大小和过期时间
- workbox-background-sync:实现离线请求的后台同步
- workbox-broadcast-update:通知页面缓存更新
- workbox-cacheable-response:基于状态码或头信息决定是否缓存
- workbox-range-requests:支持视频等资源的范围请求
构建工具集成:
- 与webpack、Rollup等构建工具集成
- 自动生成Service Worker文件
- 预缓存清单管理
javascript// webpack配置 const { GenerateSW } = require('workbox-webpack-plugin'); module.exports = { // 其他webpack配置... plugins: [ new GenerateSW({ clientsClaim: true, skipWaiting: true, // 其他Workbox配置 }) ] };
调试和测试支持:
- 开发模式下详细日志
- 与Chrome DevTools集成
- 提供测试辅助工具
如何优化Service Worker的性能?
减小Service Worker文件大小:
- 移除不必要的代码和依赖
- 使用代码分割和懒加载
- 压缩和最小化Service Worker文件
- 使用Workbox的模块化引入而非完整库
javascript// 按需导入Workbox模块 importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js'); workbox.loadModule('workbox-strategies'); workbox.loadModule('workbox-routing'); // 仅加载需要的模块
使用导航预加载(Navigation Preload):
减小Service Worker文件大小
使用导航预加载(Navigation Preload)
避免过度缓存
合理选择缓存策略
优化激活时间
11. Service Worker常用API
Service Worker提供了丰富的API,使开发者能够实现各种离线功能、缓存管理和后台处理能力。以下是Service Worker中最常用的API及其用法:
11.1 注册与生命周期API
ServiceWorkerContainer
navigator.serviceWorker
对象提供了注册和管理Service Worker的方法:
// 注册Service Worker
navigator.serviceWorker.register('/sw.js', {scope: '/'})
.then(registration => {
console.log('注册成功,作用域为:', registration.scope);
});
// 获取当前控制页面的Service Worker
const swController = navigator.serviceWorker.controller;
// 检查Service Worker更新
navigator.serviceWorker.ready.then(registration => {
registration.update();
});
// 注销Service Worker
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
// 监听控制权变化事件
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('页面控制权已转移到新的Service Worker');
});
生命周期方法
Service Worker内部可以使用以下方法控制其生命周期:
// 在安装阶段跳过等待,直接激活
self.skipWaiting();
// 在激活阶段接管未被控制的客户端页面
self.clients.claim();
11.2 缓存API (Cache API)
Cache API是Service Worker实现离线功能的核心,提供了缓存请求和响应的能力:
// 打开缓存
caches.open('cache-v1')
.then(cache => {
// 添加资源到缓存
cache.add('/index.html');
// 批量添加资源
cache.addAll([
'/styles.css',
'/script.js',
'/images/logo.png'
]);
// 手动添加请求-响应对
cache.put(
new Request('/api/data'),
new Response('{"data": "cached"}', {
headers: {'Content-Type': 'application/json'}
})
);
});
// 查找缓存中的响应
caches.match('/index.html')
.then(response => {
if (response) {
return response;
}
return fetch('/index.html');
});
// 删除缓存
caches.delete('old-cache');
// 获取所有缓存名称
caches.keys()
.then(cacheNames => {
console.log('当前缓存:', cacheNames);
});
11.3 Fetch API
Service Worker可以拦截并处理网络请求:
// 拦截fetch请求
self.addEventListener('fetch', event => {
// 使用自定义响应
event.respondWith(
caches.match(event.request)
.then(response => {
return response || fetch(event.request);
})
);
// 修改请求
event.respondWith(
fetch(new Request(event.request.url, {
headers: new Headers({
'X-Custom-Header': 'value'
})
}))
);
// 创建自定义响应
event.respondWith(
new Response('<h1>离线页面</h1>', {
headers: {'Content-Type': 'text/html'}
})
);
});
// 发起网络请求
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({key: 'value'})
})
.then(response => response.json())
.then(data => console.log(data));
11.4 消息通信API
Service Worker与页面之间可以通过消息机制进行通信:
// 在Service Worker中接收消息
self.addEventListener('message', event => {
console.log('收到消息:', event.data);
// 回复消息
event.source.postMessage({
reply: '收到你的消息',
originalMessage: event.data
});
});
// 在Service Worker中向所有客户端发送消息
self.clients.matchAll()
.then(clients => {
clients.forEach(client => {
client.postMessage({
type: 'UPDATE',
payload: '资源已更新'
});
});
});
// 在页面中向Service Worker发送消息
navigator.serviceWorker.controller.postMessage({
type: 'COMMAND',
action: 'clearCache'
});
// 在页面中接收Service Worker消息
navigator.serviceWorker.addEventListener('message', event => {
console.log('收到Service Worker消息:', event.data);
});
11.5 后台同步API
允许在用户重新联网时执行操作:
// 在页面中注册同步任务
navigator.serviceWorker.ready.then(registration => {
registration.sync.register('sync-task')
.then(() => {
console.log('后台同步任务已注册');
})
.catch(err => {
console.error('后台同步注册失败:', err);
});
});
// 在Service Worker中处理同步事件
self.addEventListener('sync', event => {
if (event.tag === 'sync-task') {
event.waitUntil(
// 执行同步操作,如发送存储的表单数据
sendCachedData()
);
}
});
11.6 推送通知API
实现服务器推送和通知功能:
// 订阅推送服务
navigator.serviceWorker.ready.then(registration => {
return registration.pushManager.subscribe({
userVisibleOnly: true, // 推送必须对用户可见
applicationServerKey: urlBase64ToUint8Array('BEl62iUY...') // VAPID公钥
});
})
.then(subscription => {
// 将订阅信息发送到服务器
return fetch('/api/save-subscription', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(subscription)
});
});
// 在Service Worker中处理推送事件
self.addEventListener('push', event => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/images/icon.png',
badge: '/images/badge.png',
data: {url: data.url},
actions: [
{action: 'view', title: '查看详情'},
{action: 'close', title: '关闭'}
]
})
);
});
// 处理通知点击事件
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'view') {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});
11.7 导航预加载API
加速导航请求,在Service Worker启动的同时并行发起网络请求:
// 在激活事件中启用导航预加载
self.addEventListener('activate', event => {
event.waitUntil(
self.registration.navigationPreload.enable()
);
});
// 在fetch事件中使用预加载响应
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(async function() {
try {
// 尝试使用预加载的响应
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
// 如果没有预加载响应,回退到正常的网络请求
return await fetch(event.request);
} catch (error) {
// 网络错误,使用缓存
return caches.match('/offline.html');
}
}());
}
});
11.8 Clients API
管理Service Worker控制的页面:
// 获取所有受控制的窗口客户端
self.clients.matchAll({type: 'window'})
.then(clients => {
clients.forEach(client => {
console.log('客户端URL:', client.url);
});
});
// 聚焦现有窗口或打开新窗口
self.clients.matchAll({type: 'window'})
.then(clients => {
for (let client of clients) {
if (client.url === '/index.html' && 'focus' in client) {
return client.focus();
}
}
// 如果没有找到匹配的窗口,打开新窗口
return self.clients.openWindow('/index.html');
});
// 声明接管所有客户端
self.clients.claim();
11.9 IndexedDB API
Service Worker可以使用IndexedDB进行数据存储:
// 打开数据库
const dbPromise = indexedDB.open('my-db', 1);
dbPromise.onupgradeneeded = event => {
const db = event.target.result;
// 创建对象存储
const store = db.createObjectStore('offline-data', {keyPath: 'id'});
store.createIndex('timestamp', 'timestamp');
};
// 存储数据
function saveData(data) {
return dbPromise.then(db => {
const tx = db.transaction('offline-data', 'readwrite');
const store = tx.objectStore('offline-data');
store.put(data);
return tx.complete;
});
}
// 读取数据
function getData(id) {
return dbPromise.then(db => {
const tx = db.transaction('offline-data', 'readonly');
const store = tx.objectStore('offline-data');
return store.get(id);
});
}
11.10 工具函数
Service Worker开发中常用的辅助函数:
// 将Base64编码的VAPID公钥转换为Uint8Array
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;
}
// 生成唯一ID
function generateUniqueId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
}
// 格式化网络错误信息
function formatNetworkError(error) {
return {
type: 'NetworkError',
message: error.message,
timestamp: new Date().toISOString()
};
}