一文了解 Service Worker
本文对原文进行了合并、勘误和现代化整理。示例代码均按现代 Service Worker API 重写,原文中无法运行的复制残留、过时的 Workbox 版本和不准确的生命周期描述已删除或修正。
目录
- 什么是 Service Worker
- 使用前提和作用域
- Service Worker 的能力和限制
- 生命周期
- 注册 Service Worker
- 安装与预缓存
- 激活与清理旧缓存
- 拦截请求与返回缓存
- 更新 Service Worker
- 缓存策略
- 页面与 Service Worker 通信
- 通知和 Web Push
- Background Sync
- Workbox
- 常见问题和注意事项
- 一个可运行的最小示例
- 总结
什么是 Service Worker
Service Worker(服务工作线程,简称 SW)是一种运行在页面 JavaScript 主线程之外的、由浏览器管理的事件驱动脚本。它没有 DOM 访问权限,却可以在满足作用域和安全条件时代理页面的网络请求,并使用 Cache Storage、IndexedDB 等 API 保存数据。
Service Worker 常用于:
- 缓存静态资源和接口响应,实现离线或弱网访问;
- 控制请求的缓存策略,例如缓存优先、网络优先;
- 接收 Push API 推送并展示系统通知;
- 配合 Background Sync,在网络恢复后重试短时、可重试的任务;
- 作为 PWA(Progressive Web App,渐进式 Web 应用)的基础能力之一。
Service Worker 不是一个永久运行的后台进程。它处于空闲状态时,浏览器可以将其终止;下次有事件发生时,浏览器再重新启动它。因此,不能依赖 Service Worker 保存普通的内存状态,也不能把它当作原生 App 的常驻后台服务。
使用前提和作用域
安全上下文
Service Worker 必须运行在安全上下文中:
- 正式环境使用
https; - 本地开发时,
http://localhost和部分本地域名通常被浏览器视为安全来源; - 普通远程
http页面不能注册 Service Worker。
作用域
Service Worker 默认只能控制脚本所在目录及其子目录:
| Service Worker 地址 | 默认可控制范围 |
|---|---|
/sw.js | 整个站点 |
/app/sw.js | /app/ 及其子目录 |
/app/sw.js 注册时指定 / | 通常仍不能越过脚本目录 |
如果需要让位于子目录的脚本控制更上层的路径,服务端需要返回 Service-Worker-Allowed 响应头:
Service-Worker-Allowed: /
即使 Service Worker 已经激活,页面也必须处于它的控制范围内,fetch 事件才会接收到页面请求。首次注册的页面通常需要刷新一次,或者在激活阶段使用 clients.claim() 立即接管未受控页面。
Service Worker 的能力和限制
可以做什么
Service Worker 运行在 ServiceWorkerGlobalScope 中,常用全局对象和 API 包括:
self:Service Worker 的全局对象;caches:Cache Storage 接口;fetch():发起网络请求;clients:查找和控制页面客户端;registration:当前 Service Worker 注册信息;indexedDB:保存结构化数据;push、sync、notificationclick等事件。
不能做什么
Service Worker 没有页面的 DOM 环境,不能直接访问:
window;document;- 页面 DOM;
- 页面中的
parent、localStorage等对象。
它可以访问 navigator 和 location,但 Service Worker 中的 location 是脚本自身的 WorkerLocation,表示 Service Worker 文件的地址,不是某个页面的当前 URL。
如果需要操作页面 DOM,页面和 Service Worker 必须通过 postMessage() 通信,由页面脚本执行 DOM 操作。
生命周期
Service Worker 的生命周期大致如下:
注册
↓
install 安装
↓
waiting 等待(如果旧版本仍控制页面)
↓
activate 激活
↓
控制页面并处理 fetch / message / push 等事件
↓
空闲时可能被终止,需要时再启动

主要状态
- installing:正在安装新版本;
- installed / waiting:安装成功,等待旧版本释放客户端;
- activating:正在激活;
- activated:已激活,可以控制客户端并处理功能事件;
- redundant:安装失败,或被更新版本替代。
重要的生命周期规则
install针对每个 Service Worker 版本执行一次。脚本内容发生变化后,浏览器会创建新版本并重新触发install;- 新版本通常要等旧版本不再控制任何页面后才会激活;
self.skipWaiting()只能跳过waiting阶段,不能跳过安装;clients.claim()只能让激活后的 Service Worker 接管当前作用域内未受控的页面,不负责更新缓存;- Service Worker 可能被浏览器终止,因此不要依赖全局变量保存持久数据。
注册 Service Worker
在页面脚本中注册:
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
})
console.log('Service Worker 注册成功:', registration.scope)
} catch (error) {
console.error('Service Worker 注册失败:', error)
}
})
}
register() 的脚本 URL 和 scope 应使用明确、稳定的路径。不要为了版本更新而把脚本 URL 从 /sw.js 改成 /sw-v2.js,否则旧 Service Worker 可能缓存了仍在使用的 HTML,导致页面始终发现不了新的脚本。
注册成功不代表当前页面已经被控制。可以使用下面的方式等待一个已经激活的 Service Worker:
navigator.serviceWorker.ready.then((registration) => {
console.log('当前页面可以使用 Service Worker:', registration.scope)
})
安装与预缓存
通常在 install 事件中预缓存应用外壳、离线页面和稳定的静态资源。event.waitUntil() 会告诉浏览器安装过程需要等待哪个 Promise 完成。
const STATIC_CACHE = 'static-v1'
const PRECACHE_URLS = [
'/',
'/offline.html',
'/styles.css',
'/app.js'
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
)
})
cache.addAll() 中只要有一个资源下载失败,整个 Promise 就会失败,安装也会失败。因此预缓存列表应该只包含稳定、必需且可访问的资源,不要随意加入可能不存在或需要特殊权限的接口地址。

为什么不能在安装时缓存所有内容
- 大量资源会延长安装时间;
- 任意一个失败请求都可能导致整个安装失败;
- 缓存过多会浪费存储空间;
- 频繁变化的接口数据不适合在安装阶段预缓存;
- 用户可能只访问页面的一小部分,没有必要提前下载整个站点。
更合理的做法是:安装时只缓存应用壳和离线必需资源,其他资源在运行时根据请求逐步缓存。
激活与清理旧缓存
在 activate 中可以删除旧版本缓存,并在需要时让新版本接管未受控页面:
const CURRENT_CACHES = new Set([
'static-v1',
'runtime-v1'
])
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const cacheNames = await caches.keys()
await Promise.all(
cacheNames
.filter((name) => !CURRENT_CACHES.has(name))
.map((name) => caches.delete(name))
)
await self.clients.claim()
})())
})
清理缓存时要注意:Cache Storage 按源(origin)隔离,但同一源下可能有多个应用。不要无条件删除所有缓存,应使用应用唯一的缓存名前缀:
const CACHE_PREFIX = 'my-app-'
const obsoleteCaches = cacheNames.filter(
(name) => name.startsWith(CACHE_PREFIX) && !CURRENT_CACHES.has(name)
)

拦截请求与返回缓存
只有被当前 Service Worker 控制的页面,其符合条件的请求才会触发 fetch 事件。缓存示例应至少过滤非 GET 请求,并明确缓存范围:
self.addEventListener('fetch', (event) => {
const request = event.request
const url = new URL(request.url)
// Cache Storage 通常只用于 GET 请求
if (request.method !== 'GET') {
return
}
// 这里只处理同源请求
if (url.origin !== self.location.origin) {
return
}
event.respondWith((async () => {
const cache = await caches.open('runtime-v1')
const cachedResponse = await cache.match(request)
if (cachedResponse) {
return cachedResponse
}
const networkResponse = await fetch(request)
if (networkResponse.ok) {
event.waitUntil(
cache.put(request, networkResponse.clone())
)
}
return networkResponse
})())
})
这里有几个关键点:
event.respondWith()必须在fetch事件处理过程中调用;- 一个 Request 或 Response 被读取后,通常需要使用
clone()才能再次使用; cache.put()是异步操作,应通过event.waitUntil()延长事件生命周期;- 不要把
POST、PUT等请求直接写入 Cache Storage; - 跨域资源涉及 CORS 和 opaque response,应单独设计,不要简单用
response.status === 200判断所有响应。

缓存新请求时的更完整写法
async function cacheNetworkResponse(request, cacheName) {
const response = await fetch(request)
if (!response.ok) {
return response
}
const cache = await caches.open(cacheName)
await cache.put(request, response.clone())
return response
}
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') {
return
}
const url = new URL(event.request.url)
if (url.origin !== self.location.origin) {
return
}
event.respondWith(
cacheNetworkResponse(event.request, 'runtime-v1')
)
})
实际项目中还应根据请求类型分别处理页面导航、JavaScript、CSS、字体、图片和接口,而不是对所有请求使用同一种策略。
更新 Service Worker
浏览器会在导航、注册和部分功能事件等时机检查 Service Worker 脚本是否更新。浏览器下载脚本后会比较内容;即使 URL 不变,只要脚本内容发生字节变化,也可能安装新版本。
更新流程通常是:
旧版本 active
↓ 发现 sw.js 内容发生变化
新版本 install
↓
新版本 waiting
↓ 旧版本不再控制客户端
新版本 activate
skipWaiting() 的风险
self.addEventListener('install', (event) => {
event.waitUntil(precacheResources())
self.skipWaiting()
})
它可以让新版本尽快激活,但可能导致:
- 页面当前使用的 HTML 属于旧版本;
- 新 Service Worker 处理后续请求;
- 新旧版本资源混合使用;
- 页面正在执行的逻辑与新缓存策略不兼容。
如果新旧版本之间可能不兼容,建议让新版本正常等待,并通过页面提示用户刷新。只有确认兼容时,才使用 skipWaiting() 和 clients.claim()。
监听更新
navigator.serviceWorker.register('/sw.js').then((registration) => {
registration.addEventListener('updatefound', () => {
const worker = registration.installing
if (!worker) {
return
}
worker.addEventListener('statechange', () => {
console.log('Service Worker 状态:', worker.state)
if (worker.state === 'installed' && navigator.serviceWorker.controller) {
console.log('发现新版本,可以提示用户刷新')
}
})
})
})
如果需要主动检查,可以调用:
const registration = await navigator.serviceWorker.ready
await registration.update()
不要通过不断更换 Service Worker 文件名来更新版本。应保持注册地址稳定,通过脚本内容变化和缓存版本号完成更新。

开发调试
Chrome DevTools 的 Application 面板中可以:
- 查看 Service Worker 状态;
- 勾选 Update on reload,让开发时每次加载都检查更新;
- 使用 Skip waiting 激活等待中的版本;
- 使用 Unregister 注销 Service Worker;
- 使用 Clear storage 清除 Cache Storage、IndexedDB 等数据。
这些选项主要用于开发调试,不应当被误认为正式环境的更新策略。强制刷新绕过 Service Worker 的行为也可能因浏览器而异,不能当作用户更新方案。
缓存策略
不同资源应使用不同策略。下面的策略名称是通用概念,Workbox 也提供了对应实现。
| 策略 | 流程 | 适合场景 |
|---|---|---|
| Cache Only | 只从缓存读取 | 已经预缓存且必须离线可用的资源 |
| Network Only | 只从网络读取 | 强实时接口、登录请求 |
| Cache First | 先缓存,未命中再网络 | 带版本指纹的静态资源、图片、字体 |
| Network First | 先网络,失败后缓存 | HTML、文章、需要较新数据的接口 |
| Stale While Revalidate | 立即返回缓存,同时请求网络更新 | 图片、头像、非关键内容 |
| Network Race | 缓存与网络竞争 | 特定高性能场景,需谨慎设计 |
“渐进式缓存”更像一种运行时缓存方式,不是浏览器内置的独立策略;“Network Race”也不是 Service Worker 的内置策略,通常需要自行实现。
Cache First
适合带内容哈希的 CSS、JavaScript、图片和字体。资源更新时应通过文件名变化或缓存版本变化避免长期使用旧文件。
async function cacheFirst(request) {
const cache = await caches.open('assets-v1')
const cached = await cache.match(request)
if (cached) {
return cached
}
const response = await fetch(request)
if (response.ok) {
await cache.put(request, response.clone())
}
return response
}
Network First
适合 HTML 和实时性较高的内容。fetch() 只有在网络层失败时才会 reject;收到 404 或 500 时仍可能返回一个已完成的 Response。如果希望 HTTP 错误也回退缓存,需要主动检查 response.ok。
async function networkFirst(request) {
const cache = await caches.open('pages-v1')
try {
const response = await fetch(request)
if (response.ok) {
await cache.put(request, response.clone())
return response
}
throw new Error(`HTTP ${response.status}`)
} catch {
const cached = await cache.match(request)
if (cached) {
return cached
}
return caches.match('/offline.html')
}
}
Stale While Revalidate
它会尽快返回缓存,同时请求网络并更新缓存。缓存命中时,用户可以快速看到旧内容;下一次请求通常能得到新内容。
async function staleWhileRevalidate(event) {
const request = event.request
const cache = await caches.open('runtime-v1')
const cached = await cache.match(request)
const network = fetch(request).then(async (response) => {
if (response.ok) {
await cache.put(request, response.clone())
}
return response
})
event.waitUntil(network.catch(() => undefined))
return cached || network
}
如果缓存和网络都失败,上面的函数仍可能抛出异常。生产代码应根据资源类型返回离线占位响应或明确的错误页面。
页面与 Service Worker 通信
Service Worker 不能直接操作页面 DOM。页面可以通过 postMessage() 发送命令,Service Worker 也可以向页面返回结果。
页面发送消息
navigator.serviceWorker.controller?.postMessage({
type: 'CLEAR_RUNTIME_CACHE'
})
navigator.serviceWorker.addEventListener('message', (event) => {
console.log('收到 Service Worker 消息:', event.data)
})
Service Worker 接收消息
self.addEventListener('message', async (event) => {
if (event.data?.type === 'CLEAR_RUNTIME_CACHE') {
await caches.delete('runtime-v1')
event.source?.postMessage({
type: 'CACHE_CLEARED'
})
}
})
对于需要在页面尚未被控制时通信的场景,可以使用 navigator.serviceWorker.ready,或在更复杂的多客户端场景中使用 clients.matchAll() 获取页面客户端。
通知和 Web Push
本地通知
Notification API 可以创建通知,Service Worker 可以使用 showNotification() 显示通知。请求权限应尽量放在用户主动点击按钮等用户手势中:
async function requestNotification() {
if (!('Notification' in window)) {
return
}
const permission = await Notification.requestPermission()
if (permission !== 'granted') {
return
}
const registration = await navigator.serviceWorker.ready
await registration.showNotification('消息提醒', {
body: '这是一条来自网页的通知',
icon: '/icons/icon-192.png'
})
}
Service Worker 中可以处理通知点击和关闭:
self.addEventListener('notificationclick', (event) => {
event.notification.close()
event.waitUntil((async () => {
const windowClients = await self.clients.matchAll({
type: 'window',
includeUncontrolled: true
})
const target = windowClients.find(
(client) => client.url === self.registration.scope
)
if (target) {
await target.focus()
} else {
await self.clients.openWindow(self.registration.scope)
}
})())
})
Web Push 的完整链路
真正的 Web Push 不只是调用 showNotification(),还需要后端参与:
- 页面请求通知权限;
- 页面通过
PushManager.subscribe()创建订阅; - 页面把订阅信息发送给后端;
- 后端使用 VAPID 等机制向 Push 服务发送消息;
- 浏览器唤醒 Service Worker 并触发
push事件; - Service Worker 解析消息并展示通知。
Service Worker 中的基本处理方式:
self.addEventListener('push', (event) => {
const data = event.data?.json() || {
title: '新消息',
body: '你收到了一条新消息'
}
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png'
})
)
})

原图中的 rawgit.com 已经停止服务,图片只保留作为流程参考,不能再依赖其中的 URL。
Background Sync
Background Sync 允许页面把短时、可重试的网络任务延迟到网络恢复后执行,例如离线提交评论、发送消息队列等。
它不是所有浏览器都支持,使用前应进行特性检测:
async function registerSync() {
const registration = await navigator.serviceWorker.ready
if (!('sync' in registration)) {
// 降级:保存到本地,并在下次打开页面时主动重试
return false
}
await registration.sync.register('send-outbox')
return true
}
Service Worker 中监听 sync 事件:
self.addEventListener('sync', (event) => {
if (event.tag === 'send-outbox') {
event.waitUntil(flushOutbox())
}
})
async function flushOutbox() {
// 从 IndexedDB 读取待发送数据
// 发送成功后删除队列中的数据
// 失败时抛出异常,让浏览器有机会再次尝试
}
需要注意:
- 同一个 tag 的任务可能被合并,不要把每一次注册都当成一次独立执行;
- 浏览器决定具体执行时机,不保证网络恢复后立即运行;
- Service Worker 仍可能被终止,异步工作必须传给
event.waitUntil(); - Background Sync 适合短任务,大文件下载应考虑 Background Fetch 或普通下载方案;
- 必须设计降级逻辑,不能把它作为唯一的数据提交途径。
Workbox
Workbox 是 Google 维护的 Service Worker 工具库,封装了预缓存、运行时缓存、路由匹配、缓存过期和后台同步等常见能力,可以减少手写生命周期和缓存策略的代码。
原文使用的 Workbox 3.2.0 已经过时,cdn.rawgit.com 也已经停止服务。现代项目应使用当前版本的 Workbox,并通过 npm、构建工具或官方文档提供的方式集成,而不是复制旧版 CDN 示例。
使用模块化 Workbox
下面是现代 Workbox 模块化写法的示例,实际项目中需要通过构建工具打包:
import { registerRoute } from 'workbox-routing'
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'
// 图片:缓存优先,并限制数量和缓存时间
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images-v1',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 7 * 24 * 60 * 60
})
]
})
)
// 页面导航:网络优先,网络失败时回退缓存
registerRoute(
({ request }) => request.mode === 'navigate',
new NetworkFirst({
cacheName: 'pages-v1',
networkTimeoutSeconds: 3
})
)
// CSS / JS:缓存优先或 stale-while-revalidate,取决于发布策略
registerRoute(
({ request }) =>
request.destination === 'style' || request.destination === 'script',
new StaleWhileRevalidate({
cacheName: 'assets-v1'
})
)
networkTimeoutSeconds 只适用于支持网络超时配置的策略,例如 NetworkFirst,原文中的 networkTimetoutSeconds 是拼写错误。
Workbox 常用策略
| Workbox 策略 | 说明 |
|---|---|
CacheFirst | 先缓存,未命中再请求网络 |
NetworkFirst | 先请求网络,失败时回退缓存 |
StaleWhileRevalidate | 先返回缓存,同时后台更新 |
NetworkOnly | 始终使用网络 |
CacheOnly | 始终使用缓存 |
Workbox 的路由和策略仍然需要结合资源类型设计。不能把所有接口、HTML 和静态资源都使用同一种策略,也不能把 Workbox 策略当作浏览器本身的内置 API。



常见问题和注意事项
1. 不要缓存敏感数据
Cache Storage 和 IndexedDB 都属于客户端存储。不要把访问令牌、密码、银行卡信息等敏感数据直接写入缓存。即使是缓存的 HTML,也要考虑同设备其他用户、注销后的数据清理和 XSS 风险。
2. 不要缓存所有请求
建议按请求类型划分:
- 页面导航:Network First,准备离线页面;
- 带版本指纹的静态资源:Cache First;
- 图片和头像:Cache First 或 Stale While Revalidate;
- 实时接口:Network Only 或 Network First;
- 登录、提交、支付等非 GET 请求:通常不要使用 Cache Storage。
3. HTML 不要轻易使用永久 Cache First
如果把 index.html 永久缓存并使用 Cache First,用户可能长期拿到旧 HTML,从而发现不了新版本 Service Worker。HTML 通常应使用 Network First、短期缓存或配合明确的版本更新机制。
4. 缓存空间不是无限的
浏览器可能因为配额、用户清理或存储压力删除缓存。应用不能把 Cache Storage 当作永不丢失的数据库,需要对缓存未命中和缓存被清除的情况做好处理。
5. 缓存 API 不是 HTTP 缓存的完全替代品
Service Worker 的 Cache Storage 与浏览器 HTTP 缓存是两套机制。请求可能先受 HTTP 缓存影响,Cache Storage 也不会自动遵循所有 HTTP 缓存头。实际项目应同时考虑响应头、Service Worker 策略和缓存版本。
6. HTTPS 和作用域问题
注册失败时优先检查:
- 当前页面是否是 HTTPS 或 localhost;
- Service Worker 脚本是否返回 JavaScript,而不是 HTML 错误页;
- 脚本路径是否正确;
- 作用域是否覆盖当前页面;
- 是否有旧版本 Service Worker 正在控制页面;
- 浏览器 DevTools 是否启用了 Disable cache、Update on reload 或 Bypass for network。
7. Service Worker 不能控制首次请求
页面第一次访问时,Service Worker 通常还没有安装、激活和控制页面,因此首次请求可能直接走网络。离线能力一般从安装完成、页面刷新并进入控制状态后开始生效。
一个可运行的最小示例
下面给出一个不依赖 Workbox 的最小示例。假设目录结构如下:
/
├── index.html
├── offline.html
├── app.js
└── sw.js
index.html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Service Worker Demo</title>
</head>
<body>
<h1>Service Worker Demo</h1>
<p id="status">正在注册 Service Worker...</p>
<script src="/app.js"></script>
</body>
</html>
app.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js')
document.querySelector('#status').textContent =
`已注册:${registration.scope}`
} catch (error) {
document.querySelector('#status').textContent = '注册失败'
console.error(error)
}
})
}
sw.js
const STATIC_CACHE = 'static-v1'
const RUNTIME_CACHE = 'runtime-v1'
const PRECACHE_URLS = [
'/',
'/offline.html',
'/app.js'
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
)
})
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const cacheNames = await caches.keys()
await Promise.all(
cacheNames
.filter((name) =>
name.startsWith('static-') && name !== STATIC_CACHE
)
.map((name) => caches.delete(name))
)
await self.clients.claim()
})())
})
self.addEventListener('fetch', (event) => {
const request = event.request
const url = new URL(request.url)
if (request.method !== 'GET' || url.origin !== self.location.origin) {
return
}
if (request.mode === 'navigate') {
event.respondWith((async () => {
try {
const response = await fetch(request)
if (response.ok) {
const cache = await caches.open(RUNTIME_CACHE)
await cache.put(request, response.clone())
}
return response
} catch {
const cache = await caches.open(STATIC_CACHE)
return (await cache.match('/offline.html')) || Response.error()
}
})())
return
}
event.respondWith((async () => {
const cache = await caches.open(RUNTIME_CACHE)
const cached = await cache.match(request)
if (cached) {
return cached
}
const response = await fetch(request)
if (response.ok) {
event.waitUntil(cache.put(request, response.clone()))
}
return response
})())
})
运行时必须通过 HTTPS 或 localhost 启动服务器,不能直接双击 HTML 文件使用 file:// 打开。
总结
- Service Worker 是浏览器管理的事件驱动工作线程,不是永久运行的后台进程;
- 它不能访问 DOM、
window和document,页面通信应使用postMessage(); - 必须使用 HTTPS 或 localhost,并且只有作用域内、已受控页面的请求才会触发
fetch; install适合预缓存,activate适合清理旧缓存和接管客户端;skipWaiting()只跳过等待,clients.claim()只负责接管未受控客户端,两者都不会自动更新缓存内容;- Cache Storage 通常只缓存 GET 请求,缓存写入应正确使用
clone()和event.waitUntil(); - HTML、静态资源、图片和接口应根据实时性选择不同缓存策略;
- Background Sync 和 Push 都需要特性检测、权限处理和降级方案;
- Workbox 可以减少模板代码,但应使用现代版本和模块化集成方式,不要直接复制旧版 CDN 示例;
- Service Worker 上线前必须测试首次访问、刷新、更新、离线、缓存清理、注销和多标签页场景。