本文主要讲 serviceWorker 生命周期和挂载、卸载等问题,适合对 serviceWorker 的作用有所了解但是具体细节不是特别清楚的读者
以下所有分析基于 Chrome V63
serviceWorker的挂载
先来一段代码感受serviceWorker注册:
1 | if ('serviceWorker' in navigator) { |
通过上述代码,我们定义在/sw.js
里的内容就会生效(对于当前页面之前没有 serviceWorker 的情况而言,我们注册的 serviceWorker 肯定会生效,如果当前页面已经有了我们之前注册的 serviceWorker,这个时候涉及到 serviceWorker的更新机制,下文详述)
如果我们在sw.js
没有变化的情况下刷新这个页面,每次还是会有注册成功的回调以及相应的log输出,但是这个时候浏览器发现我们的 serviceWorker 并没有发生变化,并不会重置一遍 serviceWorker
serviceWorker更新
我们如果想更新一个 serviceWorker,根据我们的一般web开发策略,可能会想到以下几种策略:
- 仅变更文件名(比如把
sw.js
变成sw-v2.js
或者加一个hash) - 仅变更文件内容(仅仅更新
sw.js
的内容,文件名不变) - 同时变更:同时执行以上两条
在这里,我可以很负责的告诉你,变更serviceWorker文件名绝对不是一个好的实践,浏览器判断 serviceWorker 是否相同基本和文件名没有关系,甚至有可能还会造成浏览器抛出404异常(因为找不到原来的文件名对应的文件了)。
所以我们只需要变更内容即可,实际上,我们每次打开或者刷新该页面,浏览器都会重新请求一遍 serviceWorker 的定义文件,如果发现文件内容和之前的不同了,这个时候:
(下文中,我们使用“有关 tab”来表示受 serviceWorker 控制的页面,刷新均指普通刷新(F5/CommandR)并不指Hard Reload)
这个新的 serviceWorker 就会进入到一个 “waiting to activate” 的状态,并且只要我们不关闭这个网站的所有tab(更准确地说,是这个 serviceWorker 控制的所有页面),新的 serviceWorker 始终不会进入替换原有的进入到 running 状态(就算我们只打开了一个有关 tab,直接刷新也不会让新的替换旧的)。
如果我们多次更新了 serviceWorker 并且没有关闭当前的 tab 页面,那么新的 serviceWorker 就会挤掉原先处于第二顺位(waiting to activate)的serviceWorker,变成
waiting to activate
状态
也就是说,我们只有关闭当前旧的 serviceWorker 控制的所有页面 的所有tab,之后浏览器才会把旧的 serviveWorker 移除掉,换成新的,再打开相应的页面就会使用新的了。
当然,也有一个特殊情况:如果我们在新的 serviceWorker 使用了self.skipWaiting();
,像这样:
1 | self.addEventListener('install', function(event) { |
这个时候,要分为以下两种情况:
- 如果当前我们只打开了一个有关 tab,这个时候,我们直接刷新,发现新的已经替换掉旧的了。
- 如果我们当前打开了若干有关 tab,这个时候,无论我们刷新多少次,新的也不会替换掉旧的,只有我们一个一个关掉tab(或者跳转走)只剩下最后一个了,这个时候刷新,会让新的替换旧的(也就是上一种情况)
Chrome 的这种机制,防止了同一个页面先后被新旧两个不同的 serviceWorker 接管的情况出现。
手动更新
虽然说,在页面每次进入的时候浏览器都会检查一遍 serviceWorker 是否更新,但如果我们想要手动更新 serviceWorker 也没有问题:
1 | navigator.serviceWorker.register("/sw.js").then(reg => { |
这个时候如果 serviceWorker 变化了,那么会重新触发 install 执行一遍 install 的回调函数,如果没有变,就不会触发这个生命周期。
install 生命周期钩子
我们一般会在 sw.js 中,添加install
的回调,一般在回调中,我们会进行缓存处理操作,像这样:
1 | self.addEventListener('install', function(event) { |
如果我们新打开一个页面,如果之前有 serviceWorker,那么会触发install
,如果之前没有, 那么在 serviceWorker 装载后会触发 install
。
如果我们刷新页面,serviceWorker 和之前没有变化或者 serviceWorker 已经处在 waiting to activate
,不会触发install
,如果有变化,会触发install
,但不会接管页面(上文中提到)。
activate 生命周期钩子
activate 在什么时候被触发呢?
如果当前页面没有 serviceworker ,那么会在 install 之后触发。
如果当前页面有 serviceWorker,并且有 serviceWorker更新,新的 serviceWorker 只会触发 install ,不会触发 activate
换句话说,当前变成 active 的 serviceWorker 才会被触发这个生命周期钩子
serviceWorker 代理请求
serviceWorker 代理请求相对来说比较好理解,以下是一个很简单的例子:
1 | self.addEventListener('install', function(event) { |
有两点要注意的:
我们如果这样代理了,哪怕没有 cache 命中,实际上也会在控制台写from serviceWorker,而那些真正由serviceWorker发出的请求也会显示,有一个齿轮图标,如下图:
第二点就是我们如果在 fetch 的 listener 里面 do nothing, 也不会导致这个请求直接假死掉的。
另外,通过上面的代码我们发现,实际上由于现在我们习惯给我们的文件资源加上 hash,所以我们基本上不可能手动输入需要缓存的文件列表,现在大多数情况下,我们都是借助 webpack 插件,完成这部分工作。
serviceWorker 和 页面之间的通信
serviceWorker向页面发消息:
1 | sw.js: |
当然,这里面是有坑的:
- 主界面的事件监听需要等serviceWorker注册完毕后,所以一般
navigator.serviceWorker.register
的回调到来之后再进行注册(或者延迟足够的时间)。 - 如果在主界面事件监听还没有注册成功的时候 serviceWorker 发送消息,自然是收不到的。如果我们把 serviceWorker 直接写在 install 的回调中,也是不能被正常收到的。
从页面向 serviceWorker 发送消息:
1 | 主页面: |
同样的,这也要求主界面的代码需要等到serviceWorker注册完毕后触发,另外还有一点值得注意, serviceWorker 的事件绑定代码要求主界面的serviceWorker已经注册完毕后才可以。
也就是说,如果当前页面没有该serviceWorker 第一次注册是不会收到主界面接收到的消息的。
记住,只有当前已经在 active 的 serviceWorker, 才能和主页面收发消息等。
以上就是和 serviceWorker 有关的一些内容,在下一篇文章中,我会对PWA 添加至主屏幕等功能进行总结