接上篇 Service Worker 核心 API 与典型使用场景
Service Worker 核心 API 与典型使用场景
Service Worker(SW)是浏览器在页面之外运行的独立 JavaScript 线程,充当页面与网络之间的「可编程代理」。它不能直接操作 DOM,但能拦截页面发出的所有请求、管理缓存、接收推送、处理后台同步等。
本文按「核心 API」+「典型场景」对照的方式梳理,便于快速建立全景认知。
一、生命周期 API
SW 有独立的生命周期,理解生命周期是理解一切 SW 行为的基础。
installing → installed(waiting) → activating → activated
↓
redundant(废弃)
1. self.addEventListener("install", ...)
SW 脚本首次被浏览器发现并下载后触发。通常用于预缓存关键资源。
// 预缓存 app shell
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open("app-v1").then((cache) =>
cache.addAll(["/", "/index.html", "/styles.css", "/app.js"])
)
);
});
event.waitUntil(promise):让浏览器等待 promise 完成才认为安装成功。如果 promise reject,安装失败,SW 被丢弃。self.skipWaiting():跳过 waiting 阶段,新 SW 立即激活(用于热更新)。
2. self.addEventListener("activate", ...)
新 SW 激活时触发。通常用于清理旧缓存。
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((key) => key !== "app-v2") // 删除旧版本缓存
.map((key) => caches.delete(key))
)
)
);
// 立即接管所有客户端(否则要等所有旧标签页关闭才接管)
self.clients.claim();
});
self.clients.claim():让刚激活的 SW 立即接管所有已打开的页面(默认只接管激活后新打开的页面)。clients.claim()配合skipWaiting()是开发期快速生效的标准组合。
3. 状态查询:self.registration
// 页面侧
const reg = await navigator.serviceWorker.getRegistration();
reg.active; // 当前激活的 SW
reg.waiting; // 等待激活的新 SW
reg.installing; // 正在安装的 SW
reg.update(); // 主动检查更新
reg.unregister(); // 注销 SW(慎用,会清理所有 SW 能力)
二、网络拦截 API(最常用)
self.addEventListener("fetch", ...)
页面发出的每一个请求都会触发(包括 <img>、fetch、XHR、<link>、<iframe> 等)。这是 SW 最强大的能力——可编程的网络层。
self.addEventListener("fetch", (event) => {
const { request } = event;
// event.respondWith(promise<Response>) —— 接管这个请求,用自己的响应替代默认网络请求
event.respondWith(
// 也可以选择不接管:不调用 respondWith,请求走默认网络
caches.match(request).then((cached) => {
return cached || fetch(request);
})
);
});
Request 对象(只读,来自 event.request)
| 属性 | 说明 |
|---|---|
request.url |
请求 URL |
request.method |
"GET" / "POST" 等 |
request.headers |
Headers 对象,可读不可直接改 |
request.mode |
"cors" / "no-cors" / "same-origin" / "navigate" |
request.credentials |
"omit" / "same-origin" / "include" |
request.body |
请求体(POST 数据,流式) |
修改请求头(常见需求)
Request 是只读的,要改头必须构造新的 Request/Headers:
self.addEventListener("fetch", (event) => {
if (event.request.headers.get("Authorization")) return; // 已带头则跳过
event.respondWith(
(async () => {
const headers = new Headers(event.request.headers);
headers.set("Authorization", `Bearer ${await getToken()}`);
// 重新构造请求
return fetch(event.request, {
headers,
mode: "cors", // 原 <img> 是 no-cors,强制 cors 才能带自定义头
});
})()
);
});
Response 对象
respondWith 的 promise 要 resolve 成一个 Response:
// 网络响应
const res = await fetch(request);
// 缓存命中
const cached = await caches.match(request);
// 构造自定义响应
return new Response("hello", {
status: 200,
headers: { "Content-Type": "text/plain" },
});
三、Cache API(缓存存储)
独立于 HTTP 缓存的、由 JS 显式控制的缓存层。可在 SW 和主线程中用。
核心 API
// 打开一个命名缓存
const cache = await caches.open("my-cache-v1");
// 存
await cache.put(request, response); // 存一组 request→response
await cache.addAll(["/a.html", "/b.js"]); // 批量从网络获取并存
// 取
const response = await cache.match(request); // 命中返回 Response,否则 undefined
const keys = await cache.keys(); // 所有缓存的 request 列表
// 删
await cache.delete(request);
await caches.delete("my-cache-v1"); // 删整个命名缓存
典型缓存策略(场景核心)
// 1. 缓存优先,回退网络(适合静态资源)
event.respondWith(
caches.match(request).then((c) => c || fetch(request))
);
// 2. 网络优先,回退缓存(适合频繁更新的数据)
event.respondWith(
fetch(request)
.then((res) => {
const copy = res.clone();
caches.open("data").then((c) => c.put(request, copy));
return res;
})
.catch(() => caches.match(request))
);
// 3. 竞速(stale-while-revalidate):立即返回缓存,同时后台更新
event.respondWith(
caches.open("dynamic").then(async (cache) => {
const cached = await cache.match(request);
const network = fetch(request).then((res) => {
cache.put(request, res.clone());
return res;
});
return cached || network;
})
);
四、Clients API(管理客户端)
SW 可以与它控制的所有页面(tab)通信,甚至知道哪些页面打开着。
// 获取所有被当前 SW 控制的客户端(打开的标签页)
const clients = await self.clients.matchAll({ type: "window" });
// 向所有客户端广播消息
clients.forEach((client) => {
client.postMessage({ type: "UPDATE_AVAILABLE" });
});
// 让某个客户端聚焦/打开新窗口
await self.clients.openWindow("https://example.com");
await client.focus();
页面 ↔ SW 双向通信
// 页面发消息给 SW
navigator.serviceWorker.controller.postMessage({ cmd: "SYNC" });
// SW 接收
self.addEventListener("message", (event) => {
console.log(event.data); // 来自页面
event.source.postMessage("ok"); // 回复
});
// 页面接收 SW 的回复
navigator.serviceWorker.addEventListener("message", (event) => {
console.log(event.data);
});
五、推送通知 API
SW 可以在页面关闭时仍接收服务器推送(需配合 Push 服务),实现类原生的通知能力。
推送
// SW 接收推送
self.addEventListener("push", (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: "/icon.png",
tag: "news", // 相同 tag 会替换旧通知
})
);
});
// 用户点击通知
self.addEventListener("notificationclick", (event) => {
event.notification.close();
event.waitUntil(self.clients.openWindow("/inbox"));
});
通知权限(页面侧申请)
const permission = await Notification.requestPermission();
if (permission === "granted") {
// 订阅 Push 服务
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VAPID_PUBLIC_KEY,
});
// 把 sub 发给后端
}
六、后台同步 API
页面关闭后,延迟任务在条件满足时(如恢复网络)由 SW 执行。适合「发消息时无网络,等有网自动重发」。
// 页面侧注册同步任务
await reg.sync.register("send-outbox");
// SW 执行
self.addEventListener("sync", (event) => {
if (event.tag === "send-outbox") {
event.waitUntil(sendOutboxMessages()); // 失败会自动重试
}
});
七、主线程侧 API(navigator.serviceWorker)
| API | 说明 |
|---|---|
register(scriptURL, { scope }) |
注册 SW,返回 Promise |
getRegistration() / getRegistrations() |
查询已注册的 SW |
controller |
当前控制页面的 SW 引用(无则为 null) |
addEventListener("controllerchange", fn) |
控制器变化(新 SW 接管)时触发 |
addEventListener("message", fn) |
接收 SW 的 postMessage |
startMessages() |
显式开始接收消息(默认页面加载后才开始) |
// 监听新 SW 接管,提示用户刷新
navigator.serviceWorker.addEventListener("controllerchange", () => {
console.log("新版本已就绪,刷新生效");
});
八、典型使用场景对照表
| 场景 | 核心 API | 说明 |
|---|---|---|
| 离线可用 / PWA | install 预缓存 + fetch 缓存优先 |
预缓存 app shell,网络断开仍可访问 |
| 资源缓存加速 | fetch + Cache API |
静态资源走缓存,减少请求、秒开 |
| 请求头注入(鉴权) | fetch 改写 headers |
给 <img> 等原生请求加 Authorization 头 |
| 请求改写 / 重定向 | fetch 构造新 Request |
改 URL、加 query、改 method |
| 离线表单提交 | sync 后台同步 |
无网时暂存,有网自动重发 |
| 服务器推送通知 | push + showNotification |
页面关闭也能收到通知 |
| 大文件/分片处理 | fetch body 流 |
后台下载/转码,不阻塞页面 |
| 版本更新提示 | controllerchange + clients.postMessage |
新版 SW 就绪,提示用户刷新 |
| 拦截分析 / 日志 | fetch 事件统计 |
前端监控请求成功率、耗时 |
| 请求失败兜底 | fetch catch 返回缓存/占位图 |
网络失败返回兜底内容 |
九、关键限制与注意事项
- 没有 DOM / Window 访问:
document、window、localStorage都不可用。需与页面通信用postMessage,需持久化数据用 IndexedDB(不是 localStorage)。 - 必须是 HTTPS(或 localhost):安全上下文要求,因为 SW 能中间人所有请求。
- scope 限制:SW 默认只能控制「脚本所在路径及其子路径」。要控制更大范围,需后端响应
Service-Worker-Allowed头。 - 首次注册不拦截当前页请求:新注册的 SW 只在
clients.claim()后的「新请求」才生效,当前页已发出的请求不受影响。开发期要硬刷新 + 再刷新一次。 - 不能直接操作 DOM,但可以
postMessage通知页面去改。 - 事件驱动 + 短生命周期:SW 会被浏览器随时回收以省内存,事件处理完可能很快休眠。不要在 SW 里存运行时状态变量,要持久化用 IndexedDB。
- HMR 不重跑入口模块:用 vite/webpack 时,SW 注册代码别放入口模块(热更新不会重新执行入口初始化),放
index.html内联脚本最稳。 - no-cors 请求的头会被剥离:
<img>等 no-cors 请求,SW 拦截后直接转发会被浏览器按 no-cors 规则剥离自定义头,需在 SW 内用mode: "cors"重新发起。
十、调试
- Chrome DevTools →
Application面板 →Service Workers:查看注册状态、手动 Update/Unregister、勾选 "Update on reload"(开发期强制更新)。 Application→Cache Storage/IndexedDB:查看 SW 缓存和存储的内容。- Network 面板:看请求是否被 SW 拦截(Size 列会显示
(ServiceWorker))。 - ⚠️ 别用 curl 验证:curl 不经过浏览器、不经过 SW。SW 只拦截浏览器页面发起的请求。
附:本仓库实战案例——用 SW 的 fetch 拦截为文件接口注入 Authorization 头,覆盖 <img>/<iframe>/CSS 背景等浏览器原生请求。详见 blog-file-auth-sw.md 与 file-auth-sw-migration-guide.md。
浙公网安备 33010602011771号