Service Worker 的生活

如果不了解 Service Worker 的生命周期,则很难知道其在做什么。 它们的内部运作方式似乎不透明,甚至不拘一格。 请记住,与任何其他浏览器 API 一样,Service Worker 的行为也明确定义, 并使离线应用成为可能 同时在不影响用户体验的情况下促进更新。

在深入了解 Workbox 之前, 请务必了解 Service Worker 的生命周期,这样 Workbox 才有意义。

定义术语

在进入 Service Worker 生命周期之前, 因此有必要围绕生命周期的运作方式定义一些术语。

控制力和范围

“控制”的概念对于理解 Service Worker 的运行方式至关重要。 被描述为受 Service Worker 控制的页面是指允许 Service Worker 代表其拦截网络请求的页面。 Service Worker 存在,并且能够在给定范围内为页面执行相关工作。

范围

Service Worker 的范围由其在网络服务器上的位置决定。 如果 Service Worker 在位于 /subdir/index.html 且位于 /subdir/sw.js 的页面上运行, Service Worker 的作用域为 /subdir/。 如需了解范围的概念,请查看以下示例:

  1. 导航到 https://service-worker-scope-viewer.glitch.me/subdir/index.html. 系统会显示一条消息,指出没有 Service Worker 正在控制页面。 但是,该页面从 https://service-worker-scope-viewer.glitch.me/subdir/sw.js 注册一个 Service Worker。
  2. 重新加载页面。 由于 Service Worker 已注册且现在处于活跃状态, 它正在控制该网页 包含 Service Worker 范围的表单, 当前状态,并且其网址将会显示。 注意:必须重新加载页面与作用域无关。 而是 Service Worker 生命周期,稍后我们将对此进行介绍。
  3. 现在,转到 https://service-worker-scope-viewer.glitch.me/index.html。 尽管在这个源上注册了 Service Worker, 仍然有一条消息显示当前没有 Service Worker。 这是因为此页面不在已注册的 Service Worker 的作用域内。

范围会限制 Service Worker 控制哪些页面。 在此示例中,这意味着从 /subdir/sw.js 加载的 Service Worker 只能控制位于 /subdir/ 或其子树中的页面。

以上是限定范围的默认工作原理。 但可以通过设置 Service-Worker-Allowed 响应标头, 以及传递 scope 选项添加到 register 方法。

除非有充分的理由将 Service Worker 的作用域限定为源的某个子集, 从 Web 服务器的根目录加载 Service Worker,以便尽可能扩大其作用域, 而无需担心 Service-Worker-Allowed 标头。这样对每个人来说,它都简单得多。

客户

正如我们所说的 Service Worker 在控制页面,实际上它在控制客户端。 客户端是网址在该 Service Worker 范围内的任何打开页面。 具体来说,这些是 WindowClient 的实例。

新 Service Worker 的生命周期

为了让 Service Worker 控制页面, 也就是必须先实现它 我们先来了解一下为没有活跃 Service Worker 的网站部署全新的 Service Worker 时会发生什么情况。

注册

注册是 Service Worker 生命周期的初始步骤:

<!-- In index.html, for example: -->
<script>
  // Don't register the service worker
  // until the page has fully loaded
  window.addEventListener('load', () => {
    // Is service worker available?
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service worker registered!');
      }).catch((error) => {
        console.warn('Error registering service worker:');
        console.warn(error);
      });
    }
  });
</script>

此代码在主线程上运行,并执行以下操作:

  1. 由于用户首次访问网站发生在没有注册的 Service Worker 的情况下, 等到页面完全加载后再注册一个。 这样可以在 Service Worker 预缓存任何内容时避免带宽争用。
  2. 尽管 Service Worker 得到很好的支持, 进行快速检查有助于避免在不支持此功能的浏览器出现错误。
  3. 当页面完全加载且支持 Service Worker 时,注册 /sw.js

您需要了解以下重要事项:

  • Service Worker 仅通过 HTTPS 或 localhost 提供
  • 如果 Service Worker 的内容包含语法错误, Service Worker 就会被舍弃。
  • 提醒:Service Worker 在一个作用域内运行。 此处,作用域是从根目录加载的整个源。
  • 注册开始时,Service Worker 状态设置为 'installing'

注册完成后,系统就会开始安装。

安装

Service Worker install 事件。 每个 Service Worker 仅调用一次 install,并且在更新之前不会再次触发。 您可以使用 addEventListener 在 worker 的作用域中注册 install 事件的回调:

// /sw.js
self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v1';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v1'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.bc7b80b7.css',
      '/css/home.fe5d0b23.css',
      '/js/home.d3cc4ba4.js',
      '/js/jquery.43ca4933.js'
    ]);
  }));
});

这会创建一个新的 Cache 实例并预缓存资产。 我们稍后会有很多机会来介绍预缓存 因此,我们重点了解一下 event.waitUntilevent.waitUntil 接受 promise; 并等待该 promise 得到解决。 在此示例中,该 promise 执行两项异步操作:

  1. 创建名为 'MyFancyCache_v1' 的新 Cache 实例。
  2. 创建缓存后 资源网址数组使用其异步缓存资源预缓存 addAll 方法

如果传递给 event.waitUntil 的 promise 已拒绝。 如果发生这种情况,Service Worker 会被丢弃。

如果 promise 已解析, 安装成功,Service Worker 的状态将变为 'installed',然后进行激活。

激活

如果注册和安装成功, Service Worker 激活,并且其状态变为 'activating' 工作可在激活期间通过 Service Worker 的 activate 事件。 此事件中的一项典型任务是修剪旧的缓存 但对于全新的 Service Worker 来说 这则不适合当下 我们将在讨论 Service Worker 更新时进行详细介绍。

对于新的 Service Worker,activate 会在 install 成功后立即触发。 激活完成后, Service Worker 的状态变为 'activated'。 请注意, 新 Service Worker 在下一次导航或页面刷新之前不会开始控制页面。

处理 Service Worker 更新

部署第一个 Service Worker 后, 因此以后可能需要更新 例如,如果请求处理或预缓存逻辑中发生变更,则可能需要进行更新。

何时进行更新

浏览器将在以下情况下检查 Service Worker 的更新:

  • 用户导航到 Service Worker 作用域内的页面。
  • navigator.serviceWorker.register() 使用与当前安装的 Service Worker 不同的网址调用,但请勿更改 Service Worker 的网址!
  • navigator.serviceWorker.register() 使用与已安装的 Service Worker 相同的网址调用, 但范围不同 同样,为避免出现此问题,请尽可能将作用域置于源站的根目录。
  • 'push''sync' 等事件发生时 是在过去 24 小时内触发的,但别担心这些事件。

如何进行更新

了解浏览器何时更新 Service Worker 很重要, 而“方式”也同样如此。假设 Service Worker 的网址或范围保持不变, 当前安装的 Service Worker 只有在其内容发生更改时才会更新到新版本。

浏览器通过以下几种方式检测变化:

  • importScripts(如果适用)。
  • 在 Service Worker 的顶级代码发生任何变化时, 而这会影响浏览器为它生成的指纹。

浏览器会在此完成许多繁杂的工作。 为确保浏览器具备可靠地检测 Service Worker 内容变化所需的一切, 不要告知 HTTP 缓存保留该实体,也不要更改其文件名。 当导航到 Service Worker 的作用域内的新页面时,浏览器会自动执行更新检查。

手动触发更新检查

对于更新,注册逻辑通常不应更改。 不过,一种例外情况是网站上的会话长期存在。 在单页应用中,这种情况可能会发生 导航请求很少见 因为应用通常会在生命周期开始时遇到一个导航请求。 在这种情况下,可以在主线程上触发手动更新:

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

对于传统网站 或者,在用户会话不长期存在的情况下 可能没必要触发手动更新

安装

使用捆绑器生成静态资源时, 这些资源的名称中将包含哈希值 例如 framework.3defa9d2.js。 假设其中一些资源已预缓存以供日后离线访问。 这需要更新 Service Worker 以预缓存更新后的资源:

self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v2';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v2'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.ced4aef2.css',
      '/css/home.cbe409ad.css',
      '/js/home.109defa4.js',
      '/js/jquery.38caf32d.js'
    ]);
  }));
});

以下两点与之前的第一个 install 事件示例不同:

  1. 创建了一个键为 'MyFancyCacheName_v2' 的新 Cache 实例。
  2. 预缓存的资产名称已更改。

需要注意的一点是,更新的 Service Worker 将与上一个 Service Worker 一起安装。 这意味着旧 Service Worker 仍控制所有打开的页面,在安装后, 新密钥会进入等待状态,直到激活。

默认情况下,如果旧 Service Worker 没有控制任何客户端,则会激活新的 Service Worker。 当相关网站的所有打开的标签页都关闭时,就会发生这种情况。

激活

当更新的 Service Worker 安装完毕且等待阶段结束时, 旧的 Service Worker,则会被舍弃。 在更新的 Service Worker 的 activate 事件中执行的一项常见任务是修剪旧缓存。 使用以下命令获取所有打开的 Cache 实例的键,移除旧缓存: caches.keys 以及删除 caches.delete

self.addEventListener('activate', (event) => {
  // Specify allowed cache keys
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // Get all the currently active `Cache` instances.
  event.waitUntil(caches.keys().then((keys) => {
    // Delete all caches that aren't in the allow list:
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

旧的缓存无法自行清理。 我们自己也要这样做,否则就有 存储空间配额。 由于第一个 Service Worker 的 'MyFancyCacheName_v1' 已过期, 更新缓存许可名单指定'MyFancyCacheName_v2' 即删除具有不同名称的缓存

activate 事件将在旧缓存移除后完成。 此时,新 Service Worker 将控制页面, 终于替换旧的版本了!

整个生命周期

无论使用 Workbox 来处理 Service Worker 的部署和更新, 或直接使用 Service Worker API, 了解 Service Worker 的生命周期很有必要。 有了这种理解,Service Worker 的行为应该看起来更合乎逻辑而不是神秘。

如果你有兴趣深入了解这一主题, 不妨看看 Jake Archibald 的这篇文章。 围绕服务生命周期的整个过程存在大量的细微差别, 但可学的,在使用 Workbox 时,这些知识将远不止于此。