Service Worker快速入门

424 阅读9分钟

一.作用

主要作用如下:

1.离线缓存。Server worker安装启动后对当前配置的文件进行缓存,界面刷新或者重进进入界面时只要server Worker未被注销则可从缓冲中读取命中的目标文件,以达到类applacationCache

类似的功能。且不影响当前应用。

2.消息推送。推送消息到被server worker接收管理的界面。

3.后台数据同步,后台下载更新文件不影响当前界面

4.请求劫持。

5.地理围栏,时间日期的响应,相同域名之间的切换,处理运算成本比较高的数据,预取资源比如下一组列表数据等等。

二.概念

Service Worker 首先是一个运行在后台的 Worker 线程,然后它会长期运行,充当一个服务,很适合那些不需要网页或用户互动的功能。它的最常见用途就是拦截和处理网络请求。为了节省内存,Service worker 在不使用的时候是休眠的。它也不会保存数据,所以重新启动的时候,为了拿到数据,最好把数据放在 IndexedDb 里面。

作为一个比较新的技术,大家可以把Service Worker理解为一个介于客户端和服务器之间的一个代理服务器。在Service Worker中我们可以做很多事情,比如拦截客户端的请求、向客户端发送消息、向服务器发起请求等等,其中最重要的作用之一就是离线资源缓存。Service worker
是一个注册在指定源和路径下的事件驱动[worker](https://developer.mozilla.org/zh-CN/docs/Web/API/Worker)。它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。Service worker运行在worker上下文,因此它不能访问
DOM。相对于驱动应用的主JavaScript线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步API(如[XHR](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest)和[localStorage](https://developer.mozilla.org/zh-CN/docs/Web/Guide/API/DOM/Storage))不能在service worker中使用。出于安全考量,Service workers只能由HTTPS承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险。在Firefox浏览器的
[用户隐私模式](https://support.mozilla.org/zh-CN/kb/%E9%9A%90%E7%A7%81%E6%B5%8F%E8%A7%88)

,Service Worker不可用。特点总结如下:

  • · ServiceWorker工作在worker context中,是没有访问DOM的权限的,所以我们无法在Service Worker中获取DOM节点,也无法在其中操作DOM元素;
  • ·我们可以通过postMessage接口把数据传递给其他JS文件;
  • · Service Worker中运行的代码不会被阻塞,也不会阻塞其他页面的JS文件中的代码;

三.注册

由于server worker出于安全考虑限定其只能在指定源下使用且只能在localhost和https下使用,所以注册文件一般放于和index.html同级目录下(生产环境非开发环境)。
  1. 注册

    navigator.serviceWorker.register('sw.js'.then(() => { console.info('注册成功') }).catch((err) => { console.error('注册失败') })

新建如上代码在index.html引入。

注意:
首次访问server worker控制的网站或者页面时,其会自动下载、安装、激活相关的文件和代码、为了不影响界面首次加载运行的cpu和网络占用建议推迟执行register方法,例如在load回调函数中执行。一个项目可以注册多个serviceWorker但是需要注意必须指定不同的sw.js文件和不同的scope限制。
  • scope
`规定了响应server worker的域名限制,不设置即全局,/app/即app地址路径下的所有页面如/app/index.html;/app/main/index.html。`
  • sw.js
`就是浏览器需要执行的server worker的脚本,sw的路径地址设置是相对于origin而不是当前的js文件目录(react和vue等使用构建工具如webpack的需要特别注意)。可以在这文件中监听serverworker消息,拦截客户端请求,给其控制的界面发送消息等`

2.监测生命周期

可在sw.js文件中检测当前server worker的生命周期。
  • 安装:
self.addEventListener('install', event => {
    event.waitUntil(() => console.info('安装完成'))
})

event.waitUntil() 方法为事件完成后指定回调函数。

  • 激活
安装完成后,server worker就会被激活。可以在激活完成的回调函数中缓存文件、发送消息、编辑缓存等。

3.更新

在默认情况下Service Worker必定每24小时被下载一次,如果下载的文件是最新文件,那么就会被重新注册和安装,单不会被激活,当不再有页面使用旧的Service Worker的时候才会被激活。如果这是首次启用service worker,页面会首先尝试安装,安装成功后它会被激活。如果现有service worker已启用,新版本会在后台安装,但不会被激活,这个时序称为worker in waiting。直到所有已加载的页面不再使用旧的service worker才会激活新的service worker。只要页面不再依赖旧的service worker,新的service worker会被激活(成为active worker)。你可以监听InstallEvent,事件触发时的标准行为是准备service worker用于使用,例如使用内建的storage API来创建缓存,并且放置应用离线时所需资源。还有一个activate事件,触发时可以清理旧缓存和旧的service worker关联的东西。

四.通信

通信 页面发送消息到ServiceWorker

从页面给已经注册激活的serviceWorker传递消息,要确定当前serviceWorker已经激活安装完成(可以判断navigator.serviceWorker.controller
对象是否被构建)。具体如下代码所示:
// app.js
navigator.serviceWorker.controller
&& navigator.serviceWorker.controller.postMessage("this message is from page");

在sw.js文件中可以使用如下方式接收:

// sw.js
self.addEventListener('message', function (event) {
    console.log(event.data); // this message is from page
});

通信 ServiceWorker发送消息到页面

了解serviceWorker这个功能之前希望你能简单了解下javascript的Worker功能。以便你更好的了解这个功能。
  1. 从serviceWorker到页面的信息传递最简单的方式是通过监听页面发送到serverWorker的消息然后获取windowClients,然后在windowClients上调用window的postMessage发送消息到页面,但是局限性也很大,必须要接收到页面发送到serviceWorker的消息后才能发送消息。例:

    // sw.js 接收到消息后再发送信息 this.addEventListener('message', function (event) { event.source.postMessage('this message is from sw.js, to page'); });

    // app.js 通过监听message消息接收信息 navigator.serviceWorker.addEventListener('message', function (e) { console.log(e.data); // this message is from sw.js, to page });

`直接通过client对象的postMessage方法发送消息。在sw.js文件中是可以通过serverWorker本身访问到所有的受其控制或者说注册了该serviceWoker的页面的clients对象(即windows对象),然后直接发送消息。例:`
// sw.js
this.clients.matchAll().then(client => {
    client[0].postMessage('this message is from sw.js, to page');
})
以上就是使用serviceWorker的通信部分,当然目前看来所有的通信过程都与
postMessage方法类似。那么纵向对比也许浏览器的广播系统也可能是一个更好的方式去实现通信,比如可以在多个serviceWorker之间实现通信和消息推送。有兴趣的话可以看下**MessageChannel**或者**BroadcastChannel**的API。

五.资源缓存

这个功能可以说是浏览器实现或者说兼容该api的重要原因即静态资源缓存。一般情况下打开一个网页浏览器会自动download所有的资源文件包括但不限于js/css/img/html等。受限于网络状态和网络能力能即使本地客户端很好有时也会出现很差的用户体验。serviceWorker可以实现即使在offline的情况下也能正常展示当前页面。sw安装完成之后可以缓存指定静态资源,当页面再次刷新或者重新打开页面sw未被重新安装或者删除时,页面首先会在其cacheSotre中读取其静态资源减少资源请求。

缓存指定资源

// sw.js 缓存指定静态资源
this.addEventListener('install', function (event) {
    console.log('install');
    event.waitUntil(caches.open('sw_demo').then(function (cache) {
        return cache.addAll(['/style.css','/panda.jpg','./main.js'])
    }));
});

CacheStroage在浏览器中的接口名称是caches,我们可以通过caches.open方法新建或者打开一个已存在的缓存。Cache.addAll方法的作用是请求指定链接页面的资源把他们储存到之前打开的缓存中(具体操作详解请查看CacheStroage API)。由于IO操作是异步操作锦衣将方法放置于event.waitUntil方法,其能保证资源被缓存完成前sw本身不被重新安装或者其余操作导致终止或者退出。缓存完成后可以在Chrome开发工具中的Application的Cache Strogae中可以看到我们缓存的资源。

缓存所有网络资源

缓存所有网络资源是通过监听fetch事件,当用户发起网络请求时会被触发。但是需要注意这是无差别缓存。而且要注意scope的origin路径限制。在回掉函数中我们使用事件对象提供的respondWith方法,它劫持http请求,并把一个Promise作为响应结果返回给用户。然后我们使用用户的请求对Cache Stroage进行匹配,如果匹配成功,则返回存储在缓存中的资源;如果匹配失败,则向服务器请求资源返回给用户,并使用cache.put方法把这些新的资源存储在缓存中。因为请求和响应流只能被读取一次,所以我们要使用clone方法复制一份存储到缓存中,而原版则会被返回给用户。(post请求也可以被缓存,但是缓存的位置indexedDB是一个更好的选择)。
this.addEventListener('fetch', function (event) {
    console.log(event.request.url);
    event.respondWith(caches.match(event.request).then(res => {
        return res || fetch(event.request).then(responese => {
                const responeseClone = responese.clone();
                caches.open('sw_demo').then(cache => {
                    cache.put(event.request, responeseClone);
                })
                return responese;
        }).catch(err => {
            console.log(err);
        });
    }))
});
_注意:_
  1. 当用户第一次访问页面的时候,资源的请求是早于Service Worker的安装的,所以静态资源是无法缓存的;只有当Service Worker安装完毕,用户第二次访问页面的时候,这些资源才会被缓存起来;
  2. Cache Stroage只能缓存静态资源,所以它只能缓存用户的GET请求;
  3. Cache Stroage中的缓存不会过期,但是浏览器对它的大小是有限制的,所以需要我们定期进行清理;
  • 更新缓存CacheStroage
更新缓存只需要简单的两步,第一步重命名缓存空间,第二部删除不用的缓存。注意删除操作放置于event.waitUntil中确保在新的ServiceWorker被安装前完成操作代码如下:
// sw.js 更换缓存命名空间
this.addEventListener('install', function (event) {
    console.log('install');
    event.waitUntil(caches.open('sw_demo_v2').then(function (cache) { 
        // 更换缓存命名空间
        return cache.addAll(['/style.css','/panda.jpg','./main.js'])
    }))
});

const cacheNames = ['sw_demo_v2'];
// sw.js 校验过期的缓存进行清除
this.addEventListener('activate', function (event) {
    event.waitUntil(caches.keys().then(keys => {
        return Promise.all[keys.map(key => {
            if (!cacheNames.includes(key)) {
                console.log(key);
                return caches.delete(key); // 删除不在白名单中的 Cache Stroage
            }
        })]
   }))
});