原文链接: Service Worker 生命周期
上一篇: PWA Notification API来进行消息提醒 前后台通信
【Service Worker】生命周期那些事儿
生命周期
支持的事件
-
install :Service Worker 安装成功后被触发的事件,在事件处理函数中可以添加需要缓存的文件(详见 使用 Service Worker )
-
activate :当 Service Worker 安装完成后并进入激活状态,会触发 activate 事件。通过监听 activate 事件你可以做一些预处理,如对旧版本的更新、对无用缓存的清理等。(详见 更新 Service Worker )
-
message :Service Worker 运行于独立 context 中,无法直接访问当前页面主线程的 DOM 等信息,但是通过 postMessage API,可以实现他们之间的消息传递,这样主线程就可以接受 Service Worker 的指令操作 DOM。
Service Worker 有几个重要的功能性的的事件,这些功能性的事件支撑和实现了 Service Worker 的特性。
-
fetch (请求) :当浏览器在当前指定的 scope 下发起请求时,会触发 fetch 事件,并得到传有 response 参数的回调函数,回调中就可以做各种代理缓存的事情了。
-
push (推送) :push 事件是为推送准备的。不过首先需要了解一下 Notification API 和 PUSH API 。通过 PUSH API,当订阅了推送服务后,可以使用推送方式唤醒 Service Worker 以响应来自系统消息传递服务的消息,即使用户已经关闭了页面。
-
sync (后台同步) :sync 事件由 background sync (后台同步)发出。background sync 配合 Service Worker 推出的 API,用于为 Service Worker 提供一个可以实现注册和监听同步处理的方法。但它还不在 W3C Web API 标准中。在 Chrome 中这也只是一个实验性功能,需要访问
chrome://flags/#enable-experimental-web-platform-features
,开启该功能,然后重启生效。
第一个 Service Worker
-
install
事件是 SW 触发的第一个事件,并且仅触发一次。 -
installEvent.waitUntil()
接收一个 Promise 参数,用它来表示 SW 安装的成功与否。 -
SW 在安装成功并激活之前,不会响应
fetch
或push
等事件。 -
默认情况下,页面的请求(fetch)不会通过 SW,除非它本身是通过 SW 获取的,也就是说,在安装 SW 之后,需要刷新页面才能有效果。
-
clients.claim()
可以改变这种默认行为。
作用域与控制
SW 的默认作用域为基于当前文件 URL 的 ./
。意思就是如果你在 //example.com/foo/bar.js
里注册了一个 SW,那么它默认的作用域为 //example.com/foo/
。
我们把页面,workers,shared workers 叫做 clients
。SW 只能对作用域内的 clients
有效。一旦一个 client
被“控制”了,那么它的请求都会经过这个 SW。我们可以通过查看 navigator.serviceWorker.controller
是否为 null 来查看一个 client
是否被 SW 控制。
下载-解析-执行
当你调用 .register()
的时候,第一个 SW 被下载下来,这过程中如果下载,解析或者在初始化中有错误的话,那么 register
的Promise 会返回 reject,然后 SW 会被销毁。
安装(install)
SW 首先会触发 install
,每个 SW 只会被触发一次,当你修改你的 SW 后,浏览器会认为这是一个新的 SW,从而会再触发这个新 SW 的 install
事件,在后面会详细说到。
install
是在 SW 控制 clients
之前处理缓存很好的时机。在 event.waitUntil()
传入的 Promise 会让浏览器知道 SW 什么时候安装成功以及是否成功。
当 Promise reject 的时候,代表着安装失败,浏览器将这个 SW 废弃掉,不会控制任何 clients。
激活(Activate)
安装成功后并激活(activate)成功后,SW 就可以处理“功能性的事件“了,比如 push
, sync
。 但这并不代表调用 .register()
的页面会立即生效。
第一次你请求 这个demo 的时候,虽然在 SW 被激活后很久才请求了 dog.svg
(因为这里等待了三秒),但 SW 也并没有处理这个请求,结果你看见了一只狗。当你第二次请求的时候,也就是刷新页面,这时请求被处理了,当前页面和图片都经过了 SW 的 fetch
事件,所以你看见了一只猫。
clients.claim
你可以在 activate
事件中通过调用 clients.claim()
来让没被控制的 clients 受控。
比如 这个demo ,可能第一次你就会看见一只猫,这里我说“可能”,是因为这时时间敏感的,仅当 SW 激活并且 clients.claim()
被调用成功在图片请求之前的时候才可以。
所以,可想而知,当你用 SW 加载与正常请求不同资源的时候(比如上面的例子),那用 clients.claim()
可能会遇到一些问题,这时有些资源可能不会通过你的SW。
我见过很多人在代码中把
clients.claim()
当做了必选项,但我自己很少这样做,因为仅仅是第一次加载不会通过 SW,而且页面还是都会正常运行的。
更新 Service Worker
简单来说:
-
触发更新的几种情况:
-
第一次导航到作用域范围内页面的时候
-
当在24小时内没有进行更新检测并且触发功能性时间如
push
或sync
的时候 -
SW 的 URL 发生变化并调用
.register()
时
-
-
当 SW 代码发生变化,SW 会做更新(还将包括引入的脚本)
-
更新后的 SW 会和原始的 SW 共同存在,并运行它的
install
-
如果新的 SW 不是成功状态,比如 404,解析失败,执行中报错或者在 install 过程中被 reject,它将会被废弃,之前版本的 SW 还是激活状态不变。
-
一旦新 SW 安装成功,它会进入
wait
状态直到原始 SW 不控制任何 clients。 -
self.skipWaiting()
可以阻止等待,让新 SW 安装成功后立即激活。
install
注意这里我们将缓存从 static-v1 换到了 static-v2,这代表了我用了一个新的缓存空间覆盖了之前 SW 正在使用的缓存。
这里新建了一块缓存的做法类似于原生 app 中将每块资源打包到一块指定的执行空间的做法,有时候结合实际情况,你也可以不这么做。
Waiting
一旦新 SW 安装成功,它会进入 wait
状态直到原始 SW 不控制任何 clients。这个状态是 waiting
,这也是浏览器确保在同一时间只有一个版本的 SW 运行的机制。
如果你再次打开 这个 demo ,你还是会看到一只喵,因为新的 SW 还是没有被激活,在开发者工具里你依然看到它是 waiting 状态。
尽管这个例子中你仅打开了一个 tab,但刷新页面并没有用,这是由于浏览器本身的机制,当你刷新的时候,当前页面不会离开,直到收到了一个响应头,而且即使这样,如果响应中包含 Content-Disposition
的话,当前页面还是不会离开。由于这个时间上的重叠,在刷新的时候当前的 SW 总是控制了一个 client。
为了让 SW 更新,你需要把所有用原始 SW 的页面 tab 关闭或者跳转走,这时你再访问 这个 demo ,你就会看到了一匹野马。
这种机制类似于 Chrome 本身的更新机制,Chrome 在后台更新,只有当你重启浏览器的时候才会生效,在这期间你不会被打扰,可以继续使用当前版本。然而,这样可能会使我们开发者比较痛苦,好在开发者工具帮我们解决了这个事情,后面会说到。
Activate
Activate 在旧的 SW 离开时会被触发,这时新的 SW 可以控制 clients。这时候你可以做一些在老 SW 运行时不能做的事情,比如清理缓存。
在上面的例子中,之前保留的缓存,在 activate
时间执行的时候被清理掉。
这里最好不要更新以前的版本,而是直接分配新的缓存空间。
如果你在 event.waitUntil()
中传入了一个 Promise,SW 将会缓存住功能性事件( fetch
, push
, sync
等等),直到 Promise 返回 resolve 的时候再触发,也就是说,当你的 fetch
事件被触发的时候,SW 已经被完全激活了。
cache storage API 和 localStorage,IndexedDB 一样是“同域”的。如果你在一个父域下运行多个网站,比如
yourname.github.io/myapp
,这就要小心你不要把别的网站的缓存删掉了。避免这个问题,你可以将 cache 的 key 设的具有唯一性,比如 myapp-static-v1 并且约束不要碰不以 myapp- 开头的缓存。
skipWaiting
waiting 意在让你的网站同一时间只有一个 SW 在运行,但如果你不想要这样的话,你可以通过调用 self.skipWaiting()
来让新 SW 立即激活。
这么做会让你的新 SW 踢掉旧的,然后当它变为 waiting 状态时立即激活,注意这里不会跳过 installing,只会跳过 waiting。
在 waiting 之前或者之后调用 skipWaiting()
都可以,一般情况我们在 install
事件中调用:
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
// caching etc
);
});
这个例子 中,你可能直接可以看到一只奶牛,和 clients.claim()
一样,这是一场赛跑,仅当你的新 SW 安装,激活等早于你请求图片时,奶牛才会出现。
skipWaiting()
意味着新 SW 控制了之前用旧 SW 获取的页面,也就是说你的页面有一部分资源是通过旧 SW 获取,剩下一部分是通过新 SW 获取的,如果这样做会给你带来麻烦,那就不要用skipWaiting()
,这点我们应该根据具体情况评估。
手动更新
像我之前说的,当页面刷新或者执行功能性事件时,浏览器会自动检查更新,其实我们也可以手动的来触发更新:
navigator.serviceWorker.register('/sw.js').then(reg => {
// sometime later…
reg.update();
});
如果你希望你的用户访问页面很长时间而且不用刷新,那么你可以每个一段时间调用一次 update()
。
避免改变 SW 的 URL
如果你看过我的文章 缓存最佳实践 ,你可能会考虑给每个 SW 不同的 URL。 千万不要这么做! 在 SW 中这么做是“最差实践”,要在原地址上修改 SW。
举个例子来说明为什么:
1. index.html
注册了 sw-v1.js
作为SW。
2. sw-v1.js
对 index.html
做了缓存,也就是缓存优先(offline-first)。
3.你更新了 index.html
重新注册了在新地址的 SW sw-v2.js
.
如果你像上面那么做,用户永远也拿不到 sw-v2.js
,因为 index.html
在 sw-v1.js
缓存中,这样的话,如果你想更新为 sw-v2.js
,还需要更改原来的 sw-v1.js
。
在上面的 demo 里,我给每个 SW 用了不同的 URL,这只是为了做演示,不要在生产环境中这么做。
让开发更简单
SW 的生命周期是为了用户构建的,但这样难免让我们开发带来一些烦恼,幸亏与一些工具来帮助我们。
Update on reload
这样把生命周期变得对开发友好了,每次跳转将会:
1.重新获取 SW
2.尽管字节一致,也会重新安装,也就是说 install
事件被执行并且更新缓存。
3.跳过 waiting,激活新的 SW。
4.导航到这个页面。
这就是说你每次操作都会更新而不用刷新页面或者关闭 tab。
// 浏览器没有让当前已经注册的所有service worker一直保持运行状态
// sw的生命周期直接与他所处理的事件的执行联系在一起
// 一旦处理完来自页面的事件,他就终止了,如果稍后发生了另一个事件,将会再次启动
// 如下方法
// 当fetch去异步请求数据时,事件监听器已经停止执行,一旦事件结束,在响应返回之前
// sw就会被浏览器终止,导致无法显示通知
self.addEventListener("push", function(event) {
fetch(event.url).then(response =>
self.registration.showNotification(response.text())
);
});
// sw的生命周期和他所处理的事件执行直接相关
// 使用waitUntil拓展事件的执行
// 告知push事件等待我们传入的一个promise完成或者失败,才能认为事件已经完成
// 意味着sw的生命周期也得到了延长
self.addEventListener("push", function(event) {
event.waitUntil(
fetch(event.url).then(response =>
self.registration.showNotification(response.text())
)
);
});