如果不了解服务工件的生命周期,就很难知道它们在做什么。它们的内部运作机制似乎不透明,甚至是任意的。请务必注意,与任何其他浏览器 API 一样,服务工件行为已明确定义和指定,可实现离线应用,同时还能促进更新,而不会中断用户体验。
在深入了解 Workbox 之前,请务必了解 Service Worker 生命周期,以便了解 Workbox 的运作方式。
定义字词
在深入了解服务工件生命周期之前,有必要定义一些与该生命周期运作方式相关的术语。
控制和范围
控制概念对于了解服务工件的工作原理至关重要。被描述为由 Service Worker 控制的网页是指允许 Service Worker 代表其拦截网络请求的网页。Service Worker 存在,并且能够在给定范围内为网页执行工作。
范围
服务工件的作用域取决于其在网络服务器上的位置。如果 Service Worker 在位于 /subdir/index.html
的网页上运行,并且位于 /subdir/sw.js
,则 Service Worker 的范围为 /subdir/
。如需了解作用域的实际运作方式,请查看以下示例:
- 前往 https://service-worker-scope-viewer.glitch.me/subdir/index.html。系统会显示一条消息,提示没有 Service Worker 在控制该网页。不过,该网页会从
https://service-worker-scope-viewer.glitch.me/subdir/sw.js
注册 Service Worker。 - 重新加载页面。 由于 Service Worker 已注册且目前处于活跃状态,因此它会控制网页。系统会显示一个表单,其中包含服务工件的范围、当前状态及其网址。注意:必须重新加载页面与作用域无关,而是与服务工件生命周期有关,我们稍后会对此进行说明。
- 现在,前往 https://service-worker-scope-viewer.glitch.me/index.html。 即使在此来源上注册了 Service Worker,系统仍会显示一条消息,提示当前没有 Service Worker。这是因为此网页不在注册的 Service Worker 的范围内。
作用域限制了 Service Worker 控制的网页。在此示例中,这意味着从 /subdir/sw.js
加载的服务工件只能控制位于 /subdir/
或其子树中的网页。
以上是默认的范围限定方式,但您可以通过设置 Service-Worker-Allowed
响应标头以及将 scope
选项传递给 register
方法来替换允许的最大范围。
除非有充分的理由将 Service Worker 作用域限制为来源的一部分,否则请从网络服务器的根目录加载 Service Worker,以使其作用域尽可能广泛,并且无需担心 Service-Worker-Allowed
标头。这样对所有人来说都更简单。
客户
当我们说 Service Worker 在控制网页时,实际上是指它在控制客户端。客户端是指网址位于该服务工件范围内的任何打开的网页。具体而言,这些是 WindowClient
的实例。
新服务工件的生命周期
为了让服务工件能够控制网页,必须先使其“存在”。我们先来看看,如果为没有任何活跃的服务工件(SW)的网站部署全新的 SW,会发生什么情况。
注册
注册是 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>
此代码在主线程上运行,并执行以下操作:
- 由于用户首次访问网站时没有注册 Service Worker,因此请等待网页完全加载完毕,然后再注册 Service Worker。这样做有助于避免在服务工件预缓存任何内容时出现带宽争用。
- 虽然 Service Worker 得到了良好的支持,但快速检查有助于避免在不支持 Service Worker 的浏览器中出现错误。
- 当页面完全加载后,如果支持 Service Worker,请注册
/sw.js
。
以下是一些需要了解的关键事项:
- Service Worker 仅通过 HTTPS 或 localhost 提供。
- 如果服务工件的代码包含语法错误,注册会失败,并且系统会舍弃该服务工件。
- 提醒:Service Worker 在某个作用域内运行。在这里,作用域是整个来源,因为它是从根目录加载的。
- 注册开始时,服务工件状态会设为
'installing'
。
注册完成后,系统便会开始安装。
安装
服务工件会在注册后触发其 install
事件。每个服务工件只会调用一次 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.waitUntil
的作用。event.waitUntil
会接受一个 Promise,并等待该 Promise 解析完毕。在此示例中,该 promise 会执行两项异步操作:
- 创建一个名为
'MyFancyCache_v1'
的新Cache
实例。 - 创建缓存后,系统会使用其异步
addAll
方法预缓存一组资源网址。
如果传递给 event.waitUntil
的 promise 被拒绝,安装将失败。如果发生这种情况,系统会舍弃该服务工件。
如果这些 Promise 解析,安装将成功,并且服务工件的状态将更改为 'installed'
,然后激活。
激活
如果注册和安装成功,服务工件会激活,其状态变为 'activating'
。您可以在服务工件的 activate
事件中执行激活期间的工作。此事件中的典型任务是修剪旧缓存,但对于全新的服务工件,这目前并不相关,我们将在讨论服务工件更新时对此进行详细介绍。
对于新的服务工件,activate
会在 install
成功后立即触发。激活完成后,服务工件的状态变为 'activated'
。请注意,默认情况下,新 Service Worker 不会在下次导航或页面刷新之前开始控制页面。
处理 Service Worker 更新
部署第一个服务工件后,您可能需要稍后对其进行更新。例如,如果请求处理或预缓存逻辑发生更改,则可能需要更新。
更新时间
在以下情况下,浏览器会检查服务工件的更新:
- 用户导航到 Service Worker 的范围内的网页。
- 使用与当前安装的服务工件不同的网址调用
navigator.serviceWorker.register()
,但请勿更改服务工件的网址! - 调用
navigator.serviceWorker.register()
时使用的网址与已安装的服务工件相同,但作用域不同。再次提醒,请尽可能将作用域保留在源的根目录中,以避免出现这种情况。 - 在过去 24 小时内触发了
'push'
或'sync'
等事件,但暂时不用担心这些事件。
更新方式
了解浏览器何时更新服务工件很重要,但“如何”更新也很重要。假设 Service Worker 的网址或作用域保持不变,则当前安装的 Service Worker 仅在其内容发生更改时才会更新为新版本。
浏览器会通过以下几种方式检测更改:
importScripts
请求对脚本进行的任何逐字节更改(如果适用)。- 服务工件的顶级代码中的任何更改,都会影响浏览器为其生成的指纹。
浏览器在这里要完成大量繁重工作。 为确保浏览器拥有可靠检测服务工件内容更改所需的一切信息,请勿指示 HTTP 缓存保留该内容,也不要更改其文件名。当浏览器导航到服务工件作用域内的新页面时,会自动执行更新检查。
手动触发更新检查
关于更新,注册逻辑通常不应更改。不过,如果网站上的会话是长时有效的,则可能是一个例外情况。 在导航请求很少的单页应用中,可能会发生这种情况,因为应用通常会在生命周期开始时遇到一个导航请求。在这种情况下,可以在主线程中触发手动更新:
navigator.serviceWorker.ready.then((registration) => {
registration.update();
});
对于传统网站,或者在用户会话不持久的任何情况下,可能无需触发手动更新。
安装
使用捆绑器生成静态资源时,这些资源的名称中会包含哈希值,例如 framework.3defa9d2.js
。假设其中一些资源已预缓存,以供稍后离线访问。这需要更新服务工件,以预缓存更新后的资源:
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
事件示例相比,有两点不同:
- 系统会创建一个键为
'MyFancyCacheName_v2'
的新Cache
实例。 - 预缓存的资源名称已更改。
请注意,更新后的服务工件会与之前的服务工件一起安装。这意味着,旧版服务工件仍会控制所有打开的页面,而新版服务工件在安装后会进入等待状态,直到被激活。
默认情况下,当旧服务工件不再控制任何客户端时,新服务工件将激活。当相关网站的所有打开标签页都关闭时,就会发生这种情况。
激活
更新后的服务工件安装完毕且等待阶段结束后,该服务工件会激活,旧服务工件会被舍弃。在更新后的服务工件的 activate
事件中执行的一项常见任务是修剪旧缓存。使用 caches.keys
获取所有打开的 Cache
实例的键,然后使用 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);
}
}));
}));
});
旧缓存不会自行整理。我们需要自行执行此操作,否则可能会超出存储空间配额。由于第一个服务工件的 'MyFancyCacheName_v1'
已过时,因此缓存许可名单会更新为指定 'MyFancyCacheName_v2'
,这会删除名称不同的缓存。
移除旧缓存后,activate
事件将结束。此时,新 Service Worker 将接管网页,最终取代旧 Service Worker!
生命周期永不停息
无论是使用 Workbox 来处理服务工件部署和更新,还是直接使用 Service Worker API,了解服务工件生命周期都是有益的。有了这样的理解,服务工件行为应该会显得更合理,而不是神秘。
如果您有兴趣深入了解此主题,不妨参阅 Jake Archibald 撰写的这篇文章。服务生命周期的整个过程包含许多细微之处,但这些细微之处是可以了解的,在使用 Workbox 时,这些知识将发挥重要作用。