前言
本文主要讲ServiceWorker的生命周期,以解决在阅读MDN官网上描述Registering your worker的示例中碰到的一点疑惑,以及为什么ServiceWorker多了一个waiting状态;请大家指正。
articles/2023-03-11-ServiceWorker生命周期(一个调皮的孩子).md at main · slshsl/articles (github.com)。
ServiceWorker三剑客
本段主要通过注册ServiceWorker来引出ServiceWorkerContainer、ServiceWorkerRegistration、ServiceWorker这3个接口。
假如通过一个register.js文件来注册ServiceWorker,代码如下(下面的代码主要是为了更好的观察serviceWorker的状态变化):
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("./sw.js").then((registration) => {
registration.addEventListener("updatefound", () => {
let curUpdateWorker = registration.installing;
console.log(`当前更新的serviceWorker目前的状态为${curUpdateWorker.state}`);
curUpdateWorker.addEventListener("statechange", (e) => {
console.log(`当前更新的serviceWorker目前的状态变更为${e.target.state}`);
if (e.target.state === "actived") {
console.log("更新完成,已接管页面.");
}
});
});
});
} else {
console.error("Service worker are not supported!");
}
在上面代码中navigator.serviceWorker是一个ServiceWorkerContainer的实例,可以简单理解为serviceWorker的容器或者工厂,以下用swC来表示它的实例。
通过swC的register方法注册一个ServiceWorker,该方法返回一个promise,该promise resolve的返回值是一个ServiceWorkerRegistration的实例,对应上面代码中的registration,以下就用swR来表示ServiceWorkerRegistration的实例。
ServiceWorker是一个继承EventTarget的类。以下用sw来表示它的实例。我们可以从swC.controller(当前页面激活的sw)或者swR.installing、swR.waiting、swR.activate来获得处于不同状态的sw。
注册过程
先简单说一下sw的几个状态,以方便讲解其注册过程,详细的状态后面会提到;sw的state属性保存了sw的状态,主要有:
installing:正在安装installed:安装完成activated:已激活
当第一次注册的过程中会触发 swR的updatefound事件,在该事件中,我们可以从swR.installing拿到正在安装的sw(该sw此时状态为installing)并注册该sw的stateChange事件;此时swR.waiting、swR.activate都为null;观察sw状态最后变为activated(先暂时不描述状态变化的详细过程),此时swR.installing、swR.waiting都为null,swR.activate为我们之拿到的sw。
概述来讲,注册的sw会随着它的状态的变化,不断的在swR的installing、waiting、activate这三个属性中擦肩而过;当sw.state为installing时,它跑到了swR.installing这里,当sw.state为installed时,它跑到了swR.waiting那里,当sw.state为activated时,停在了swR.activate;就像一个调皮的孩子玩累了回到了家。这时该sw已经控制了后续的事件。
在MDN web官网上描述Registering your worker的部分是我想写这篇文章的原因,当我看到它的示例代码时,代码如下:
const registerServiceWorker = async () => {
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
if (registration.installing) {
console.log("Service worker installing");
} else if (registration.waiting) {
console.log("Service worker installed");
} else if (registration.active) {
console.log("Service worker active");
}
} catch (error) {
console.error(`Registration failed with ${error}`);
}
}
};
// …
registerServiceWorker();
上面的代码容易让人觉得这三个属性的值不会同时存在,也许官网以此例子来讲解sw的状态变化;至于如何注册sw,在MDN web官网上描述ServiceWorkerContainer中给出的注册sw更为合适,代码如下:
if ("serviceWorker" in navigator) {
// Register a service worker hosted at the root of the
// site using the default scope.
navigator.serviceWorker
.register("/sw.js")
.then((registration) => {
console.log("Service worker registration succeeded:", registration);
// At this point, you can optionally do something
// with registration. See https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
})
.catch((error) => {
console.error(`Service worker registration failed: ${error}`);
});
// Independent of the registration, let's also display
// information about whether the current page is controlled
// by an existing service worker, and when that
// controller changes.
// First, do a one-off check if there's currently a
// service worker in control.
if (navigator.serviceWorker.controller) {
console.log(
"This page is currently controlled by:",
navigator.serviceWorker.controller
);
}
// Then, register a handler to detect when a new or
// updated service worker takes control.
navigator.serviceWorker.oncontrollerchange = () => {
console.log(
"This page is now controlled by",
navigator.serviceWorker.controller
);
};
} else {
console.log("Service workers are not supported.");
}
可能是我又较真了,人家不就是给个例子嘛;是的,所以不要在意,正好也借此深入了解。
更新过程
在第一次注册完成后,当前存在activated的sw;此时,假设我们改变了注册的sw.js文件中的内容,这时浏览器在拿到新的sw.js文件后,内部去diff是否需要更新sw。更新过程也会触发swR的updatefound事件。与第一次注册过程中不同的是,新的sw的状态变化到installed(安装完成)后,就一直等待,因为当前存在activated的sw,无法进入下面的状态。
这时swR.waiting、swR.activate上都有值,swR.waiting对应着新的sw,swR.activate对应着老的sw。
然后需要关闭该页面(此时刷新页面没有用,原因后面或则下一篇文章会提到,也可以参考下web.dev中Service workers这篇文章的这个段落,简单来讲就是浏览器从一个旧的过渡到新的sw是平滑的过度),然后再打开,新的sw才会过渡到activated状态,接管后续的事件。
当然有解决方案可以实现刷新页面就可以使新的
sw激活(通过self.skipWaiting()与swC的controllerchange事件回调实现),也有解决方案可以实现不刷新页面也可以使新的sw激活(通过self.skipWaiting()与clients.claim()实现);后续也许会单独出一章节来详细讲解。本篇文章主要讲解sw在其生命周期里状态变化的过程中,swR.installing、swR.waiting、swR.activate的三个属性值的变化情况。
以上主要说明swR.installing、swR.waiting、swR.activate会出现两个同时有值,且对应不同的sw,目前个人理解以上三个属性不会出现同时有值的逻辑。
ServiceWorker实例
sw的state属性一共有以下6个取值:
-
parsed:永远不会被sw的stateChange事件监听到 -
installing:表示该sw正在安装 -
installed:表示该sw安装完成,此状态经常被称为waiting状态The service worker in this state is considered a waiting worker. 这里需要强调一下,网上看了许多
PWA的文章,主要描述sw在安装成功后一直处于waiting状态,这里就比较模糊,需要特殊说明一下,这个waiting状态实际上是installed状态。之所以也可以称为waiting状态,是因为swR中用waiting属性会保存installed状态的sw的引用。这也是我写这篇文章的另一个原因,网上大部分的文章都提到sw会有可能一直处于waiting的状态,这是为什么呢?官网上给出的状态没有这个waiting状态,所以仔细阅读了官网,给出了上面的解释,希望能帮到同样困惑的小伙伴。 -
activating:表示该sw正在激活中 -
activated:该状态表示当前sw已经准备好接管事件(fetch等) -
redundant:表示该sw已被丢弃。发生的场景一般有三个:- 当前的
sw安装失败 - 当前存在
activated的sw,也存在waiting状态的sw(此场景比较好解释,比如第一次注册成功后,假设改变了注册的sw.js文件中的内容,触发sw第一次更新(假设没有通过self.skipWaiting()使其跳过installed状态,一直处于waiting状态),此时又接着改变sw.js文件中的内容,触发sw的第二次更新,则之前第一次更新后一直处于waiting状态的sw状态变为redundant - 当前存在
activated状态的sw,不存在waiting状态的sw(此场景也比较好解释,比如,假设改变了注册的sw.js文件中的内容,触发sw第一次更新,并且使用了self.skipWaiting()使其跳过waiting,通过刷新页面或者clients.claim()使其进入activated状态,则之前活跃的sw被丢弃)。
- 当前的
ServiceWorker生命周期
-
install:此时sw状态处于installing,该回调只发生一次。The install event is the first event a service worker gets, and it only happens once.
一般来处理预加载哪些资源缓存到
cache storage中;也可以在该回调内执行self.skipWaiting()使其跳过waiting状态。self.addEventListener("install", function (event) { self.skipWaiting(); event.waitUntil( caches .open(CACHE_NAME) .then((cache) => cache.addAll(REQUESTS_EXPECTED_CACHE)) ); }); -
activate:此时sw状态处于activating。一般来清理旧版本
sw缓存的资源,也可以在该回调内执行clients.claim()使其不刷新页面就可以接管页面(此处可能会有一些问题,暂不展开)。self.addEventListener("activate", function (event) { event.waitUntil( caches.keys().then((keys) => { return Promise.all( keys.map((key) => { if (CACHE_NAME !== key) { return caches.delete(key); } else { return caches .open(CACHE_NAME) .then((cache) => cache.addAll(REQUESTS_EXPECTED_CACHE)); } }) ); }) ); event.waitUntil(clients.claim()); }); -
fetch:此时sw状态处于activated。监听请求事件,有多种策略可以选择,这里不作过多的描述。
self.addEventListener("fetch", (event) => { event.respondWith( caches.match(event.request).then((response) => { if (response !== undefined) { return response; } else { return fetch(event.request) .then((response) => { let responseClone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, responseClone); }); return response; }) .catch(() => { return new Response("", { status: 404 }); }); } }) ); });
ServiceWorker生命周期过程中的状态变化完整代码
-
register.js:采用通知用户有新数据更新,将是否更新交给用户来决定if ("serviceWorker" in navigator) { // 数据有更新,需要通知用户,来触发skipWaiting function postSkipWaiting(curWaitingWorker) { console.log("提示用户更新"); if (window.confirm("站点数据有更新,请手动刷新!")) { curWaitingWorker.postMessage("skipWaiting"); } } navigator.serviceWorker.register("./sw.js").then((registration) => { //当前活跃的serviceWorker let curActiveWorker = navigator.serviceWorker.controller; //上次更新,已安装完成,处于installed状态,一直等待激活的serviceWorker let curWaitingWorker = registration.waiting; // 如果用户在第一次更新提示没有选择更新,而是手动刷新页面,则再次弹出提示 if ( curWaitingWorker && curActiveWorker && curWaitingWorker !== curActiveWorker // 只是逻辑洁癖,如果两个都存在,应该不相等 ) { postSkipWaiting(curWaitingWorker); } // 有serviceWorker更新发生 registration.addEventListener("updatefound", () => { // 下面三个变量不可能同时存在,curUpdateWorker一定存在 // 当前更新的serviceWorker let curUpdateWorker = registration.installing; // 当前活跃的serviceWorker curActiveWorker = registration.active; // 上次更新,已安装完成,处于installed状态,一直等待激活的serviceWorker curWaitingWorker = registration.waiting; console.log(`当前更新的serviceWorker目前的状态为${curUpdateWorker.state}`); if (curWaitingWorker && curWaitingWorker !== curUpdateWorker) { curWaitingWorker.addEventListener("statechange", (e) => { // 当前安装新的serviceWorker时,如果存在上次更新未最后激活的而一直处于waiting状态的serviceWoker // 那么这个之前处于waiting状态的serviceWoker应该被标记为多余状态redundant console.log(`上次更新未最后激活的而一直处于waiting状态的serviceWoker被标记为${e.target.state}` ); }); } curUpdateWorker.addEventListener("statechange", (e) => { // 监听状态改变 console.log(`当前更新的serviceWorker目前的状态变更为${e.target.state}`); // 当前存在活跃的serviceWorker,当前更新的serviceWorker已安装完毕处于installed,可以提示用户更新 if ( curActiveWorker && curActiveWorker !== e.target && e.target.state === "installed" //注意这里不要写成了waiting ) { postSkipWaiting(e.target); } if (e.target.state === "actived") { // 当前更新的serviceWorker变为活跃的,已接管页面与事件 console.log("当前更新的serviceWorker已完成."); } }); }); }); } else { console.error("Service worker are not supported!"); } -
sw.js:采用了无刷新接管页面// 一般来处理预加载哪些请求的资源作为缓存到cache storage中 self.addEventListener("install", function (event) { // 以下可自行添加预加载资源代码 // ....... }); // 在对应回调中一般来清理旧版本serverWorker缓存的资源 self.addEventListener("activate", function (event) { // 以下可自行添加清理旧版本资源代码 // ....... // 无刷新接管页面 event.waitUntil(clients.claim()); }); // 当前有新的serviceWork正处在handled状态,一直在等待更新,需要接受用户的通知来决定是否跳过等待 self.addEventListener("message", (event) => { if (event.data === "skipWaiting") { self.skipWaiting(); } }); // 监听请求事件 self.addEventListener("fetch", (event) => { // 以下可自行添加缓存策略相关代码 // ....... });如果采用刷新页面的方式来接管页面,先删除
sw.js里注册的activate事件中的event.waitUntil(clients.claim())这句代码;然后在register.js中添加给swC注册controllerchange事件的代码,如下:navigator.serviceWorker.addEventListener("controllerchange", () => { window.location.reload(); });以上代码主要是为了观察
sw的状态变化。
总结
大体描述了sw生命周期中的事件及其状态的变化,里面没有讲到self.skipWaiting(),clients.claim()详细的使用方式;没有讲到为什么要使用waitUntil;也没有讲到Cache Storage;后续有时间会再写篇文章单独讲解,欢迎大家留言指正。
参考文献
本文发布内容来自个人对于MDN和web.dev网站关于Service Worker的阅读及实践后的理解,文章未经授权禁止任何形式的转载。