Service Worker 思维模式

如何思考服务工件。

服务工件功能强大,绝对值得学习。借助这些功能,您可以为用户提供全新的体验。您的网站可以即时加载。它可以离线使用。它可以作为平台专用应用安装,并且在使用体验上丝毫不逊色于原生应用,但同时又具有 Web 应用的广泛覆盖面和自由度。

但服务工件与大多数 Web 开发者的常用工具大不相同。它们的学习曲线较陡,并且存在一些需要注意的问题。

Google 开发者和我最近合作了一个项目,名为 Service Workies,这是一款免费游戏,旨在帮助用户了解服务工件。在构建该应用并处理服务工件的复杂细节时,我遇到了一些问题。对我来说最有帮助的是,我想到了一些描述性的比喻。在本文中,我们将探索这些心理模型,并深入了解使服务工件既棘手又出色的矛盾特征。

相同,但不同

在编写服务工件代码时,您会发现很多内容都很熟悉。您可以使用自己喜爱的新 JavaScript 语言功能。监听生命周期事件的方式与监听界面事件一样。您可以像往常一样使用 promise 来管理控制流。

但其他 Service Worker 行为却让您感到困惑不已。尤其是在您刷新页面后,没有看到代码更改生效时。

新图层

通常,在构建网站时,您只需考虑两个层:客户端和服务器。服务工件是位于中间的全新层。

服务工件充当客户端和服务器之间的中间层

您可以将 Service Worker 视为一种浏览器扩展程序,您的网站可以在用户的浏览器中安装此类扩展程序。安装后,Service Worker 会使用强大的中间层扩展您网站的浏览器。此 Service Worker 层可以拦截并处理您的网站发出的所有请求。

服务工件层有自己的生命周期,独立于浏览器标签页。简单的页面刷新不足以更新服务工件,就像您不会期望页面刷新更新部署在服务器上的代码一样。每层都有自己的更新规则。

Service Workies 游戏中,我们将介绍 Service Worker 生命周期的许多细节,并为您提供大量实践机会来使用它。

功能强大,但受限

在您的网站上使用 Service Worker 可为您带来诸多好处。您的网站可以:

  • 即使用户处于离线状态,也能正常运行
  • 通过缓存显著提升性能
  • 使用推送通知
  • PWA 的形式安装

虽然服务工件可以执行很多操作,但其功能受到设计限制。它们无法执行任何同步操作,也无法与您的网站在同一线程中执行操作。这意味着您将无法访问以下内容:

  • localStorage
  • DOM
  • 窗口

好消息是,网页可以通过多种方式与其服务工件进行通信,包括直接 postMessage、一对一消息通道和一对多广播通道

长期有效,但有效期较短

即使用户离开您的网站或关闭标签页,活跃的 Service Worker 也会继续运行。浏览器会保留此服务工件,以便在用户下次访问您的网站时使用。在发出第一个请求之前,Service Worker 有机会拦截该请求并控制网页。这正是网站能够离线运行的原因:即使用户未连接到互联网,Service Worker 也可以提供网页本身的缓存版本。

服务工作器中,我们通过 Kolohe(一个友好的服务工作器)拦截和处理请求来直观呈现此概念。

已停止

虽然 Service Worker 看起来是永久存在的,但几乎随时都可以停止。浏览器不希望在当前未执行任何操作的 Service Worker 上浪费资源。停止与终止不同,服务工件仍会保持安装和激活状态。它只是进入了休眠状态。下次需要该脚本时(例如,处理请求),浏览器会将其唤醒。

waitUntil

由于服务工件随时都有可能被休眠,因此需要一种方法来告知浏览器自己正在执行重要任务,不想休息。这时,event.waitUntil() 就派上用场了。此方法会延长其所使用的生命周期,使其在我们准备就绪之前不会停止也不会进入生命周期的下一个阶段。这样,我们就有时间设置缓存、从网络提取资源等。

以下示例会告知浏览器,在创建 assets 缓存并填充剑图片之前,我们的服务工件尚未安装完毕:

self.addEventListener("install", event => {
  event.waitUntil(
    caches.open("assets").then(cache => {
      return cache.addAll(["/weapons/sword/blade.png"]);
    })
  );
});

注意全局状态

发生这种启动/停止时,系统会重置服务工件的全局作用域。因此,请务必不要在服务工件中使用任何全局状态,否则当它下次唤醒时,状态与预期不同,您会很失望。

请考虑以下使用全局状态的示例:

const favoriteNumber = Math.random();
let hasHandledARequest = false;

self.addEventListener("fetch", event => {
  console.log(favoriteNumber);
  console.log(hasHandledARequest);
  hasHandledARequest = true;
});

在处理每个请求时,此服务工件都会记录一个数字(假设为 0.13981866382421893)。hasHandledARequest 变量也会更改为 true。现在,Service Worker 会空闲一段时间,因此浏览器会将其停止。下次有请求时,系统又需要 Service Worker,因此浏览器会唤醒它。系统会重新评估其脚本。现在,hasHandledARequest 已重置为 false,而 favoriteNumber 是完全不同的内容 - 0.5907281835659033

您无法依赖服务工件中存储的状态。此外,创建消息通道等实例可能会导致 bug:每次服务工件停止/启动时,您都会获得一个全新的实例。

“服务工作器”第 3 章中,我们将停止的服务工作器视为在等待被唤醒时失去所有颜色。

已停止的服务工件的可视化

在一起,但分开

您的网页一次只能由一个 Service Worker 控制。但它可以同时安装两个服务工件。更改服务工件代码并刷新页面后,您实际上并未修改服务工件。Service Worker 是immutable的。而是要创建一个全新的账号。这个新的服务工件(我们称之为 SW2)将安装,但尚未激活。它必须等待当前的服务工件 (SW1) 终止(当用户离开您的网站时)。

干扰其他服务工件的缓存

在安装过程中,SW2 可以进行设置(通常是创建和填充缓存)。不过请注意:这个新服务工件可以访问当前服务工件可以访问的所有内容。如果您不小心,新的等待状态 Service Worker 可能会给当前的 Service Worker 带来麻烦。以下是一些可能会给您带来麻烦的示例:

  • SW2 可能会删除 SW1 正在使用的缓存。
  • SW2 可能会修改 SW1 正在使用的缓存的内容,导致 SW1 响应网页未预期的资源。

跳过 skipWaiting

Service Worker 还可以在安装完成后立即使用风险较高的 skipWaiting() 方法来控制网页。除非您有意尝试替换存在 bug 的服务工件,否则通常不建议这样做。新的服务工件可能会使用当前网页不期望的更新资源,从而导致错误和 bug。

从清洁开始

如需防止服务工件相互覆盖,请确保它们使用不同的缓存。实现此目的的最简单方法是为它们使用的缓存名称设置版本。

const version = 1;
const assetCacheName = `assets-${version}`;

self.addEventListener("install", event => {
  caches.open(assetCacheName).then(cache => {
    // confidently do stuff with your very own cache
  });
});

部署新的 Service Worker 时,您需要提升 version,以便它使用与上一个 Service Worker 完全分离的缓存来执行所需操作。

缓存的可视化

干净利落地结束

当您的服务工件达到 activated 状态时,您就知道它已接管,之前的服务工件已过时。此时,请务必清理旧的服务工件。这不仅能遵守用户的缓存存储空间限制,还能防止意外 bug。

caches.match() 方法是一种常用的快捷方式,用于从包含匹配项的任何缓存中检索项。但它会按创建顺序迭代缓存。假设您在两个不同的缓存(assets-1assets-2)中拥有脚本文件 app.js 的两个版本。您的网页预期使用存储在 assets-2 中的较新脚本。但是,如果您尚未删除旧缓存,caches.match('app.js') 将从 assets-1 返回旧缓存,并且很可能会破坏您的网站。

如需清理之前的服务工件,只需删除新服务工件不需要的任何缓存即可:

const version = 2;
const assetCacheName = `assets-${version}`;

self.addEventListener("activate", event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== assetCacheName){
            return caches.delete(cacheName);
          }
        });
      );
    });
  );
});

防止您的 Service Worker 相互干扰需要付出一些努力并遵守一些规则,但值得您这么做。

Service Worker 思维方式

在思考服务工件时,采用正确的思维方式有助于您充满信心地构建服务工件。熟悉这些功能后,您将能够为用户打造出色的体验。

如果您想通过玩游戏来了解这一切,那么您很幸运!玩一玩 Service Workies,了解 Service Worker 的运作方式,以便消灭离线怪兽。