Service Worker 思维模式

如何思考 Service Worker。

Service Worker 非常强大,绝对值得学习。它们可让您为用户提供全新水平的体验。您的网站可以即时加载。它可以离线使用。它可以作为平台专用应用进行安装,使用起来更加精细,但又具备网络的普及性和自由度。

但 Service Worker 与我们大多数 Web 开发者习惯使用的任何事物不同。这类游戏的学习过程较为复杂,有些问题需要你注意。

我最近与 Google Developers 合作开发了一个项目 Service Workies,这是一个帮助了解 Service Worker 的免费游戏。在构建它以及处理 Service Worker 的来龙去脉时,我遇到了一些麻烦。给我最大的帮助是想出几个描绘性的比喻。在这篇博文中,我们将探索这些心智模式,并探究使 Service Worker 变得既灵活又聪明的自相矛盾特征。

相同,但不同

在为 Service Worker 编写代码时,很多方面都会让您感到熟悉。您可以使用自己喜爱的新 JavaScript 语言功能了。您可以像监听界面事件一样监听生命周期事件。您可以像往常一样通过承诺来管理控制流。

但是,其他 Service Worker 行为会让您感到困惑。尤其是在您刷新页面但没有看到已应用的代码更改时。

新图层

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

Service Worker 充当客户端与服务器之间的中间层

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

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

Service Workies 游戏中,我们介绍了有关 Service Worker 生命周期的许多细节,并为您提供大量使用该生命周期的练习。

功能强大,但功能有限

在网站上安排一个 Service Worker,会为您带来不可思议的好处。您的网站可以:

  • 即使在用户离线时也能完美运行
  • 通过缓存大幅提升性能
  • 使用推送通知
  • 作为 PWA 安装

尽管 Service Worker 可以执行各种操作,但它们受到设计的限制。他们无法与您的网站同步或在同一线程中执行任何操作。这意味着无法访问以下内容:

  • localStorage
  • DOM
  • 窗户

好消息是,网页可以通过多种方式与其 Service Worker 通信,包括直接 postMessage、一对一消息渠道和一对多广播频道

长期存在,但短期有效

即使在用户离开您的网站或关闭标签页后,活跃的 Service Worker 仍会继续工作。浏览器会保留此 Service Worker,以便在用户下次返回您的网站时准备就绪。在发出第一个请求之前,Service Worker 有机会拦截请求并控制页面。正因如此,网站才能离线工作。即使用户没有连接到互联网,Service Worker 也可以提供页面本身的缓存版本。

Service Workies 中,我们通过 Kolohe(一个友好的 Service Worker)拦截和处理请求来直观呈现这一概念。

已停止

尽管 Service Worker 看起来不死,但它们几乎随时被停止。浏览器不希望在当前未执行任何操作的 Service Worker 上浪费资源。停止与终止并不相同,Service Worker 仍保持安装和激活状态。然后就直接进入休眠状态了。下次需要时(例如,处理请求时),浏览器会将其唤醒。

waitUntil

由于始终有可能会进入休眠状态,因此 Service Worker 需要一种方法来让浏览器知道它正在执行重要操作,并且不想打盹。这正是 event.waitUntil() 的用武之地。此方法会延长其使用生命周期,使其不会停止,并且不会进入其生命周期的下一阶段,直到我们准备就绪。这让我们有时间设置缓存、从网络获取资源等。

此示例告知浏览器,在创建 assets 缓存并使用剑图片填充之前,我们的 Service Worker 不会完成安装:

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

注意全局状态

发生这种启动/停止时,Service Worker 的全局范围会重置。因此,请注意不要在您的 Service Worker 中使用任何全局状态,否则,下次它被唤醒且状态与预期不同时,您会感到失望。

请参考下面这个使用全局状态的示例:

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

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

在每次请求时,此 Service Worker 将记录一个数字(假设为 0.13981866382421893)。hasHandledARequest 变量也会更改为 true。现在,Service Worker 会处于空闲状态,因此浏览器会将其停止。下次出现请求时,将再次需要 Service Worker,因此浏览器会将其唤醒。其脚本会再次评估。现在,hasHandledARequest 重置为 false,而 favoriteNumber 则完全不同 - 0.5907281835659033

您不能依赖于 Service Worker 中的存储状态。此外,创建类似消息渠道的实例也会导致 bug:每次 Service Worker 停止/启动时,您都会得到一个全新的实例。

Service Workies 第 3 章中,我们可视化停止的 Service Worker 在等待被唤醒时失去所有颜色。

已停止的 Service Worker 的可视化

彼此独立,但彼此独立

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

与其他 Service Worker 的缓存混淆

安装时,SW2 可以进行设置,通常是创建和填充缓存。但请注意:这个新的 Service Worker 可以访问当前 Service Worker 有权访问的所有内容。如果不小心,新的等待 Service Worker 确实可能会给当前的 Service Worker 带来问题。以下是一些可能给您带来麻烦的示例:

  • SW2 可能会删除 SW1 正在使用的缓存。
  • SW2 可以修改 SW1 正在使用的缓存内容,导致 SW1 使用页面不符合预期的资源进行响应。

跳过 skipWaiting

Service Worker 还可以使用有风险的 skipWaiting() 方法,在完成安装后立即控制页面。这通常是一个糟糕的主意,除非您是有意替换有缺陷的 Service Worker。新 Service Worker 可能正在使用当前页面不符合其预期的更新资源,从而导致错误和 bug。

开始清理

防止 Service Worker 相互干扰的方法是确保它们使用不同的缓存。完成该操作最简单的方法是,对使用的缓存名称进行版本控制。

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 完全不同的缓存来满足自己的需求。

缓存的可视化

结束清理

一旦您的 Service Worker 达到 activated 状态,您就会知道它已接管,并且之前的 Service Worker 是冗余的。此时,必须在旧 Service Worker 之后进行清理。不仅尊重用户的但这样也能避免意外的错误

caches.match() 方法是常用的快捷方式,用于从任何存在匹配项的缓存中检索项。但会按照缓存的创建顺序遍历缓存。因此,假设您在两个不同的缓存(assets-1assets-2)中有两个版本的脚本文件 app.js。您的网页需要使用存储在 assets-2 中的新版脚本。但如果您尚未删除旧缓存,caches.match('app.js') 将从 assets-1 返回旧缓存,这很可能会破坏您的网站。

在之前的 Service Worker 之后进行清理时,只需删除新 Service Worker 不需要的任何缓存即可:

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 Worker,将有助于您自信地构建自己的心态。掌握其中的窍门后,您就能为用户打造不可思议的体验。

如果您想通过玩游戏了解这一切,那么恭喜您!玩玩 Service Workies,了解 Service Worker 杀死离线怪物的方法。