web离线应用解决方案 ServiceWorker

1,530 阅读9分钟
  1. ServiceWorker是什么

    Service worker是一个注册在指定源和路径下的事件驱动worker,它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是网络不可用)下的表现。
  2. worker是什么?

    使用构造函数(例如,Worker())创建一个 worker 对象, 构造函数接受一个 JavaScript文件URL — 这个文件包含了将在 worker 线程中运行的代码

     通过使用Web Workers,Web应用程序可以在独立于主线程的后台线程中,运行一个脚本操作。
     这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是UI线程)不会因此被阻塞/放慢。
    

    特点: 

     (1)同源限制
    
         分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。(同一协议和同一域名下面)
    
     (2)DOM 限制
    
         Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,
         也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。
    
     (3)通信联系
    
         Worker 线程和主线程不在同一个上下文环境(这个环境对应的对象为DedicatedWorkerGlobalScope,Dedicated workers 由单个脚本使用; 
         Shared workers使用SharedWorkerGlobalScope。),它们不能直接通信,必须通过消息完成。(举个例子)
         worker 上下文中大部分 window 对象的方法和属性是可以使用的,包括 WebSockets,
         以及诸如 IndexedDB 和 FireFox OS 中独有的 Data Store API 这一类数据存储机制。
     (4)脚本限制
    
         Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
    
     (5)文件限制
    
         Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。   
     注意:改网络请求的能力暴露给中间人攻击会非常危险。所以出于安全考量,Service workers只支持HTTPS
    

    tip: WorkerGlobalScope更多信息请参见: Functions and classes available to workers

  3. Worker与shareWorker

    WebWork分有俩个类型, Worker与shareWorker

    专有类型:Dedicated Worker 共享类型:Shared Worker 他们之间有什么区别?

    • 最明显的区别,shared Worker 可以共享,worker 不能.体现在如下方便
      • 来自同源的脚本可以都用访问同一个 shareWorker 对象用作通讯,而 worker 对象只能被他创建的脚本访问,通讯
      • 作用域不同:
        • 所有shareWorker对象共享同一个作用域
        • worker 对象的作用域与创建他的主进程相关联.
    • 共享类型必须通过打开的活动端口实现通讯.(在专用worker中这一部分是隐式进行的)
        //显示打开
        myWorker.port.addEventListener("message",
            function(event) {
                alert(event.data);
            }, false
        );
        myWorker.port.start();
    
        //隐式打开:直接使用 onmessage 
        myWorker.port.onmessage=function(event){
            alert(event.data);
        }
        
    
  4. ShareWorker与ServiceWorker

    与开头所述,serviceWorker也是一种 worker.所以他继承了 Worker 的所有属性和特点

    •  serviceWorker与shareWorker一样
      • 运行在全局workers上下文里(拥有自己的线程)
      • 没有绑定在特有的页面上
    • 与 shareWorker 不同的是:
      • seriveWorker不依赖任何页面(当 shareWorker 被引用的页面都关闭之后,shareWorker 也就关闭了)
      • seriveWorker 有更长的生命周期,
      • seriveWorker 有自己的模块更新机制

    具体不同见:www.w3.org/TR/service-…

  5. 关于如何杀死 worker 进程,错误监听等具体可以参考 developer.mozilla.org/zh-CN/docs/…

  6. ServiceWorker

    ServiceWorker可以不依赖任何页面,一旦注册可以一直存在(除非手动销毁)并作用于所在域内用户可访问的URL.

    sw 可以使你的应用先访问本地缓存资源,所以在离线状态时,在没有通过网络接收到更多的数据前,仍可以提供基本的功能(一般称之为 Offline First)。

    特点:

    1. ServiceWorker也是 worker,故会有 Dom 限制,无法直接操作 Dom;但可以通过 postMessage发送消息控制相关页面操作Dom
    2. ServiceWorker缓存机制依赖Cache API实现
    3. ServiceWorker 都是基于 promise 接口编程
    4. ServiceWorker 依赖 html5 的fetch API做网络编程
    5. ServiceWorker必须运行在 https下(保证安全性)

    生命周期

    生命周期

    事件

    事件

    如何实现离线?

    ServiceWorker 是浏览器和网络之间的虚拟代理,在其worker上下文中,可以通过 fetch 事件监听所有sw作用域下的资源请求动作,通过 event.respondWith() 方法来劫持HTTP响应,自定义返回.

    具体步骤如下:

    注册

        if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('sw.js', { scope: './' }).then(function(reg) {
            // registration worked
            console.log('Registration succeeded. Scope is ' + reg.scope);
        }).catch(function(error) {
            // registration failed
            console.log('Registration failed with ' + error);
        });
        }
    

    安装和激活:填充缓存

    这里需要注意的是 cache.addAll 一个路径不对就会全部缓存失败.

        //在 sw.js 中
        this.addEventListener('install', function(event) {
            event.waitUntil(
                caches.open('v1').then(function(cache) {
                return cache.addAll([
                    '/',
                    '/index.html',
                    '/style.css',
                    '/app.js',
                    '/image-list.js',
                    '/star-wars-logo.jpg',
                    '/gallery/',
                    '/gallery/bountyHunters.jpg',
                    '/gallery/myLittleVader.jpg',
                    '/gallery/snowTroopers.jpg'
                ]);
                })
            );
        });
    

    自定义请求响应

    对于请求我们我们首先会在缓存中查找资源是否被缓存,如果有,将会返回缓存的资源,如果不存在,会转而从网络中请求数据,然后将它缓存起来,这样下次有相同的请求发生时,我们就可以直接使用缓存。这种策略是缓存优先

        self.addEventListener('fetch', function(event) {
            // caches.match(event.request):请求通过 url匹配 cache中的资源
            event.respondWith(caches.match(event.request).then(function(response) {
                //这里response已经是访问缓存后的返回了.
                // caches.match() always resolves
                // but in case of success response will have value
                if (response !== undefined) {
                    return response;
                } else {
                return fetch(event.request).then(function (response) {
                    // response may be used only once
                    // we need to save clone to put one copy in cache
                    // and serve second one
                    let responseClone = response.clone();
                    
                    caches.open('v1').then(function (cache) {
                    cache.put(event.request, responseClone);
                    });
                    return response;
                }).catch(function () {
                    return caches.match('/gallery/myLittleVader.jpg');
                }); 
                }
            }));
        });
    

    这样我们就实现了离线缓存了.

    更新,清理

    1. 更新缓存 版本总会有更新资源的时候 我们应该如何去更新它的Service Worker?我们存放在缓存名称中的版本号是这个问题的关键:

          let cacheName='v1'
      

      在把版本号更新为 v2 的时候,线程中会有一个新的Service Worker安装,并将最新资源缓存在 v2 的 cache 中.而旧的service worker仍然会正确的运行,直到没有任何页面使用到它为止,这时候新的service worker将会被激活.触发activate事件:

    2. 清理缓存

          self.addEventListener('activate', function(event) {
              var cacheWhitelist = [cacheName];
              event.waitUntil(
                  caches.keys().then(function(keyList) {
                      return Promise.all(keyList.map(function(key) {
                          //删除白名单之外的缓存
                          if (cacheWhitelist.indexOf(key) === -1) {
                              return caches.delete(key);
                          }
                      }));
                  })
              );
          });
      

    卸载

    当我们网站应用不再需要 serviceWorker 的时候,如何卸载?在下个版本不注册serviceWorker? 正确的做法是:我们可以在新版本里使用unregister卸载.

        if ('serviceWorker' in navigator) {
                navigator.serviceWorker.ready.then(registration => {
                registration.unregister();
            });
        }
    

    这样就好了吗? 注意:registration.unregister()会卸载掉 sw,但是并不会删除我们之前的缓存文件,所以在卸载sw之前,我们必须干掉在之前serviceWorker中用到的 IndexedDB,Storage,Caches

    缓存策略

    以下最为保守的缓存策略。

    • HTM:如果你想让页面离线可以访问,使用 NetworkFirst,如果不需要离线访问,使用 NetworkOnly,其他策略均不建议对 HTML 使用。
    • CSS 和 JS:情况比较复杂,因为一般站点的 CSS,JS 都在 CDN 上,SW 并没有办法判断从 CDN 上请求下来的资源是否正确(HTTP 200),如果缓存了失败的结果,问题就大了。这种我建议使用 Stale-While-Revalidate 策略,既保证了页面速度,即便失败,用户刷新一下就更新了。 如果你的 CSS,JS 与站点在同一个域下,并且文件名中带了 Hash 版本号,那可以直接使用 Cache First 策略。
    • 图片: 建议使用 Cache First,并设置一定的失效事件,请求一次就不会再变动了。

    注意:对于不在同一域下的任何资源,绝对不能使用 Cache only 和 Cache first。

    更多缓存策略请见离线指南

    调试

    chrome访问chrome://inspect/#service-workerschrome://serviceworker-internals查看service-workers firefox通过about:debugging#workers查看service-workers

    兼容性

    Desktop

    Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari (WebKit)
    Basic support 40.0 33.0 (33.0)[1] 未实现 24 未实现

    Mobile

    Feature Android Chrome for Android Firefox Mobile (Gecko) Firefox OS IE Phone Opera Mobile Safari Mobile
    Basic support 未实现 40.0 (Yes) (Yes) 未实现 (Yes) 未实现

    兼容方案:

    • 关于 service workers 一个很棒的事情就是,如果你用像上面一样的浏览器特性检测方式发现浏览器并不支持。与此同时,如果你在一个页面上同时使用 AppCache 和 SW , 不支持 SW 但是支持 AppCache 的浏览器,可以使用 AppCache,如果都支持的话,则会采用 SW
    • 使用ServiceWorker cache polyfill让旧版本浏览器支持 ServiceWorker cache API,

    关于ServiceWorker的一些其他应用

    • 1 后台数据同步(sync 事件)
    • 2 响应推送(push 事件)
    • 3 性能增强(比如预取用户可能需要的资源,比如相册中的后面数张图片)

    实际应用

    从开头对Service Worker的介绍就知道

    ServiceWorker可以不依赖任何页面,一旦注册可以一直存在(除非手动销毁)并作用于所在域内用户可访问的URL

    因此,我们在网络编程中的缓存策略尤为重要,而所有站点 Service Worker 的 install 和 active 都差不多,无非是做预缓存资源列表,更新后缓存清理的工作,逻辑不太复杂.

    针对这种情况,我们更多的使用是Google 官方的 PWA 框架:Workbox3代替,它正是解决Service Worker Api( install、active、 fetch 事件做相应逻辑处理等)过于复杂的问题.

    谁在使用

    淘宝: PC 首页的 Service Worker 上线已经有一段时间了,经过不断地对缓存策略的调整,收益还是比较明显的,页面总下载时间从平均 1.7s,下降到了平均 1.4s,缩短了近 18% 的下载时间。(2018-08-09)

    蓝狐:蓝狐是我们经常用的编辑查看原型文档应用,里面有很多图片等静态资源,非常适合使用 SW缓存

    CSDN:在使用CSDN的时候经常有消息推送就是使用 SW 实现的

    写到最后

    这次分享主要是给大家介绍Service Worker的基础知识,实现离线应用的原理.希望大家可以从中有所收获.

    注:PWA(Progressive web apps,渐进式 Web 应用)运用现代的 Web API 以及传统的渐进式增强策略来创建跨平台 Web 应用程序。这些应用无处不在、功能丰富,使其具有与原生应用相同的用户体验优势。
    参考文献