接上篇 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>fetchXHR<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 返回缓存/占位图 网络失败返回兜底内容

九、关键限制与注意事项

  1. 没有 DOM / Window 访问:documentwindowlocalStorage 都不可用。需与页面通信用 postMessage,需持久化数据用 IndexedDB(不是 localStorage)。
  2. 必须是 HTTPS(或 localhost):安全上下文要求,因为 SW 能中间人所有请求。
  3. scope 限制:SW 默认只能控制「脚本所在路径及其子路径」。要控制更大范围,需后端响应 Service-Worker-Allowed 头。
  4. 首次注册不拦截当前页请求:新注册的 SW 只在 clients.claim() 后的「新请求」才生效,当前页已发出的请求不受影响。开发期要硬刷新 + 再刷新一次。
  5. 不能直接操作 DOM,但可以 postMessage 通知页面去改。
  6. 事件驱动 + 短生命周期:SW 会被浏览器随时回收以省内存,事件处理完可能很快休眠。不要在 SW 里存运行时状态变量,要持久化用 IndexedDB。
  7. HMR 不重跑入口模块:用 vite/webpack 时,SW 注册代码别放入口模块(热更新不会重新执行入口初始化),放 index.html 内联脚本最稳。
  8. no-cors 请求的头会被剥离:<img> 等 no-cors 请求,SW 拦截后直接转发会被浏览器按 no-cors 规则剥离自定义头,需在 SW 内用 mode: "cors" 重新发起。

十、调试

  • Chrome DevToolsApplication 面板 → Service Workers:查看注册状态、手动 Update/Unregister、勾选 "Update on reload"(开发期强制更新)。
  • ApplicationCache Storage / IndexedDB:查看 SW 缓存和存储的内容。
  • Network 面板:看请求是否被 SW 拦截(Size 列会显示 (ServiceWorker))。
  • ⚠️ 别用 curl 验证:curl 不经过浏览器、不经过 SW。SW 只拦截浏览器页面发起的请求。

附:本仓库实战案例——用 SW 的 fetch 拦截为文件接口注入 Authorization 头,覆盖 <img>/<iframe>/CSS 背景等浏览器原生请求。详见 blog-file-auth-sw.mdfile-auth-sw-migration-guide.md

posted on 2026-08-21 17:00  中文还在写码  阅读(10)  评论(0)    收藏  举报