思考 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 是一个全新的层级,位于中间。
将您的 Service Worker 视为一种浏览器扩展程序——您的网站可以在用户的浏览器中安装的扩展程序。一旦安装,Service Worker 就会使用强大的中间层扩展您网站的浏览器。这个 Service Worker 层可以拦截和处理您的网站发出的所有请求。
Service Worker 层有其自己的生命周期,独立于浏览器标签页。简单的页面刷新不足以更新 Service Worker——就像您不会期望页面刷新来更新部署在服务器上的代码一样。每个层级都有其自己独特的更新规则。
在 Service Workies 游戏中,我们涵盖了 Service Worker 生命周期的许多细节,并为您提供了大量练习机会。
功能强大,但有限制
在您的网站上拥有 Service Worker 可以为您带来难以置信的好处。您的网站可以
尽管 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.13981866382421893
。hasHandledARequest
变量也会更改为 true
。现在 Service Worker 处于空闲状态一段时间,因此浏览器停止了它。下次有请求时,再次需要 Service Worker,因此浏览器将其唤醒。它的脚本被再次评估。现在 hasHandledARequest
重置为 false
,并且 favoriteNumber
是完全不同的东西——0.5907281835659033
。
您不能依赖 Service Worker 中存储的状态。此外,创建诸如 Message Channel 之类的实例可能会导致 Bug:每次 Service Worker 停止/启动时,您都会获得一个全新的实例。
在 Service Workies 第 3 章 中,我们将已停止的 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 完全独立的缓存来执行所需的操作。
清洁结束
一旦您的 Service Worker 达到 activated
状态,您就知道它已经接管,并且之前的 Service Worker 是多余的。此时,清理旧 Service Worker 非常重要。这不仅尊重用户的缓存存储限制,还可以防止意外的 Bug。
caches.match()
方法是从任何缓存中检索项目的常用快捷方式,只要有匹配项。但它会按照缓存的创建顺序迭代缓存。假设您在两个不同的缓存——assets-1
和 assets-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 的方法,以便杀死离线野兽。