Service worker 的思维模式

思考 Service Worker 时的思维方式。

Service Worker 非常强大,绝对值得学习。它们让您可以为用户提供全新的体验。您的网站可以瞬间加载。它可以离线工作。它可以作为特定于平台的应用程序安装,并且感觉同样精致,但具有 Web 的覆盖范围和自由度。

但是 Service Worker 与我们大多数 Web 开发者习惯的任何东西都不同。它们具有陡峭的学习曲线和一些您必须注意的障碍。

Google 开发者 和我最近合作了一个项目——Service Workies——一个用于理解 Service Worker 的免费游戏。在构建它并处理 Service Worker 复杂而细微之处的过程中,我遇到了一些障碍。对我帮助最大的是提出了一些描述性的隐喻。在这篇文章中,我们将探索这些心理模型,并围绕使 Service Worker 既棘手又出色的矛盾特性展开思考。

相同但不同

在编写 Service Worker 代码时,许多事情都会感觉很熟悉。您可以使用您最喜欢的新 JavaScript 语言特性。您像使用 UI 事件一样监听生命周期事件。您像习惯的那样使用 Promise 管理控制流。

但是其他 Service Worker 行为会让您困惑地挠头。尤其是当您刷新页面并且没有看到您的代码更改应用时。

一个新的层级

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

A service worker acts as a middle layer between the client and the server

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

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

Service Workies 游戏中,我们涵盖了 Service Worker 生命周期的许多细节,并为您提供了大量练习机会。

功能强大,但有限制

在您的网站上拥有 Service Worker 可以为您带来难以置信的好处。您的网站可以

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

尽管 Service Worker 可以做很多事情,但它们在设计上是有限制的。它们无法执行任何同步操作或与您的网站在同一线程中执行操作。因此,这意味着无法访问

  • localStorage
  • DOM
  • window

好消息是,您的页面可以通过多种方式与其 Service Worker 通信,包括直接 postMessage、一对一 Message Channel 和一对多 Broadcast Channel

长寿命,但短时存在

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

Service Workies 中,我们使用 Kolohe(一个友好的 Service Worker)拦截和处理请求来可视化这个概念。

已停止

尽管 Service Worker 看起来是不朽的,但它们几乎随时都可能被停止。浏览器不想在当前未执行任何操作的 Service Worker 上浪费资源。被停止与被终止不同——Service Worker 仍然安装并激活。它只是进入休眠状态。下次需要它时(例如,处理请求),浏览器会将其唤醒。

waitUntil

由于 Service Worker 始终可能进入休眠状态,因此您的 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.13981866382421893hasHandledARequest 变量也会更改为 true。现在 Service Worker 处于空闲状态一段时间,因此浏览器停止了它。下次有请求时,再次需要 Service Worker,因此浏览器将其唤醒。它的脚本被再次评估。现在 hasHandledARequest 重置为 false,并且 favoriteNumber 是完全不同的东西——0.5907281835659033

您不能依赖 Service Worker 中存储的状态。此外,创建诸如 Message Channel 之类的实例可能会导致 Bug:每次 Service Worker 停止/启动时,您都会获得一个全新的实例。

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

visualization of a stopped 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 使用页面未预期的资源进行响应。

跳过等待

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 完全独立的缓存来执行所需的操作。

visualization of a cache

清洁结束

一旦您的 Service Worker 达到 activated 状态,您就知道它已经接管,并且之前的 Service Worker 是多余的。此时,清理旧 Service Worker 非常重要。这不仅尊重用户的缓存存储限制,还可以防止意外的 Bug。

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 Worker。一旦您掌握了它们,您将能够为您的用户创造令人难以置信的体验。

如果您想通过玩游戏来理解所有这些,那么您真幸运!去玩 Service Workies,在那里您将学习 Service Worker 的方法,以便杀死离线野兽。