Service Worker 的生命周期是其最复杂的部分。如果您不知道它想要做什么以及有什么好处,您可能会觉得它在与您作对。不过,一旦您了解了其工作原理,就可以为用户提供顺畅、不打扰的更新,将 Web 和原生模式的优势融为一体。
这是一篇深入探究文章,但每个部分开头的要点涵盖了您需要了解的大部分内容。
意图
生命周期的目的是:
- 实现离线优先。
- 允许新的 Service Worker 在不中断当前 Service Worker 的情况下做好准备。
- 确保范围内的页面始终由同一个 Service Worker(或没有 Service Worker)控制。
- 确保您的网站一次只有一个版本在运行。
最后一点非常重要。如果没有 Service Worker,用户可以加载一个标签页访问您的网站,然后打开另一个标签页。这可能会导致网站的两个版本同时运行。有时这没问题,但如果您要处理存储空间,则很容易出现两个标签页对共享存储空间的管理方式有很大分歧的情况。这可能会导致错误,甚至更严重的数据丢失。
第一个 Service Worker
简而言之:
install事件是 Service Worker 收到的第一个事件,并且只会发生一次。- 传递给
installEvent.waitUntil()的 promise 会指示安装的时长以及成功或失败。 - 在成功完成安装并变为“有效”之前,Service Worker 不会收到
fetch和push等事件。 - 默认情况下,除非网页请求本身通过 Service Worker,否则网页的提取不会通过 Service Worker。因此,您需要刷新页面才能看到 Service Worker 的效果。
clients.claim()可以替换此默认设置,并控制不受控制的页面。
以下面的 HTML 为例:
<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered!', reg))
.catch(err => console.log('Boo!', err));
setTimeout(() => {
const img = new Image();
img.src = '/dog.svg';
document.body.appendChild(img);
}, 3000);
</script>
它会注册一个 Service Worker,并在 3 秒后添加一张狗狗图片。
以下是其 Service Worker sw.js:
self.addEventListener('install', event => {
console.log('V1 installing…');
// cache a cat SVG
event.waitUntil(
caches.open('static-v1').then(cache => cache.add('/cat.svg'))
);
});
self.addEventListener('activate', event => {
console.log('V1 now ready to handle fetches!');
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// serve the cat SVG from the cache if the request is
// same-origin and the path is '/dog.svg'
if (url.origin == location.origin && url.pathname == '/dog.svg') {
event.respondWith(caches.match('/cat.svg'));
}
});
它会缓存猫的图片,并在每次收到对 /dog.svg 的请求时传送该图片。不过,如果您运行上述示例,则会在首次加载页面时看到一只狗。点击刷新,您就会看到猫。
范围和控制
Service Worker 注册的默认范围是 ./(相对于脚本网址)。这意味着,如果您在 //example.com/foo/bar.js 注册 Service Worker,其默认范围为 //example.com/foo/。
我们将页面、工作器和共享工作器称为clients。您的 Service Worker 只能控制范围内的客户端。客户端被“控制”后,其提取操作会通过范围内的 Service Worker 进行。您可以通过 navigator.serviceWorker.controller 检测客户端是否受控,该属性将为 null 或 Service Worker 实例。
下载、解析和执行
当您调用 .register() 时,系统会下载您的第一个 Service Worker。如果您的脚本在初始执行时未能下载、解析或抛出错误,则注册 promise 会拒绝,并且 Service Worker 会被舍弃。
Chrome 的开发者工具会在控制台和“应用”标签页的 Service Worker 部分显示错误:
安装
Service Worker 接收到的第一个事件是 install。它会在 Service Worker 执行后立即触发,并且每个 Service Worker 只会调用一次。如果您更改了 Service Worker 脚本,浏览器会将其视为不同的 Service Worker,并且它会获得自己的 install 事件。我稍后会详细介绍这些更新。
install 事件是您在控制客户端之前缓存所需一切内容的机会。您传递给 event.waitUntil() 的 promise 会让浏览器知道安装何时完成以及是否成功。
如果 promise 被拒绝,则表示安装失败,浏览器会丢弃 Service Worker。它永远不会控制客户。这意味着,我们可以依赖 cat.svg 存在于 fetch 事件的缓存中。这是一个依赖项。
激活
当 Service Worker 准备好控制客户端并处理 push 和 sync 等功能事件时,您会收到 activate 事件。但这并不意味着调用 .register() 的页面会受到控制。
首次加载演示时,即使在 Service Worker 激活后很久才请求 dog.svg,它也不会处理该请求,您仍然会看到狗的图片。默认值为一致性,如果您的网页在没有 Service Worker 的情况下加载,其子资源也不会加载。如果您再次加载演示(即刷新页面),它将受到控制。网页和图片都会经历 fetch 事件,您会看到一只猫。
clients.claim
您可以在 Service Worker 激活后,在其中调用 clients.claim(),从而控制不受控制的客户端。
以下是上述演示的变体,它在其 activate 事件中调用 clients.claim()。您应该会看到一只猫。我之所以说“应该”,是因为这与时间有关。只有在 Service Worker 激活且 clients.claim() 在图片尝试加载之前生效时,您才会看到猫。
如果您使用 Service Worker 加载网页的方式与通过网络加载网页的方式不同,则 clients.claim() 可能会带来麻烦,因为 Service Worker 最终会控制一些在没有 Service Worker 的情况下加载的客户端。
更新 Service Worker
简而言之:
- 如果发生以下任一情况,则会触发更新:
- 导航到范围内的页面。
- 功能性事件,例如
push和sync,除非过去 24 小时内进行过更新检查。 - 仅在 Service Worker 网址发生更改时调用
.register()。不过,您应避免更改工作器网址。
- 大多数浏览器(包括 Chrome 68 及更高版本)在检查已注册 Service Worker 脚本的更新时,默认会忽略缓存标头。在通过
importScripts()提取 Service Worker 内加载的资源时,它们仍会遵循缓存标头。您可以在注册 Service Worker 时设置updateViaCache选项,以替换此默认行为。 - 如果您的 Service Worker 与浏览器中已有的 Service Worker 存在字节差异,则会被视为已更新。(我们正在扩展此功能,以便也包含导入的脚本/模块。)
- 更新后的 Service Worker 会与现有 Service Worker 一起启动,并获得自己的
install事件。 - 如果新 worker 的状态代码不正常(例如 404)、无法解析、在执行期间抛出错误或在安装期间拒绝,则新 worker 会被丢弃,但当前 worker 仍会保持活跃状态。
- 成功安装后,更新后的 worker 将
wait,直到现有 worker 控制的客户端数量为零。(请注意,客户端在刷新期间会重叠。) self.skipWaiting()可防止等待,这意味着 Service Worker 在完成安装后会立即激活。
假设我们更改了 Service Worker 脚本,使其返回马的图片而不是猫的图片:
const expectedCaches = ['static-v2'];
self.addEventListener('install', event => {
console.log('V2 installing…');
// cache a horse SVG into a new cache, static-v2
event.waitUntil(
caches.open('static-v2').then(cache => cache.add('/horse.svg'))
);
});
self.addEventListener('activate', event => {
// delete any caches that aren't in expectedCaches
// which will get rid of static-v1
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.map(key => {
if (!expectedCaches.includes(key)) {
return caches.delete(key);
}
})
)).then(() => {
console.log('V2 now ready to handle fetches!');
})
);
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// serve the horse SVG from the cache if the request is
// same-origin and the path is '/dog.svg'
if (url.origin == location.origin && url.pathname == '/dog.svg') {
event.respondWith(caches.match('/horse.svg'));
}
});
观看上述内容的演示。您应该仍会看到猫的图片。原因如下…
安装
请注意,我已将缓存名称从 static-v1 更改为 static-v2。这意味着我可以设置新缓存,而不会覆盖当前缓存中的内容,因为旧 Service Worker 仍在使用当前缓存。
此模式会创建特定于版本的缓存,类似于原生应用与其可执行文件捆绑的资源。您可能还拥有不特定于版本的缓存,例如 avatars。
正在等待
成功安装后,更新后的 Service Worker 会延迟激活,直到现有 Service Worker 不再控制客户端。此状态称为“等待”,浏览器通过这种方式确保一次只有一个版本的 Service Worker 运行。
如果您运行了更新后的演示,应该仍会看到猫的图片,因为 V2 worker 尚未激活。您可以在 DevTools 的“Application”标签页中看到新的 Service Worker 正在等待:
即使您只打开了一个演示标签页,仅刷新页面也不足以让新版本接管。这是因为浏览器导航的工作方式。当您进行导航时,当前页面在收到响应标头之前不会消失,即使收到响应标头,如果响应包含 Content-Disposition 标头,当前页面也可能会保留。由于存在这种重叠,当前 Service Worker 在刷新期间始终控制着客户端。
如需获取更新,请关闭或离开使用当前 Service Worker 的所有标签页。然后,当您再次前往演示页面时,应该会看到马。
此模式与 Chrome 的更新方式类似。Chrome 更新会在后台下载,但只有在 Chrome 重启后才会应用。在此期间,您可以继续使用当前版本,不会受到任何干扰。不过,这在开发过程中会带来不便,但开发者工具有一些方法可以简化此过程,我将在本文后面介绍这些方法。
激活
一旦旧 Service Worker 消失,并且新 Service Worker 能够控制客户端,就会触发此事件。此时非常适合执行在旧工作器仍在使用时无法执行的操作,例如迁移数据库和清除缓存。
在上面的演示中,我维护了一个我希望存在的缓存列表,在 activate 事件中,我删除了所有其他缓存,从而移除了旧的 static-v1 缓存。
如果您向 event.waitUntil() 传递一个 promise,它会缓冲功能事件(fetch、push、sync 等),直到 promise 解析。因此,当 fetch 事件触发时,激活即告完成。
跳过等待阶段
等待阶段意味着您一次只能运行一个版本的网站,但如果您不需要该功能,可以通过调用 self.skipWaiting() 来让新的 Service Worker 更快激活。
这会导致 Service Worker 踢出当前活跃的 Worker,并在进入等待阶段后立即自行激活(如果已处于等待阶段,则立即激活)。这不会导致工作器跳过安装,而只是跳过等待。
何时调用 skipWaiting() 并不重要,只要在等待期间或之前调用即可。在 install 事件中调用它非常常见:
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
// caching etc
);
});
但您可能希望在向 Service Worker 发送 postMessage() 后调用它。例如,您希望在用户互动后skipWaiting()。
以下是使用 skipWaiting() 的演示。您应该会看到一张奶牛的照片,而无需离开当前页面。与 clients.claim() 一样,这也是一场竞赛,因此只有在新 Service Worker 在页面尝试加载图片之前提取、安装并激活时,您才会看到奶牛。
手动更新
如前所述,浏览器会在导航和功能事件发生后自动检查更新,但您也可以手动触发更新:
navigator.serviceWorker.register('/sw.js').then(reg => {
// sometime later…
reg.update();
});
如果您预计用户会长时间使用您的网站而不重新加载,则可能需要定期(例如每小时)调用 update()。
避免更改 Service Worker 脚本的网址
如果您已阅读我关于缓存最佳实践的博文,则可以考虑为 Service Worker 的每个版本提供一个唯一的网址。请勿这样做!对于 Service Worker 而言,这通常是不好的做法,只需在当前位置更新脚本即可。
这可能会导致出现如下问题:
index.html将sw-v1.js注册为 Service Worker。sw-v1.js会缓存并提供index.html,以便它能以离线优先的方式运行。- 您更新了
index.html,以便注册新的sw-v2.js。
如果您执行上述操作,用户将永远不会获得 sw-v2.js,因为 sw-v1.js 会从其缓存中提供旧版 index.html。您已经将自己置于需要更新 Service Worker 才能更新 Service Worker 的境地。呃。
不过,对于上面的演示,我确实更改了 Service Worker 的网址。这样,您就可以在演示过程中切换版本。我不会在生产环境中这样做。
让开发变得轻而易举
Service Worker 的生命周期在设计时充分考虑了用户体验,但在开发过程中却有些麻烦。好在有一些工具可以提供帮助:
重新加载时更新
这是我最喜欢的。
这会将生命周期更改为对开发者更友好。每次导航都会:
- 重新提取 Service Worker。
- 即使新版本与旧版本完全相同,也请将其作为新版本安装,这意味着您的
install事件会运行,并且缓存会更新。 - 跳过等待阶段,以便激活新的 Service Worker。
- 浏览页面。
这意味着,您将在每次导航(包括刷新)时获得更新,而无需重新加载两次或关闭标签页。
跳过等待
如果您有正在等待的 worker,可以在开发者工具中点击“skip waiting”(跳过等待),立即将其提升为“active”(活跃)。
Shift-reload
如果您强制重新加载页面(shift-reload),则会完全绕过 Service Worker。它将不受控制。此功能已纳入规范,因此可在其他支持 Service Worker 的浏览器中运行。
处理更新
Service Worker 是作为可扩展 Web 的一部分设计的。我们的理念是,作为浏览器开发者,我们承认自己在 Web 开发方面不如 Web 开发者。因此,我们不应提供狭隘的高级 API,这些 API 使用我们喜欢的模式来解决特定问题,而是让您能够访问浏览器的核心,并让您以最适合您用户的方式,按照您想要的方式来完成。
因此,为了尽可能多地支持各种模式,整个更新周期都是可观察的:
navigator.serviceWorker.register('/sw.js').then(reg => {
reg.installing; // the installing worker, or undefined
reg.waiting; // the waiting worker, or undefined
reg.active; // the active worker, or undefined
reg.addEventListener('updatefound', () => {
// A wild service worker has appeared in reg.installing!
const newWorker = reg.installing;
newWorker.state;
// "installing" - the install event has fired, but not yet complete
// "installed" - install complete
// "activating" - the activate event has fired, but not yet complete
// "activated" - fully active
// "redundant" - discarded. Either failed install, or it's been
// replaced by a newer version
newWorker.addEventListener('statechange', () => {
// newWorker.state has changed
});
});
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
// This fires when the service worker controlling this page
// changes, eg a new worker has skipped waiting and become
// the new active worker.
});
生命周期永无止境
如您所见,了解 Service Worker 生命周期大有裨益。了解这些知识后,Service Worker 的行为应该会显得更合乎逻辑,不那么神秘。这些知识将让您在部署和更新 Service Worker 时更有信心。