Service Worker是什么
从功能上来说,Service Worker
是一种提供离线缓存控制功能的一种Worker
,同时,也具有消息推送和后台同步的功能,可以通过Service Worker来缓存网页的资源,然后拦截Fetch请求来执行相应的缓存处理操作。因为是一种Worker,所以Service Worker也具有Worker的一些基本特性,例如:
- 独立于主线程运行
- 不能访问
window
对象,但是拥有自身的一个执行上下文,例如Service Worker
的ServiceWorkerGlobalScope
- 具有消息api来和页面进行消息交互
Service Worker是一种共享型Worker,它不同于专用型Worker只能在创建它的页面中使用,默认配置下,service Worker可以被当前注册脚本的域名下所有页面公用。
也就是说,只要在一个根域名注册一个ServiceWorker,那么所有这个域名下的页面都会收到影响。
Service Worker使用
要使用ServiceWorker,首先,需要通过serviceWorkerContainer.register()
来进行注册,例如:
ServiceWorkerContainer.register("/test/service.js", {scope:"./"})
.then(
function(ServiceWorkerRegistration) {
// do something
}
);
假设当前域名为www.baidu.com/serviceWork
,那么上面的方法就在www.baidu.com/serviceWork/test
下面注册了一个ServiceWorker,假如把scope改成./hahaha,那么作用域就变成了www.baidu.com/serviceWork/test/hahaha
,只要处于这个域名之下的所有页面,都受到这个ServiceWorker的控制。但是,假如将scope改成"../",页面就会报出一个错误,因为这时候不允许设置比service.js所在位置层级更高的路径,除非添加一个Service-Worker-Allowed
header。
注册完成之后,受控界面会去安装serviceWorker,安装完成之后会处于等待状态,接下来有可能会进入激活状态,激活状态之后,页面还不一定是受控的,不过有相应的api可以控制这一系列流程。这些流程就是ServiceWorker最最复杂的生命周期了。
ServiceWorker生命周期
serviceWorker的生命周期有点复杂,情况很多,但是基本上符合一个理念,那就是渐进式。
install
执行完注册之后,浏览器会去下载,假如脚本没有错误的话,就会进行安装,安装会在Worker内触发install事件,安装分为两个状态:installing和installed,这里可以执行一些操作。
self.addEventListener("install",installEvent=>{
self.skipWaiting();//跳过waiting
installEvent.waitUntil(
caches.open(CACHE_NAME).then(cache=>{
return cache.add("http://upload.jianshu.io/users/upload_avatars/9545112/f186bd4a-1da4-4912-b926-a744bc128d06.png?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240");
})
)
});
这里会有一个installEvent,通过waitUtil方法可以阻塞安装状态,这个方法可以传入一个promise,只有这个promise正确resolve之后,才会完成安装,同时,还有一个skipwaiting方法,这个方法可以跳过installed和activating状态之间的waiting状态。
首次打开页面会安装work,然后接下来再检测到新的work也会再次执行安装。
触发更新的几种情况:
- 第一次导航到作用域范围内页面的时候
- 当在24小时内没有进行更新检测并且触发功能性时间如push或sync的时候
- SW 的 URL 发生变化并调用.register()时
- 手动执行reg.update()
- 重载页面(有些情况下不会更新,原因不明)
更新会比对旧版本和新版本的字节,假如字节不一致,则更新。在更新install完成之后,会进入waiting状态,直到旧版本不控制任何client(受控的页面)再进入activating状态。
activate
activate的状态比测试妹子的思考逻辑还难猜测。但是这个遵循一个原则,只有所有的页面都不受老的serviceWorker控制的时候,才会开始激活。意思就是所有worker作用的页面都关了,相当于重启更新,再次启动的时候,才会进入激活状态。激活也会触发一个activate状态.
self.addEventListener("activate", ExtendableEvent => {
ExtendableEvent.waitUntil(self.clients.claim());
}
activate也和install一样,也有waitUtil方法,效果也是一样的。激活完成之后其实还不一定有效果,因为页面可能还处于不受控的状态,或者有另外一个页面,没有刷新的,但是这个时候新开一个页面更新了work,这个时候就需要调用clients.claim操作来让所有打开的页面受控。但是...work的执行是异步的,也就是说,页面在执行这个方法之前是不受控,或者是不是受最新的work控制的,在利用work来缓存的时候,就会出现问题,导致版本不一致,所以,个人感觉serviceWorker比较适合做一个辅助者,没有也行,有也行,可以通过设置一个button之类的来提醒用户,发现新版本,是否需要刷新之类的操作。
下面这张图很好的概括了生命周期
Fetch
激活完成之后,work内可以拦截到fetch事件,通过这个,就可以执行缓存操作。fetch事件回调会返回一个fetchEvent对象,通过fetchEvent.respondWith()可以自定义返回的response。
其他事件
....
杂谈
ServiceWorker的大多数功能都要在worker线程中使用,在页面中,通过注册返回的ServiceWorkerRegistration可以获取很多有用的信息,例如正在安装的worker,等待中的worker,激活的worker,worker作用域等。同时,可以监听状态的变换。例如,
ServiceWorkerRegistration.addEventListener("updatefound",()=>{
const netWorker=ServiceWorkerRegistration.installing;
console.log(`state:${netWorker.state}`);
netWorker.addEventListener("statechange", () => {
// newWorker 状态发生变化
console.log(`state变换:${netWorker.state}`)
if(netWorker.state=="installed"){
console.log("检测到新版本!");
}
});
})
这里,先获取正在安装中的worker,然后监听状态变换,当切换成installed状态时,假如之前已经存在serviceWorker,就可以判定新安装的worker是更新进去的,这个时候就有可能部分资源是通过老的worker缓存请求的,部分资源在worker受控之后是通过worker缓存请求的,这样就存在不一致的问题,所以这时候就可以提醒用户有新版本更新,从而刷新更新。假如之前没有worker,那么就说明这个worker是第一次注册安装,因为是第一次,所以没有所谓的缓存资源版本不一致的问题,没必要提醒用户更新。
针对serviceWorker机制所导致的各种资源同步问题,将其想像成渐进式的就轻松多了,将worker当作一个辅助者,把缓存想象成可有可无的。进一步来看可以不使用skipWaiting,只有在用户重启的时候才可以更新,这时候所有的资源就不存在不同步的问题了,所有的请求都会在重启更新之后直接进入worker。