1. PWA和Service Worker的关系 html
PWA (Progressive Web Apps) 不是一项技术,也不是一个框架,咱们能够把她理解为一种模式,一种经过应用一些技术将 Web App 在安全、性能和体验等方面带来渐进式的提高的一种 Web App的模式。对于 webview 来讲,Service Worker 是一个独立于js主线程的一种 Web Worker 线程, 一个独立于主线程的 Context,可是面向开发者来讲 Service Worker 的形态其实就是一个须要开发者本身维护的文件,咱们假设这个文件叫作 sw.js。经过 service worker 咱们能够代理 webview 的请求至关因而一个正向代理的线程,fiddler也是干这些事情),在特定路径注册 service worker 后,能够拦截并处理该路径下全部的网络请求,进而实现页面资源的可编程式缓存,在弱网和无网状况下带来流畅的产品体验,因此 service worker 能够看作是实现pwa模式的一项技术实现。前端
2. service worker简介vue
-
注意事项webpack
- service worker 是一种JS工做线程,没法直接访问DOM, 该线程经过postMessage接口消息形式来与其控制的页面进行通讯;
- service worker 普遍使用了Promise,这些在接下来代码示例中将会看到;
- 目前并非全部主流浏览器支持 service worker, 能够经过 navigator && navigator.serviceWorker 来进行特性探测;
- 在开发过程当中,能够经过 localhost 使用服务工做线程,如若上线部署,必需要经过https来访问注册服务工做线程的页面,但有种场景是咱们的测试环境可能并不支持https,这时就要经过更改host文件将localhost指向测试环境ip来巧妙绕过该问题(例如:192.168.22.144 localhost);
-
生命周期web
- service worker的生命周期彻底独立于网页,要为网站安装服务工做线程,咱们须要在页面业务js代码中注册,浏览器从指定路径下载并解析服务工做线程脚本进而浏览器将会在后台启动安装步骤,在安装过程当中,咱们一般会缓存静态资源,若是全部文件都成功缓存,那么服务工程线程就安装完毕,若是任何文件下载失败或缓存失败,那么安装步骤将会失败,固然也不会被激活。安装后就进入激活步骤,这里是管理旧缓存的绝佳机会(后面代码示例中将会介绍缘由),激活后service worker将开始对其做用域内的全部页面实施控制。这里须要注意的是,首次注册 service worker 线程的页面须要再次加载才会受其控制。在成功安装完成并处于激活状态以前,服务工程线程不会收到fetch和push事件;
-
工做流程chrome
-
注册npm
- 这里须要注意的是register方法注册服务工做线程文件的位置,该path就是默认的 serviceworker 的做用域,例如注册path为/a/b/service-worker.js,则默认scope为/a/b/,固然也能够经过传入{scope: '/a/b/c/'}来指定本身的scope,但这里要特别注意的是,传入的scope参数必定是在默认做用域范围内再自定义(例如/a/b/c/),反之自定义为/d/e/就不行;
- 通俗来说,上面提到的scope就是 service worker 可以控制和发挥做用的范围;
- 注意注册是在本身的业务代码中进行,后面会有具体经过插件来实现注册的代码示例;
if(navigator && navigator.serviceWorker) {
navigator.serviceWorker.register('/service-worker.js').then(function (registration) {
console.log(registration)
}).catch(function (err) {
console.log(err)
})
}
-
安装编程
- 下面代码就是前面注册的service-worker.js文件内容;
- 咱们经过install事件来定义安装步骤,经过缓存名称调用caches.open(), 以后再调用cache.addAll()并传入具体缓存文件清单数组,这是一个Promise链式event.waitUntil()方法带有Promise参数并使用它来判断花费耗时以及安装是否成功;
- 正如前面提到,安装过程当中若是全部清单中文件成功缓存,则安装结束,不然安装过程视为失败,因此在实践中咱们尽量缓存核心资源以免服务工做线程未能安装;
var cacheVersion = 'test_2017122608';
// 安装服务工做线程
self.addEventListener('install', function(event){
// 须要缓存的资源
var cacheFiles = [
'/dist/index.html',
'/dist/js/index_async_bundle.js'
];
console.log('service worker: run into install');
event.waitUntil(caches.open(cacheVersion).then(function(cache)
{
return cache.addAll(cacheFiles);
}));
});
-
激活segmentfault
- 在某个时间点服务工程线程须要更新(例如:service-worker.js文件发生更改并上线),用户访问页面时浏览器会尝试在后台从新下载service-worker.js,若是服务工程线程文件与当前所用文件存在字节差别,则将其视为“新服务工做线程”;
- 新服务工做线程将会启动,且将会触发 install 事件;
- 此时旧的服务工做线程仍将控制着当前页面,所以新服务工做线程将会进入waiting状态;
- 当网站当前页面关闭时,旧服务工做线程将会终止,新服务工做线程将会取得控权;
- 新服务工做线程取得控制权后,将会触发 activate 事件;
- 监听 activate 事件的回调函数中常见的任务是管理缓存,前面我也提到过这是管理旧缓存的绝佳时机,由于若是在安装步骤中清理了旧缓存,因为旧的服务工做线程仍旧控制着页面,将没法从缓存中提取文件,可是在 activate 时旧服务工做线程已经终止了页面控制权,所在在这里清理旧缓存再合适不过;
// 新的service worker线程被激活(其实和离线包同样存在"二次生效"的机理)
self.addEventListener('activate', function (event) {
console.log('service worker: run into activate');
event.waitUntil(caches.keys().then(function (cacheNames) {
return Promise.all(cacheNames.map(function (cacheName) {
// 注意这里cacheVersion也能够是一个数组
if(cacheName !== cacheVersion){
console.log('service worker: clear cache' + cacheName);
return caches.delete(cacheName);
}
}));
}));
});
-
监听数组
- 这里经过监听fetch事件来代理响应,进而实现自定义前端资源缓存;
- 在event.respondWith()中咱们传入来自caches.match()的一个promise,此方法拦截请求并从服务工做线程所建立的任何缓存中查找缓存结果,如若发现匹配的响应则返回缓存的值,不然,将会调用fetch以代理发出网络请求,并将从网络中检索的数据做为结果返回;
- 若是但愿连续性缓存新的请求,则注意注释的代码部分,其经过cache.put来将请求的响应添加到缓存来实现;
- 在fetch请求中添加对then()的回调,得到响应后执行检查,并clone响应,注意这样处理的缘由是该响应是stream,主体只能使用一次,咱们须要返回能被浏览器使用的响应,还要传递到缓存以供使用,所以须要克隆一份副本;
// 拦截请求并响应
self.addEventListener('fetch', function (event) {
console.log('service worker: run into fetch');
event.respondWith(caches.match(event.request).then(function (response) {
// 发现匹配的响应缓存
if(response){
console.log('service worker 匹配并读取缓存:' + event.request.url);
return response;
}
console.log('没有匹配上:' + event.request.url);
return fetch(event.request);
/*var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(function(response){
if(!response || response.status !== 200 || response.type !== 'basic'){
return response;
}
var responseToCache = response.clone();
caches.open(cacheVersion).then(function (cache) {
console.log(cache);
cache.put(fetchRequest, responseToCache);
});
return response;
});*/
}));
});
3. 前端资源缓存演进
- 利用webview自身的http缓存机制。这里每每须要服务器运维同事配合,对于前端来说不够灵活且缓存粒度太粗,并且在http协议在不一样版本下缓存机制有必定的差别(例如1.0版本中If-Modified-Since、Last-Modified、expires, 1.1版本中对缓存进行了优化,添加If-None-Match、Etag、cache-control等;
- 离线包策略,其大体原理是经过将静态资源打包至离线管理平台(自行开发),在app启动时从离线管理平台拉取资源包并存放于本地,后续终端将会拦截url请求并基于约定规则将请求代理到本地文件系统,进而加快静态资源的访问以及为cdn减压,该方案的缺陷在于须要离线资源管理平台和终端的配合,牵扯资源过多,但其优势是不存在兼容性问题;
- h5离线缓存manifest,其实质就是一个缓存清单文件(xx.manifest),而后在html标签设置manifest属性为xx.manifest,该缓存方案也存在“二次更新”的问题,该方案须要注意的问题是xx.manifest文件自身不要被webview缓存,且manifest文件cache部分不能使用通配符,必须手动指定,不过好在能够经过构建工具来解决,主流浏览器对该方案支持度也不错。与service worker相对,其业务JS代码没法感知缓存更新的时机,因此service worker方案更具备想象空间;
- service worker 经过一个独立JS线程来实现资源的可编程式缓存;
4. 项目如何快速接入service worker
- 在接入前有两个问题摆在咱们面前,service worker能够帮助咱们解决资源缓存问题,有缓存就必需要有更新的机制,service-worker.js自己也会被浏览器缓存,后续产品迭代过程当中如何解决该文件自身的更新问题,不然其余资源的缓存更新也就无从谈起(旧的服务工做线程将一直控制页面),无可厚非每次构建部署时service-worker.js须要携带版本号(例如?v=201801021721),固然也能够在服务器运维层控制该文件的cache-control: no-cache从而规避浏览器缓存问题,但这样太麻烦;
-
咱们是在业务代码中经过register的方式引入service-worker.js, 那问题就变为如何在注册服务工做线程的位置引入版本号呢,咱们能够经过sw-register-webpack-plugin来解决该问题,其思路是将服务工做线程的注册放在一个单独的文件中(sw-register.js),而后自动在页面入口(例如index.html)写入一段JS脚原本动态加载sw-register.js文件,这里sw-register.js的加载路径是带有实时时间戳的,而生成的sw-register.js文件内容中注册service-worker.js的位置自动携带构建版本号参数(默认是当前构建时间),该插件配置以下(基于webpack构建的项目):
let SwRegisterWebpackPlugin = require('sw-register-webpack-plugin')
...
plugins: [
new SwRegisterWebpackPlugin({
filePath: path.resolve(__dirname, '../src/sw-register.js')
})
]
- 构建后html新增部分如图:

- 构建后生成的sw-register.js文件变化如图:

- 这样处理后,sw-register.js文件就不会被浏览器缓存,也即每次刷新会多一次sw-register.js的文件请求,因为它只是用来作注册的工做,体量不会太大,能够接受,关键是前端能够自行控制
-
已缓存资源文件如何更新呢?上述插件只是解决了service-worker.js文件自己的更新的问题(保证每次构建部署后会新启一个服务工做线程),但对于service-worker.js文件中定义的cacheFiles而言,当咱们修改了已缓存文件后如何来更新缓存呢,个人项目是基于vue.js + webpack,打包后的JS文件是[name].[hash].[ext]格式,从前面的介绍可知资源的缓存也是基于url(做为key)来的,不可能每次构建后都手动去调整service-worker.js文件内容中cacheFiles的路径值吧,应该是将构建后的文件名(包括路径)直接放到service-worker.js内容中,看到这里你应该想到了有webpack插件已经帮咱们作好了,那就是sw-precache-webpack-plugin,该插件会自动在dist目录下生成service-worker.js文件,供给service worker运行,也就是说service-worker.js文件自己不须要咱们手动添加了,但问题是咱们如何自定义须要缓存的文件呢,该插件的配置参数会告诉你,个人项目该插件配置以下:
// 生成service-worker.js和配置缓存清单
new SwPrecacheWebpackPlugin({
cacheId: 'attendance-mobile-cache',
filename: 'service-worker.js',
minify: true,
dontCacheBustUrlsMatching: false,
staticFileGlobs: [
'dist/static/js/manifest.**.*',
'dist/static/js/vendor.**.*',
'dist/static/js/app.**.*'
],
stripPrefix: 'dist/'
})
- 由上可知,咱们可以经过正则来匹配须要缓存的文件,这里特别要注意的是stripPrefix参数的使用,咱们配置的缓存文件路径是项目中的路径,但对于部署线上而言,咱们可能须要过滤前缀的部分路径(个人项目线上部署文件根目录下就是static等,因此须要过滤dist路径),最终该插件生成的service-worker.js文件如图所示(仅截取缓存文件清单部分代码)

4. 调试service worker
- 经过上述两个插件,咱们的service-worker接入工做基本完成,那接下来就是验证服务工做线程运行是否ok,经过chrome devTools(Application项)咱们能够很方面的查看当前服务工做线程的运行状况和已缓存了哪些文件,具体如何查看这里再也不介绍;
- 当首次运行 service worker 时咱们会发现要缓存的文件仍是走正常的网络请求,cache storage 下也看不到咱们的缓存项,由于服务工程线程也存在“二次生效”的机制(即便须要缓存的资源延迟加载),具体以下图所示:


- 经过刷新访问咱们能够看到,service worker 缓存文件已经生效,在network面板下自定义的缓存文件size项都显示为“from ServiceWorker”, 耗时也明显很低。在cache storage下面也能够看到已经缓存的文件列表,具体以下图所示:


- 接下来咱们更新service-worker.js文件来看下新服务工做线程如何工做,正如前面所讲新服务工做线程将会启动安装,但因为旧服务工做线程控制着页面,因此新服务工做线程将进入waiting状态,当当前打开的页面关闭时,旧服务工做线程将会被终止,新服务工做线程会得的控制权并触发activate事件,在开发过程当中咱们须要经过Chrome Devtools的skipWaiting或者勾选Updated on reload来强制激活新服务工做线程,具体以下图所示:

- 在开发过程当中咱们能够经过上述来了解新服务工做线程的更新流程,但在实际项目中咱们能够经过self.skipWaiting()跳过等待过程安装后直接激活,通常咱们在install事件中调用,具体可参见sw-precache-webpack-plugin生成的service-worker源代码。这会致使新服务工做线程将当前活动的工做线程逐出,skipWaiting()意味着新服务工做线程可能会控制使用较旧工做线程加载的页面,也就是页面获取的部分数据由旧工做线程处理,而新服务工做线程处理后来获取的数据,若是有问题就不要使用skipWaiting();
- 手动清理service worker缓存后刷新页面,在 Network 面板中,咱们会看到本应缓存文件的一组初始请求。以后是前面带有齿轮图标的第二轮请求,这些请求彷佛要获取相同的资源,“齿轮”图标表明这些请求来自服务工做线程,若是不unregsiter该服务工做线程,咱们会发现即便屡次刷新页面,Network 面板依然如此,其实也就是说资源没有再次缓存(由于服务工做线程已经安装且控制当前页面,刷新操做不会从新触发install事件,也就不会再次添加资源到缓存,除非unregister或者更新service-worker.js文件),具体以下图所示:


5. 异常回滚(注销)
-
某些场景下若是service worker使用出现异常,好比不一样页面间 service worker 控制的scope存在“重叠污染”的问题,那么咱们就须要紧急回滚(撤销)当前 service worker,在开发环境很好解决,咱们依然能够经过Chrome Devtools来进行unregister, 那么在线上环境已经有服务工做线程在运行的状况下呢,咱们须要在新上线版本的service worker注册前将被污染或者异常的service worker注销掉,具体代码以下:
if (navigator.serviceWorker) {
navigator.serviceWorker.getRegistrations().then(function (registrations) {
for (var item of registrations) {
if (item.scope === 'http://localhost/attendance-mobile/dist/') {
item.unregister();
}
}
// 注销掉污染 Service Worker 以后再从新注册...
});
}
备注:文中部份内容摘选自Google开发者文档