Service Worker 的生命周期是最复杂的一环。如果您不了解它要做什么以及它有哪些优势,那么您会感觉它让您败下阵来。然而,一旦您明白它的工作原理,您就可以向用户提供几乎无法察觉的无缝更新,从而使网络和原生模式的优势为您所用。
这是一个深度教程,但每个章节开头的项目列表包含了您需要了解的大部分内容。
intent
服务工作线程生命周期的目的:
- 实现离线优先。
- 允许新 Service Worker 自行做好运行准备,无需中断当前的 Service Worker。
- 确保整个过程中作用域页面由同一个 Service Worker(或者没有 Service Worker)控制。
- 确保每次只运行网站的一个版本。
最后一点非常重要。如果没有 Service Worker,用户可以将一个标签页加载到您的网站,稍后打开另一个标签页。这会导致同时运行网站的两个版本。有时候这样做没什么问题,但如果您正在处理存储,那么,出现两个标签很容易会让您的操作中断,因为它们的共享的存储空间管理机制大相径庭。这可能会导致错误,更糟糕的情况是导致数据丢失。
第一个 Service Worker
简而言之:
install
事件是服务工作线程获取的第一个事件,并且只发生一次。- 传递到
installEvent.waitUntil()
的一个 promise 可表明安装的持续时间以及安装是否成功。 - 在成功完成安装并处于“活动状态”之前,Service Worker 不会收到
fetch
和push
等事件。 - 默认情况下,不会通过 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
时提供该图像。不过,如果您运行上述示例,首次加载页面时您看到的是一条小狗。按 refresh,您将看到小猫。
范围和控制
Service Worker 注册的默认作用域是与脚本网址相对的 ./
。这意味着如果您在 //example.com/foo/bar.js
注册一个服务工作线程,则它的默认作用域为 //example.com/foo/
。
我们将页面、Worker 和共享的 Worker 称为 clients
。您的 Service Worker 只能控制位于作用域内的客户端。在客户端“受控制”后,它在获取数据时将执行作用域内的 Service Worker。您可以通过 navigator.serviceWorker.controller
(其将为 null 或一个 Service Worker 实例)检测客户端是否受控制。
下载、解析和执行
在调用 .register()
时,将下载您的第一个 Service Worker。如果您的脚本在初始执行中未能进行下载、解析,或引发错误,则注册器 promise 将拒绝,并舍弃此 Service Worker。
Chrome 的 DevTools 在控制台和应用标签的 Service Worker 部分中显示此错误:
安装
Service Worker 获取的第一个事件为 install
。该事件在 Worker 执行时立即触发,并且它只能被每个 Service Worker 调用一次。如果您更改您的 Service Worker 脚本,则浏览器将其视为一个不同的 Service Worker,并且它将获得自己的 install
事件。我将在后面对更新进行详细介绍。
在能够控制客户端之前,install
事件让您有机会缓存您需要的所有内容。您传递到 event.waitUntil()
的 promise 让浏览器了解安装在何时完成,以及安装是否成功。
如果您的 promise 拒绝,则表明安装失败,浏览器将丢弃 Service Worker。它将无法控制客户端。这意味着我们可以依靠 fetch
事件的缓存中存在的 cat.svg
。它是一个依赖项。
激活
在您的 Service Worker 准备控制客户端并处理 push
和 sync
等功能事件时,您将获得一个 activate
事件。但这并不意味着调用 .register()
的页面将受控制。
首次加载此演示时,即使在 Service Worker 激活很长时间后请求 dog.svg
,它也不会处理此请求,您仍会看到小狗的图像。默认值为 consistency,如果在页面加载时不使用 Service Worker,那么也不会使用它的子资源。如果您第二次加载此演示(即刷新页面),该页面将受控制。页面和图像都将执行 fetch
事件,您将看到一只猫。
clients.claim
激活 Service Worker 后,您可以通过在其中调用 clients.claim()
来控制未受控制的客户端。
下面是上述演示的变体,其在 activate
事件中调用 clients.claim()
。首先,您应该看到一只猫。我说“应该”是因为这受时间约束。如果在图像尝试加载之前,Service Worker 激活且 clients.claim()
生效,那么,您将只看到一只猫。
如果您使用 Service Worker 加载页面的方式与通过网络加载页面的方式不同,clients.claim()
会有些棘手,因为您的 Service Worker 最终会控制一些未使用它加载的客户端。
更新服务工件
简而言之:
- 如果发生以下任一情况,系统就会触发更新:
- 导航到一个作用域内的页面。
- 更新
push
和sync
等功能事件,除非在前 24 小时内已进行更新检查。 - 仅在 Service Worker 网址发生变化时调用
.register()
。不过,您应避免更改 worker 网址。
- 大多数浏览器(包括 Chrome 68 及更高版本)在检查已注册的 Service Worker 脚本的更新时,默认情况下都会忽略缓存标头。在通过
importScripts()
获取 Service Worker 内加载的资源时,它们仍会遵循缓存标头。您可以在注册 Service Worker 时,通过设置updateViaCache
选项来替换此默认行为。 - 如果 Service Worker 的字节与浏览器已有的字节不同,则考虑更新 Service Worker。(我们正在扩展此内容,以便将导入的脚本/模块也包含在内。)
- 更新的 Service Worker 与现有 Service Worker 一起启动,并获取自己的
install
事件。 - 如果新 Worker 出现不正常状态代码(例如,404)、解析失败,在执行中引发错误或在安装期间被拒,则系统将舍弃新 Worker,但当前 Worker 仍处于活动状态。
- 安装成功后,更新的工作线程将
wait
,直到现有工作线程控制零个客户端。(注意,在刷新期间客户端会重叠。) 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 不再控制任何客户端。此状态称为“waiting”,这是浏览器确保每次只运行一个 Service Worker 版本的方式。
如果您运行了更新后的演示,您应仍会看到一张猫的图片,因为 V2 Worker 尚未激活。在 DevTools 的“Application”标签中,您会看到等待的新 Service Worker:
即使在演示中您仅打开一个标签,刷新页面时也不会显示新版本。原因在于浏览器导航的工作原理。当您导航时,在收到响应标头前,当前页面不会消失,即使此响应具有一个 Content-Disposition
标头,当前页面也不会消失。由于存在这种重叠情况,在刷新时当前 Service Worker 始终会控制一个客户端。
如需获取更新,请关闭或退出使用当前 Service Worker 的所有标签页。然后,当您再次浏览演示时,您看到的应该是一匹马。
此模式与 Chrome 更新的方式相似。Chrome 的更新在后台下载,但只有在 Chrome 重启后才能生效。在此期间,您可以继续使用当前版本而不会受干扰。不过,这在开发期间却是个难题,但 DevTools 为我们提供了可简化它的方法,本文后面会进行介绍。
激活
旧 Service Worker 退出时将触发 Activate,新 Service Worker 将能够控制客户端。此时,您可以执行在仍使用旧 Worker 时无法执行的操作,如迁移数据库和清除缓存。
在上面的演示中,我维护了一个期望保存的缓存列表,并且在 activate
事件中,我删除了所有其他缓存,从而也移除了旧的 static-v1
缓存。
如果您将一个 promise 传递到 event.waitUntil()
,它将缓冲功能事件(fetch
、push
、sync
等),直到 promise 进行解析。因此,当您的 fetch
事件触发时,激活已全部完成。
跳过等待阶段
等待阶段表示您每次只能运行一个网站版本,但如果您不需要该功能,您可以通过调用 self.skipWaiting()
尽快将新 Service Worker 激活。
这会导致您的 Service Worker 将当前活动的 Worker 逐出,并在进入等待阶段时尽快激活自己(或立即激活,前提是已经处于等待阶段)。这不会让您的 Worker 跳过安装,只是跳过等待阶段。
skipWaiting()
在等待期间调用还是在之前调用并没有什么不同。一般情况下是在 install
事件中调用它:
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
// caching etc
);
});
但是,您可能想在对服务工作器发出 postMessage()
时调用它。例如,在用户互动后您想要 skipWaiting()
。
下面是一个使用 skipWaiting()
的演示。无需离开您就应能看到一头牛的图片。与 clients.claim()
一样,它是一个竞态,因此,如果新 Service Worker 在页面尝试加载图像前获取数据、安装并进行激活,那么,您将只会看到牛。
手动更新
如前所述,在执行导航和功能事件后,浏览器将自动检查更新,但是您也可以手动触发更新:
navigator.serviceWorker.register('/sw.js').then(reg => {
// sometime later…
reg.update();
});
如果您期望用户可以长时间使用您的网站而不必重新加载,您需要按一定间隔(如每小时)调用 update()
。
避免更改 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。
- 浏览页面。
这意味着每次浏览时(包括刷新)都将进行更新,无需重新加载两次或关闭标签。
跳过等待
如果您有一个工作线程在等待,您可以按 DevTools 中的“skip waiting”以立即将其提升到“active”。
Shift-reload
如果您强制重新加载页面 (shift-reload),则将完全绕过 Service Worker。页面将变得不受控制。此功能已列入规范,因此,它在其他支持 Service Worker 的浏览器中也适用。
处理更新
Service Worker 是作为可扩展网页的一部分进行设计的。我们的想法是,作为浏览器开发者,必须承认网页开发者比我们更了解网页开发。因此,我们不应提供狭隘的高级 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.
});
生命周期永不停息
如您所见,了解服务工作线程生命周期非常有用。有了这些了解,服务工作线程行为应该会显得更合理、更易懂。这些知识有助于您更自信地部署和更新服务工作线程。