Service Worker

参考:web.dev Service Worker 生命周期

1. 什么是 Service Worker?

Web Worker 是临时的,每次做的事情的结果不能被持久存下来;下次同样的复杂操作还得重新做。能不能有一个 Worker 一直持久存在,随时接受主线程命令?基于这种需求,推出了最初的 Service Worker:在 Web Worker 的基础上加上持久离线缓存能力。

它本质上也是浏览器缓存资源用的,只不过它不仅仅是 Cache,而是通过 worker 的方式进一步优化。它基于 h5 的 Web Worker,所以不会阻塞当前 JS 线程,核心原理:

  1. 后台线程:独立于当前网页线程。
  2. 网络代理:在网页发起请求时代理,缓存文件。

Service Worker 充当服务器与浏览器之间的中间人角色。如果网站中注册了 Service Worker,它可以拦截当前网站所有的请求并进行判断:

  • 需要向服务器发请求的 → 转给服务器。
  • 可以直接使用缓存的 → 直接返回缓存,不再转给服务器。

Service Worker 必须基于 HTTPS 协议,因为涉及请求拦截,必须用 HTTPS 保障安全。

2. 生命周期与更新

生命周期包括:注册 register(或更新 update)、installing 安装、activated 激活、fetch 拦截请求。

  • event.waitUntil():在 install/activate 阶段延长生命周期,确保异步操作完成再切换状态。
  • clients.claim():使 Service Worker 在激活后能立即控制未受控页面。
  • 第一次不会被 sw 控制,之后进入或刷新页面才会被控制。

触发更新的时机

  • 导航到作用域内的页面。
  • 更新 push 和 sync 等功能事件(除非前 24 小时内已进行更新检查)。
  • 仅在 Service Worker 网址发生变化时调用 .register()(应避免更改 worker 网址)。
  • Service Worker 字节与浏览器已有的不同时。

更新过程(Soft Update)

  • 由浏览器触发,逐字节比对不同时启动更新算法。
  • 更新的 SW 与现有 SW 一起启动,并获取自己的 install 事件。
  • 安装成功后,更新的工作线程 wait,直到现有工作线程控制零个客户端;self.skipWaiting() 可跳过等待立即激活。
  • 不要给 sw 设置不同的名字。

3. 与静态资源的差异

HTML 页面是网站的入口,它对其他资源文件的引用描述了资源间的依赖关系。Service Worker 与静态资源有所不同:Service Worker 不属于页面资源脚本,程序入口的执行点可能不是页面,而是 Service Worker 本身。如果给 Service Worker 增加 MD5,可能导致严重后果:

  • 新的 HTML 未载入(页面中通常包含 register 代码),Service Worker 就无法更新;
  • Service Worker 无法更新就无法载入新的 HTML。

虽然可以通过小心编码避免上述问题(如在 Service Worker 中对 HTML 采用网络优先缓存策略),但让软件能否正常更新依赖于小心编码,总是不如依赖固化的编译部署流程。

4. 注册与作用域

scope 参数可选,指定 Service Worker 控制内容的子目录。'./' 表示根网域下所有内容(默认值)。Service Worker 和网站同源,可为当前网站的所有请求做代理。

// 注册
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function () {
    navigator.serviceWorker.register('./serviceWorker.js', { scope: './' })
      .then(function (registration) {
        console.log('ServiceWorker registration successful with scope: ', registration.scope);
      })
      .catch(function (err) {
        console.log('ServiceWorker registration failed: ', err);
      });
  });
}

5. 离线缓存示例

// 离线缓存:install 阶段
this.addEventListener('install', function (event) {
  event.waitUntil(
    caches.open('v1').then(function (cache) {
      return cache.addAll(['./index.html']);
    })
  );
});

// 拦截请求:fetch 阶段
this.addEventListener('fetch', function (event) {
  event.respondWith(caches.match(event.request));
});

下面是更完整的 install/activate/fetch 示例,包含旧缓存清理:

const expectedCaches = ['static-v2'];

self.addEventListener('install', (event) => {
  console.log('V2 installing…');
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) return caches.delete(key);
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (url.origin === location.origin && url.pathname === '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

fetch 用于拦截代理所有指定的请求,并进行对应操作,是最关键的阶段。

6. 注意事项

  • Service Worker 运行在 Worker 上下文,不能访问 DOM。
  • 设计为完全异步,同步 API(XHR、localStorage)不能在 Service Worker 中使用。
  • 出于安全考量,只能由 HTTPS 承载。
  • Firefox 浏览器的隐私模式下 Service Worker 不可用。
  • 生命周期与页面无关(没有关联页面时也可以启动,关联页面未关闭时也可以退出)。
  • 在 Chrome DevTools 的 Application 面板可查看 Service Worker、Cache Storage;Network 中被缓存的文件 Size 项显示为 from Service Worker。

7. Service Worker API

参考:MDN Service Worker API

8. Workbox 工具

importScripts 是 Service Worker 中加载 JS 的方式。

Workbox 的缓存分为两种:

  • precache:在 installing 阶段读取并缓存资源,适合跨域 CDN 和域内静态资源。
  • runtime cache:在 install 之后的 activated 和 fetch 阶段处理运行时缓存,对不同类型资源有不同策略:
    • Stale While Revalidate:命中缓存直接返回,同时后台更新缓存。
    • Network First:优先尝试网络请求,成功后写入缓存。
    • Cache First:优先读缓存,没有则发网络请求并写入缓存。

Workbox 源码亮点

  1. 通过 Proxy 按需依赖:子模块按需 importScript 到线程中,通过 Proxy 代理全局对象 Workbox。
  2. 通过 freeze 冻结对外暴露 API:内部模块(如 DBWrapper、Cache Wrapper、fetchWrapper、logger 等)通过 Object.freeze 防止外部修改。

来源整理自:我的有道云笔记