Web离线应用解决方案——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生命周期的文章中最常见的就是下面这张图。
整个过程中一个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的更多相关文章
- localForage——轻松实现 Web 离线存储
Web 应用程序有离线功能,如保存大量数据集和二进制文件.你甚至可以做缓存 MP3 文件这样的事情.浏览器技术可以保存离线数据和大量的储存.但问题是,如何选择合适技术,如何方便灵活的实现. 如果你需要 ...
- 宣布发布 Windows Azure 导入/导出服务的预览版以及 Web 和移动解决方案场景的若干增强功能
客户评估基于云的存储解决方案时,面临的挑战之一是以经济高效.安全快速的方式从 Blob 存储区移进和移出大量数据.今天,我们很高兴地宣布发布 Windows Azure 导入/导出的预览版,这款新服务 ...
- web离线应用--dom storage
web离线应用--dom storage dom storage是html5添加的新功能,其实也不是什么新的应用,只不过是cookie的放大版本,由于cookie的大小只有4kb,而且在每次请求一个新 ...
- Web安全测试解决方案
Web安全测试解决方案 介绍常见的Web安全风险,Web安全测试方法.测试基本理论和测试过程中的工具引入
- 手把手让你实现开源企业级web高并发解决方案(lvs+heartbeat+varnish+nginx+eAccelerator+memcached)
原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://freeze.blog.51cto.com/1846439/677348 此文凝聚 ...
- Web打印的解决方案之证件套打
由于以前未接触过套打,一直觉得套打是一个比较神秘和麻烦的事情,因为打印机的位置总是需要调整的,你总不能硬编码吧?但是如果位置可调,有需要直观一些来处理,那就比较麻烦了. 在前面介绍过<Web打印 ...
- Lightning Web Components 来自salesforce 的web 组件化解决方案
Lightning Web Components 是一个轻量,快速,企业级别的web 组件化解决方案,官方网站也提供了很全的文档 对于我们学习使用还是很方便的,同时我们也可以方便的学习了解salesf ...
- WEB应用安全解决方案测试验证
WEB应用安全解决方案测试报告 --- By jiang.jx at 2017-08-11 WEB应用安全解决方案.docx 链接:https://share.weiyun.com/068b05467 ...
- CAD_DWG图Web可视化一站式解决方案-唯杰地图-vjmap
背景 DWG图是AutoCAD是私有格式,只能在CAD软件上编辑查看,如何发布至Web上做数据展示,GIS分析应用开发,一直是业内头疼的事情. 传统的办法采用的解析AutoCAD图形绘制,并封装成Ac ...
随机推荐
- 第十一章 多GPU系统的CUDA C
本章介绍了 显存和零拷贝内存的拷贝与计算对比 #include <stdio.h> #include "cuda_runtime.h" #include "d ...
- Linux 新建文件/文件夹,删除文件文件夹,查找文件 打开文件
1.新建文件夹:mkdir xx 2.新建文件: touch 1.py 3.删除文件/文件夹: rm -r xx rm 1.py 4.打开文件:cat 1.py 只显示前几行 :head -2 1. ...
- 如何将一个div水平垂直居中?4种方法做推荐
方案一: div绝对定位水平垂直居中[margin:auto实现绝对定位元素的居中], 兼容性:,IE7及之前版本不支持 div{ width: 200px; height: 200px; backg ...
- 通过PING命令中的TTL来判断对方操作系统
---恢复内容开始--- 通过PING命令中的TTL来判断对方操作系统简单来说,TTL全程Time to Live,意思就是生存周期.首先要说明ping命令是使用的网络层协议ICMP,所以TTL指的是 ...
- Dijkstra算法(Swift版)
原理 我们知道,使用Breadth-first search算法能够找到到达某个目标的最短路径,但这个算法没考虑weight,因此我们再为每个edge添加了权重后,我们就需要使用Dijkstra算法来 ...
- 解决mssql localdb 中文乱码问题
使用以下查询语句即可. alter database "E:\.Net Core\Database\hm.mdf" set single_user with rollback im ...
- [转载] Java实现生产者消费者问题
转载自http://www.cnblogs.com/happyPawpaw/archive/2013/01/18/2865957.html 引言 生产者和消费者问题是线程模型中的经典问题:生产者和消费 ...
- MarkdownPad2代码高亮插件兼容移动端样式
如果不知道MarkdownPad2使用代码高亮插件可以查看前一篇文章<MarkdownPad2使用代码高亮插件> 先看移动端效果图: 移动端点击查看效果 或者手机扫如下二维码: 我们经常阅 ...
- [转]ORACLE分区表的使用和管理
转自:http://love-flying-snow.iteye.com/blog/573303 废话少说,直接讲分区语法. Oracle表分区分为四种:范围分区,散列分区,列表分区和复合分区. 一: ...
- 《java.util.concurrent 包源码阅读》26 Fork/Join框架之Join
接下来看看调用ForkJoinTask的join方法都发生了什么: public final V join() { // doJoin方法返回该任务的状态,状态值有三种: // NORMAL, CAN ...