什么是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生命周期的文章中最常见的就是下面这张图。

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

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

  1. if (navigator.serviceWorker != null) {
  2. // 使用浏览器特定方法注册一个新的service worker
  3. navigator.serviceWorker.register('sw.js')
  4. .then(function(registration) {
  5. window.registration = registration;
  6. console.log('Registered events at scope: ', registration.scope);
  7. });
  8. }

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

  1. var cacheStorageKey = 'minimal-pwa-3';
  2.  
  3. var cacheList = [
  4. '/',
  5. "index.html",
  6. "main.css",
  7. "e.png",
  8. "pwa-fonts.png"
  9. ]
  10.  
  11. // 当浏览器解析完sw文件时,serviceworker内部触发install事件
  12. self.addEventListener('install', function(e) {
  13. console.log('Cache event!')
  14. // 打开一个缓存空间,将相关需要缓存的资源添加到缓存里面
  15. e.waitUntil(
  16. caches.open(cacheStorageKey).then(function(cache) {
  17. console.log('Adding to Cache:', cacheList)
  18. return cache.addAll(cacheList)
  19. })
  20. )
  21. })

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

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

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

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

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

  1. // 如果当前浏览器没有激活的service worker或者已经激活的worker被解雇,
  2. // 新的service worker进入active事件
  3. self.addEventListener('activate', function(e) {
  4. console.log('Activate event');
  5. console.log('Promise all', Promise, Promise.all);
  6. // active事件中通常做一些过期资源释放的工作
  7. var cacheDeletePromises = caches.keys().then(cacheNames => {
  8. console.log('cacheNames', cacheNames, cacheNames.map);
  9. return Promise.all(cacheNames.map(name => {
  10. if (name !== cacheStorageKey) { // 如果资源的key与当前需要缓存的key不同则释放资源
  11. console.log('caches.delete', caches.delete);
  12. var deletePromise = caches.delete(name);
  13. console.log('cache delete result: ', deletePromise);
  14. return deletePromise;
  15. } else {
  16. return Promise.resolve();
  17. }
  18. }));
  19. });
  20.  
  21. console.log('cacheDeletePromises: ', cacheDeletePromises);
  22. e.waitUntil(
  23. Promise.all([cacheDeletePromises]
  24. )
  25. )
  26. })

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

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

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

  1. fetch('./data.json')

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

  1. self.addEventListener('fetch', function(e) {
  2. console.log('Fetch event ' + cacheStorageKey + ' :', e.request.url);
  3. e.respondWith( // 首先判断缓存当中是否已有相同资源
  4. caches.match(e.request).then(function(response) {
  5. if (response != null) { // 如果缓存中已有资源则直接使用
  6. // 否则使用fetch API请求新的资源
  7. console.log('Using cache for:', e.request.url)
  8. return response
  9. }
  10. console.log('Fallback to fetch:', e.request.url)
  11. return fetch(e.request.url);
  12. })
  13. )
  14. })

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

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

缓存资源更新

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

  1. // 当浏览器解析完sw文件时,serviceworker内部触发install事件
  2. self.addEventListener('install', function(e) {
  3. debugger;
  4. console.log('Cache event!')
  5. // 打开一个缓存空间,将相关需要缓存的资源添加到缓存里面
  6. e.waitUntil(
  7. caches.open(cacheStorageKey).then(function(cache) {
  8. console.log('Adding to Cache:', cacheList)
  9. return cache.addAll(cacheList)
  10. }).then(function() {
  11. console.log('install event open cache ' + cacheStorageKey);
  12. console.log('Skip waiting!')
  13. return self.skipWaiting();
  14. })
  15. )
  16. })
  17.  
  18. // 如果当前浏览器没有激活的service worker或者已经激活的worker被解雇,
  19. // 新的service worker进入active事件
  20. self.addEventListener('activate', function(e) {
  21. debugger;
  22. console.log('Activate event');
  23. console.log('Promise all', Promise, Promise.all);
  24. // active事件中通常做一些过期资源释放的工作
  25. var cacheDeletePromises = caches.keys().then(cacheNames => {
  26. console.log('cacheNames', cacheNames, cacheNames.map);
  27. return Promise.all(cacheNames.map(name => {
  28. if (name !== cacheStorageKey) { // 如果资源的key与当前需要缓存的key不同则释放资源
  29. console.log('caches.delete', caches.delete);
  30. var deletePromise = caches.delete(name);
  31. console.log('cache delete result: ', deletePromise);
  32. return deletePromise;
  33. } else {
  34. return Promise.resolve();
  35. }
  36. }));
  37. });
  38.  
  39. console.log('cacheDeletePromises: ', cacheDeletePromises);
  40. e.waitUntil(
  41. Promise.all([cacheDeletePromises]
  42. ).then(() => {
  43. console.log('activate event ' + cacheStorageKey);
  44. console.log('Clients claims.')
  45. return self.clients.claim();
  46. })
  47. )
  48. })

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

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

  1. self.addEventListener('fetch', function(e) {
  2. console.log('Fetch event ' + cacheStorageKey + ' :', e.request.url);
  3. e.respondWith( // 该策略先从网络中获取资源,如果获取失败则再从缓存中读取资源
  4. fetch(e.request.url)
  5. .then(function (httpRes) {
  6.  
  7. // 请求失败了,直接返回失败的结果
  8. if (!httpRes || httpRes.status !== 200) {
  9. // return httpRes;
  10. return caches.match(e.request)
  11. }
  12.  
  13. // 请求成功的话,将请求缓存起来。
  14. var responseClone = httpRes.clone();
  15. caches.open(cacheStorageKey).then(function (cache) {
  16. return cache.delete(e.request)
  17. .then(function() {
  18. cache.put(e.request, responseClone);
  19. });
  20. });
  21.  
  22. return httpRes;
  23. })
  24. .catch(function(err) { // 无网络情况下从缓存中读取
  25. console.error(err);
  26. return caches.match(e.request);
  27. })
  28. )
  29. })

注意事项

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

  依赖项:

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

  错误排查:

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

  同时这里我也为大家录制视频,可以更清晰的看到这些细节。

Web离线应用解决方案——ServiceWorker的更多相关文章

  1. localForage——轻松实现 Web 离线存储

    Web 应用程序有离线功能,如保存大量数据集和二进制文件.你甚至可以做缓存 MP3 文件这样的事情.浏览器技术可以保存离线数据和大量的储存.但问题是,如何选择合适技术,如何方便灵活的实现. 如果你需要 ...

  2. 宣布发布 Windows Azure 导入/导出服务的预览版以及 Web 和移动解决方案场景的若干增强功能

    客户评估基于云的存储解决方案时,面临的挑战之一是以经济高效.安全快速的方式从 Blob 存储区移进和移出大量数据.今天,我们很高兴地宣布发布 Windows Azure 导入/导出的预览版,这款新服务 ...

  3. web离线应用--dom storage

    web离线应用--dom storage dom storage是html5添加的新功能,其实也不是什么新的应用,只不过是cookie的放大版本,由于cookie的大小只有4kb,而且在每次请求一个新 ...

  4. ​Web安全测试解决方案

    Web安全测试解决方案 介绍常见的Web安全风险,Web安全测试方法.测试基本理论和测试过程中的工具引入

  5. 手把手让你实现开源企业级web高并发解决方案(lvs+heartbeat+varnish+nginx+eAccelerator+memcached)

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://freeze.blog.51cto.com/1846439/677348 此文凝聚 ...

  6. Web打印的解决方案之证件套打

    由于以前未接触过套打,一直觉得套打是一个比较神秘和麻烦的事情,因为打印机的位置总是需要调整的,你总不能硬编码吧?但是如果位置可调,有需要直观一些来处理,那就比较麻烦了. 在前面介绍过<Web打印 ...

  7. Lightning Web Components 来自salesforce 的web 组件化解决方案

    Lightning Web Components 是一个轻量,快速,企业级别的web 组件化解决方案,官方网站也提供了很全的文档 对于我们学习使用还是很方便的,同时我们也可以方便的学习了解salesf ...

  8. WEB应用安全解决方案测试验证

    WEB应用安全解决方案测试报告 --- By jiang.jx at 2017-08-11 WEB应用安全解决方案.docx 链接:https://share.weiyun.com/068b05467 ...

  9. CAD_DWG图Web可视化一站式解决方案-唯杰地图-vjmap

    背景 DWG图是AutoCAD是私有格式,只能在CAD软件上编辑查看,如何发布至Web上做数据展示,GIS分析应用开发,一直是业内头疼的事情. 传统的办法采用的解析AutoCAD图形绘制,并封装成Ac ...

随机推荐

  1. rsync 服务部署详解

    第1章 rsync 软件介绍 1.1 什么是rsync rsync 是一款开源的.快速的.多功能的.可实现全量及增量的本地或远程数据同步备份的优秀工具. http://www.samba.org/ft ...

  2. server-sent-event使用流信息向客户端发送数据

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. css中单位 px、em 的区别【转载】

    原文:http://www.admin10000.com/document/6267.html     在国内网站中,包括三大门户,以及“引领”中国网站设计潮流的蓝色理想,ChinaUI等都是使用了p ...

  4. 一个简单的MVC框架的实现

    1.Action接口 package com.togogo.webtoservice; import javax.servlet.http.HttpServletRequest; import jav ...

  5. ldap数据库--ODSEE--ACI

    查看跟DN下的aci ldapsearch -h hostname -p port -D "cn=Directory Manager" -w - -b "BASE_DN& ...

  6. 获取所有栈的信息,只有最上面的和最下面的,但是不能获取栈中间的activity信息

    直接在cmd窗口上输入 adb shell后,再输入dumpsys activity activities,可以看到所有的activity以及相关栈状态

  7. Power BI连接SSAS(微软的分析服务)进行权限控制(本地部署)

    尬聊...... 在干活之前先尬聊一会儿 丸子我在10月下旬左右就开始弄power BI连接SSAS进行权限控制的问题,中间也是历经波折,看了网上很多资料,可是都是SSAS怎么进行权限控制,没有SSA ...

  8. 发现AspNet.Core版本控制库Bug一枚,你还想入坑?

    我,博客写作小白一枚,注册账号多年却未曾留下只言片语,在潜水的这些年里从大家的博客中收获了很多新的知识忽觉惶恐心有不安,是时候给大家分享一些我的经验和教训了.嗯嗯,实话告诉大家前面的话的都是来凑字数的 ...

  9. 关于iOS GangSDK的使用,为App快速集成社群公会模块

    手上有一个自己开发的小游戏,想加一个家族系统活跃下游戏的氛围,想到这块儿可能会有大量的工作需要自己做,就偷了个懒去网上搜罗了一波,结果惊奇的发现居然真的有类似的服务,并且还是免费的,所以决定入坑尝试一 ...

  10. formData实现图片上传

    前言 在 上一篇 已经实现了图片预览,那么如何上传图片呢?有两种思路: 1.将图片转化为dataURL(base64),这样就成为了一串字符串,再传到服务端.不过这样缺点很多,数据量比转换之前增加1/ ...