Web离线应用解决方案——ServiceWorker

摘要:
什么是ServiceWorker在介绍ServiceWorker之前,让我们谈谈PWA。由于PWA是由Google提出的,ServiceWorker也有一些功能要求:后台消息传递网络代理、转发请求、伪造响应离线缓存消息推送。在这个阶段,ServiceWorker的主要功能集中在网络代理和离线缓存上。下图是介绍ServiceWorker生命周期的所有文章中最常见的一个。此图将ServiceWorker的声明周期分为两部分,即ServiceWorker主线程中的状态和子线程中的状况。此时,注册。从主线程返回的waiting属性表示处于已安装状态的ServiceWorker。

什么是ServiceWorker

  在介绍ServiceWorker之前,我们先来谈谈PWA。PWA (Progressive Web Apps) 是一种 Web App 新模型,并不是具体指某一种前沿的技术或者某一个单一的知识点,,这是一个渐进式的 Web App,是通过一系列新的 Web 特性,配合优秀的 UI 交互设计,逐步的增强 Web App 的用户体验。

  • Https环境部署
  • 响应式设计,一次部署,可以在移动设备和 PC 设备上运行 在不同浏览器下可正常访问。
  • 浏览器离线和弱网环境可极速访问
  • 可以把 App Icon 入口添加到桌面。
  • 点击 Icon 入口有类似 Native App 的动画效果。
  • 灵活的热更新

  在PWA要求的各种能力上,关于离线环境的支持我们就需要仰赖ServiceWorker。Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。由于PWA是谷歌提出,那么对ServiceWorker,同样也提出一些能力要求:

  • 后台消息传递
  • 网络代理,转发请求,伪造响应
  • 离线缓存
  • 消息推送

  在目前阶段,ServiceWorker的主要能力集中在网络代理和离线缓存上。具体的实现上,可以理解为ServiceWorker是一个能在网页关闭时仍然运行的WebWorker。

ServiceWorker的生命周期

  刚才讲到ServiceWorker拥有离线能力的WebWorker,既然这么强的能力,那就需要好好管理起来。所以我们要明白ServiceWorker的生命周期,也就是它从创建到销毁的过程。在所有介绍ServiceWorker生命周期的文章中最常见的就是下面这张图。

Web离线应用解决方案——ServiceWorker第1张

  整个过程中一个ServiceWorker会经历:安装、激活、等待、销毁的阶段。但实际上这张图我感觉并没有清晰的解释ServiceWorker的声明周期,所以我制作了下面这张图。

Web离线应用解决方案——ServiceWorker第2张

  这张图把ServiceWorker的声明周期分为了两部分,主线程中的状态和ServiceWorker子线程中的状态。子线程中的代码处在一个单独的模块中,当我们需要使用ServiceWorker时,按照如下的方式来加载:

复制代码
if (navigator.serviceWorker != null) {
      // 使用浏览器特定方法注册一个新的service worker
      navigator.serviceWorker.register('sw.js')
      .then(function(registration) {
        window.registration = registration;
        console.log('Registered events at scope: ', registration.scope);
      });
    }
复制代码

  这个时候ServiceWorker处于Parsed解析阶段。当解析完成后ServiceWorker处于Installing安装阶段,主线程的registration的installing属性代表正在安装的ServiceWorker实例,同时子线程中会触发install事件,并在install事件中指定缓存资源

复制代码
var cacheStorageKey = 'minimal-pwa-3';

var cacheList = [
  '/',
  "index.html",
  "main.css",
  "e.png",
  "pwa-fonts.png"
]

// 当浏览器解析完sw文件时,serviceworker内部触发install事件
self.addEventListener('install', function(e) {
  console.log('Cache event!')
  // 打开一个缓存空间,将相关需要缓存的资源添加到缓存里面
  e.waitUntil(
    caches.open(cacheStorageKey).then(function(cache) {
      console.log('Adding to Cache:', cacheList)
      return cache.addAll(cacheList)
    })
  )
})
复制代码

  这里使用了Cache API来将资源缓存起来,同时使用e.waitUntil接手一个Promise来等待资源缓存成功,等到这个Promise状态成功后,ServiceWorker进入installed状态,意味着安装完毕。这时候主线程中返回的registration.waiting属性代表进入installed状态的ServiceWorker。

/* In main.js */
navigator.serviceWorker.register('./sw.js').then(function(registration) {  
    if (registration.waiting) {
        // Service Worker is Waiting
    }
})

  然而这个时候并不意味着这个ServiceWorker会立马进入下一个阶段,除非之前没有新的ServiceWorker实例,如果之前已有ServiceWorker,这个版本只是对ServiceWorker进行了更新,那么需要满足如下任意一个条件,新的ServiceWorker才会进入下一个阶段:

  • 在新的ServiceWorker线程代码里,使用了self.skipWaiting() 
  • 或者当用户导航到别的网页,因此释放了旧的ServiceWorker时候
  • 或者指定的时间过去后,释放了之前的ServiceWorker

  这个时候ServiceWorker的生命周期进入Activating阶段,ServiceWorker子线程接收到activate事件:

复制代码
// 如果当前浏览器没有激活的service worker或者已经激活的worker被解雇,
// 新的service worker进入active事件
self.addEventListener('activate', function(e) {
  console.log('Activate event');
  console.log('Promise all', Promise, Promise.all);
  // active事件中通常做一些过期资源释放的工作
  var cacheDeletePromises = caches.keys().then(cacheNames => {
    console.log('cacheNames', cacheNames, cacheNames.map);
    return Promise.all(cacheNames.map(name => {
      if (name !== cacheStorageKey) { // 如果资源的key与当前需要缓存的key不同则释放资源
        console.log('caches.delete', caches.delete);
        var deletePromise = caches.delete(name);
        console.log('cache delete result: ', deletePromise);
        return deletePromise;
      } else {
        return Promise.resolve();
      }
    }));
  });

  console.log('cacheDeletePromises: ', cacheDeletePromises);
  e.waitUntil(
    Promise.all([cacheDeletePromises]
    )
  )
})
复制代码

  这个时候通常做一些缓存清理工作,当e.waitUntil接收的Promise进入成功状态后,ServiceWorker的生命周期则进入activated状态。这个时候主线程中的registration的active属性代表进入activated状态的ServiceWorker实例

/* In main.js */
navigator.serviceWorker.register('./sw.js').then(function(registration) {  
    if (registration.active) {
        // Service Worker is Active
    }
})

  到此一个ServiceWorker正式进入激活状态,可以拦截网络请求了。如果主线程有fetch方式请求资源,那么就可以在ServiceWorker代码中触发fetch事件:

fetch('./data.json')

  这时在子线程就会触发fetch事件:

复制代码
self.addEventListener('fetch', function(e) {
  console.log('Fetch event ' + cacheStorageKey + ' :', e.request.url);
  e.respondWith( // 首先判断缓存当中是否已有相同资源
    caches.match(e.request).then(function(response) {
      if (response != null) { // 如果缓存中已有资源则直接使用
        // 否则使用fetch API请求新的资源
        console.log('Using cache for:', e.request.url)
        return response
      }
      console.log('Fallback to fetch:', e.request.url)
      return fetch(e.request.url);
    })
  )
})
复制代码

  那么如果在install或者active事件中失败,ServiceWorker则会直接进入Redundant状态,浏览器会释放资源销毁ServiceWorker。

  现在如果没有网络进入离线状态,或者资源命中缓存那么就会优先读取缓存的资源:

Web离线应用解决方案——ServiceWorker第11张

缓存资源更新

  那么如果我们在新版本中更新了ServiceWorker子线程代码,当访问网站页面时浏览器获取了新的文件,逐字节比对 /sw.js 文件发现不同时它会认为有更新启动 更新算法open_in_new,于是会安装新的文件并触发 install 事件。但是此时已经处于激活状态的旧的 Service Worker 还在运行,新的 Service Worker 完成安装后会进入 waiting 状态。直到所有已打开的页面都关闭,旧的 Service Worker 自动停止,新的 Service Worker 才会在接下来重新打开的页面里生效。如果想要立即更新需要在新的代码中做一些处理。首先在install事件中调用self.skipWaiting()方法,然后在active事件中调用self.clients.claim()方法通知各个客户端。

复制代码
// 当浏览器解析完sw文件时,serviceworker内部触发install事件
self.addEventListener('install', function(e) {
  debugger;
  console.log('Cache event!')
  // 打开一个缓存空间,将相关需要缓存的资源添加到缓存里面
  e.waitUntil(
    caches.open(cacheStorageKey).then(function(cache) {
      console.log('Adding to Cache:', cacheList)
      return cache.addAll(cacheList)
    }).then(function() {
      console.log('install event open cache ' + cacheStorageKey);
      console.log('Skip waiting!')
      return self.skipWaiting();
    })
  )
})

// 如果当前浏览器没有激活的service worker或者已经激活的worker被解雇,
// 新的service worker进入active事件
self.addEventListener('activate', function(e) {
  debugger;
  console.log('Activate event');
  console.log('Promise all', Promise, Promise.all);
  // active事件中通常做一些过期资源释放的工作
  var cacheDeletePromises = caches.keys().then(cacheNames => {
    console.log('cacheNames', cacheNames, cacheNames.map);
    return Promise.all(cacheNames.map(name => {
      if (name !== cacheStorageKey) { // 如果资源的key与当前需要缓存的key不同则释放资源
        console.log('caches.delete', caches.delete);
        var deletePromise = caches.delete(name);
        console.log('cache delete result: ', deletePromise);
        return deletePromise;
      } else {
        return Promise.resolve();
      }
    }));
  });

  console.log('cacheDeletePromises: ', cacheDeletePromises);
  e.waitUntil(
    Promise.all([cacheDeletePromises]
    ).then(() => {
      console.log('activate event ' + cacheStorageKey);
      console.log('Clients claims.')
      return self.clients.claim();
    })
  )
})
复制代码

  注意这里说的是浏览器获取了新版本的ServiceWorker代码,如果浏览器本身对sw.js进行缓存的话,也不会得到最新代码,所以对sw文件最好配置成cache-control: no-cache或者添加md5。

  实际过程中像我们刚才把index.html也放到了缓存中,而在我们的fetch事件中,如果缓存命中那么直接从缓存中取,这就会导致即使我们的index页面有更新,浏览器获取到的永远也是都是之前的ServiceWorker缓存的index页面,所以有些ServiceWorker框架支持我们配置资源更新策略,比如我们可以对主页这种做策略,首先使用网络请求获取资源,如果获取到资源就使用新资源,同时更新缓存,如果没有获取到则使用缓存中的资源。代码如下:

复制代码
self.addEventListener('fetch', function(e) {
  console.log('Fetch event ' + cacheStorageKey + ' :', e.request.url);
  e.respondWith( // 该策略先从网络中获取资源,如果获取失败则再从缓存中读取资源
    fetch(e.request.url)
    .then(function (httpRes) {

      // 请求失败了,直接返回失败的结果
      if (!httpRes || httpRes.status !== 200) {
          // return httpRes;
          return caches.match(e.request)
      }

      // 请求成功的话,将请求缓存起来。
      var responseClone = httpRes.clone();
      caches.open(cacheStorageKey).then(function (cache) {
          return cache.delete(e.request)
          .then(function() {
              cache.put(e.request, responseClone);
          });
      });

      return httpRes;
    })
    .catch(function(err) { // 无网络情况下从缓存中读取
      console.error(err);
      return caches.match(e.request);
    })
  )
})
复制代码

注意事项

  ServiceWorker是一项新能力,目前IOS平台对他的支持性并不友好,但是在安卓侧已经没有大问题。而微信平台对它的支持也不错。

Web离线应用解决方案——ServiceWorker第16张

  依赖项:

  • 依赖Cache API
  • 依赖Fetch API Promise API
  • Https环境

   错误排查:

  • install或active事件失败
  • 非Https环境
  • sw.js安装路径问题
  • scope设置

免责声明:文章转载自《Web离线应用解决方案——ServiceWorker》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇黄聪:VirtualBox 安装ghost版windows XP【转】go test命令(Go语言测试命令)完全攻略下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

云端远程Ubuntu系统进行无桌面Web浏览器自动化测试

【摘要】 利用xvfb提供的显卡帧缓冲区,让浏览器以为有桌面,以达到无桌面系统下测试真实浏览器兼容性的目的。 自动化web界面测试往往需要验证真实浏览器的兼容性,但是云端系统往往并不提供图形化的桌面,所以自动化web界面测试在云端就成为一个问题。本文描述了一个技巧解决这个问题,其主要原理是利用xvfb提供的显卡帧缓冲区,让浏览器以为有桌面。 Install...

spring-kafka生产者消费者配置详解

一、生产者1、重要配置    # 高优先级配置    # 以逗号分隔的主机:端口对列表,用于建立与Kafka群集的初始连接    spring.kafka.producer.bootstrap-servers=TopKafka1:9092,TopKafka2:9092,TopKafka3:9092         # 设置大于0的值将使客户端重新发送任何数...

了解SQL Server锁争用:NOLOCK 和 ROWLOCK 的秘密

http://blog.csdn.net/Atwind/article/details/1832844 关系型数据库,如SQL Server,使用锁来避免多用户修改数据时的并发冲突。当一组数据被某个用户锁定时,除非第一个用户结束修改并释放锁,否则其他用户就无法修改该组数据。 有些数据库,包括SQL Server,用锁来避免用户检索未递交的修改记录。在这...

主流服务器虚拟化技术简单使用——KVM(二)

通过Linux工具管理KVM 主流服务器虚拟化技术简单使用——KVM(一)部署了一台KVM主机,提到KVM可以通过命令行工具(virt-install、virsh)和GUI工具(virt-manager)管理虚拟机。实际上virt-install、virsh、virt-manager只是管理工具,如果部署多台KVM,并不需要每一台都安装这些管理工具,因为它...

C# WebApi 接口传参详解

本篇打算通过get、post、put、delete四种请求方式分别谈谈基础类型(包括int/string/datetime等)、实体、数组等类型的参数如何传递。 一、get请求 对于取数据,我们使用最多的应该就是get请求了吧。下面通过几个示例看看我们的get请求参数传递。 1、基础类型参数 ? 1 2 3 4 5 [HttpGet] publ...

HTTP缓存

本文是《HTTP权威指南》读书笔记 Web缓存是可以自动保存常见文档副本的设备。当Web请求抵达缓存时,如果本地在“已缓存”的的副本,就可以从本地存储设备而不是原始服务器中提取这个文档。使用缓存可以有以下优点: 缓存节省了冗余的数据的传输,节省了网络费用; 缓解了网络瓶颈问题,不需要更多的带宽就可以更快地加载页面; 缓存降低了对原始服务器的要求, 让服...