什么是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时,按照如下的方式来加载:

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。

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

缓存资源更新

  那么如果我们在新版本中更新了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平台对他的支持性并不友好,但是在安卓侧已经没有大问题。而微信平台对它的支持也不错。

  依赖项:

  • 依赖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. Best Coder #86 1001 Price List(大水题)

    Price List Accepts: 880 Submissions: 2184 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 26214 ...

  2. Hadoop 中 最重要的两个模块

    Hadoop 中 最重要的两个模块 HDFS   分布式的文件系统 主节点: NameNode SecondaryNamenode ResourceManager 从节点: DataNode Node ...

  3. 数据库集群 MySQL主从复制

    MySQL主从复制 本节内容我们联系使用MySQL的主从复制功能配置Master和Slave节点,验证数据MySQL的数据同步功能. 因为要使用多个MySQL数据库,所以不建议在电脑上安装多个MySQ ...

  4. [Intel Edison开发板] 06、Edison开发在linux中烧写、配置、搭建开发环境

    1.前言 linux上烧写.配置.搭建Edison环境,千万不要用默认的setup tool for ubuntu!!! (即使,你用的就是ubuntu) 因为,其默认的工具会从一个坏链接下载配置文件 ...

  5. Linux端图形处理工具ImageMagick在Centos上的安装

    一.安装背景程序要用到用户上传图片,编辑的功能,能进行旋转,裁剪,缩放等. 二.ImageMagick介绍 ImageMagick是用C语言开发图片处理程序.可以对图片进行改变大小.旋转.锐化.减色或 ...

  6. [转载] ZooKeeper原理及使用

    转载自http://www.wuzesheng.com/?p=2609 ZooKeeper是Hadoop Ecosystem中非常重要的组件,它的主要功能是为分布式系统提供一致性协调(Coordina ...

  7. python基础-------模块与包(二)

    sys模块.logging模块.序列化 一.sys模块 sys.argv           命令行参数List,第一个元素是程序本身路径 sys.exit(n)        退出程序,正常退出时e ...

  8. Server 2008 R2远程桌面授权,解决120天过期问题

    平时在使用远程桌面过程,我们经常会遇到这样的两个问题. 问题一.远程桌面的连接数限制 Server 2008 R2默认远程桌面连接数是2个用户,如果多余两个用户进行远程桌面连接时,系统就会提示超过连接 ...

  9. Android 使用GangSDK创建第三方家族公会系统经验分享

    由于需要对之前的游戏加入一个家族系统,想到这块儿可能会有大量的工作需要自己做,就偷了个懒去网上搜罗了一波,有没有类似现成的系统?结果让我惊奇的发现,目前市面上居然真的有类似的服务,虽然是小公司开发的, ...

  10. sort学习 - LeetCode #406 Queue Reconstruction by Height

    用python实现多级排序,可以像C语言那样写个my_cmp,然后在sort的时候赋给参数cmp即可 但实际上,python处理cmp 是很慢的,因为每次比较都会调用my_cmp:而使用key和rev ...