1.PWA与ServiceWorker的关系PWA(ProgressiveWebApps)不是一种技术,也不是一个框架,我们可以把她理解为一种模型,一种从安全性、性能、体验等方面逐步提升WebApp的技术。Web应用程序模型。对于webview来说,ServiceWorker是一个独立于js主线程的WebWorker线程,一个独立于主线程的Context,但是对于开发者来说,ServiceWorker的形式其实是一个需要开发者维护的文件。假设此文件名为sw.js。通过serviceworker,我们可以代理webview的请求,相当于一个正向代理线程,fiddler也做这些事情)。在特定路径上注册serviceworker后,可以拦截并处理该路径下的所有网络请求,进而实现页面资源。可编程缓存在弱网或无网的情况下带来流畅的产品体验,因此ServiceWorker可以看作是PWA模式的一种技术实现。2、serviceworker介绍注意事项serviceworker是一个JS工作线程,不能直接访问DOM。线程通过postMessage接口消息与自己控制的页面进行通信;serviceworker广泛使用Promise,在以下代码示例中显示;目前并不是所有的主流浏览器都支持serviceworker,可以使用navigator&&navigator.serviceWorker进行特征检测;在开发过程中,可以通过localhost使用serviceworkerthreads,如果在线部署,必须使用https访问注册serviceworkerthread的页面,但是有一种场景我们的测试环境可能不支持https,那么我们需要通过修改host文件将localhost指向测试环境ip(例如:192.168.22.144localhost)来绕过这个问题;生命周期ServiceWorker的生命周期完全独立于网页。为网站安装serviceworker线程,我们需要在页面业务js代码中注册。浏览器从指定路径下载并解析serviceworker线程脚本,浏览器会在后台启动安装步骤。在安装过程中,我们通常会缓存静态资源。如果成功缓存所有文件,则安装服务工程线程。如果任何文件下载或缓存失败,那么安装步骤就会失败,当然也不会被激活。安装之后是激活步骤,这是管理旧缓存的绝佳机会(原因将在稍后的代码示例中解释),激活后,serviceworker将开始对其范围内的所有页面进行控制。这里需要注意的是,第一次注册serviceworker线程的页面需要重新加载,才能被它控制。在成功安装完成并激活之前,服务工程线程不会接收到fetch和push事件;这里的workflow注册需要注意register方法的位置是注册serviceworker线程文件,路径是默认serviceworker的范围,比如注册的路径是/a/b/service-worker。js,默认范围是/a/b/。当然你也可以通过传入{scope:'/a/b/c/'}来指定你自己的scope。但是这里要特别注意,传入的scope参数必须自定义在默认scope内(比如/a/b/c/),否则自定义为/d/e是不行的/;一般来说,上述范围是serviceworker可以控制和发挥作用的范围;注意注册是在自己的业务代码中进行的,后面会有代码示例通过插件实现注册;if(navigator&&navigator.serviceWorker){navigator.serviceWorker.register('/service-worker.js').then(function(registration){console.log(registration)}).catch(function(err){控制台。log(err)})}install下面的代码是之前注册的service-worker.js文件的内容;我们通过install事件定义安装步骤,通过缓存名称调用caches.open(),然后调用cache.addAll()并传入具体的缓存文件列表数组,这是一个Promise链事件.waitUntil()带有Promise参数的方法,并使用它来确定耗时和成功的安装;前面提到,如果在安装过程中manifest中的所有文件都被成功缓存,则安装结束,否则认为安装过程失败,所以在实践中我们尽可能缓存核心资源,避免serviceworker线程安装失败;varcacheVersion='test_2017122608';//安装serviceworker线程self.addEventListener('install',function(event){//需要缓存的资源varcacheFiles=['/dist/index.html','/dist/js/index_async_bundle.js'];console.log('serviceworker:运行到install');event.waitUntil(caches.open(cacheVersion).then(function(cache){returncache.addAll(cacheFiles);}));});Activation在某个时间点,服务工程线程需要更新(例如:service-worker.js文件变更上线),当用户访问页面时,浏览器会尝试重新下载服务-worker.js在后台。如果服务工程线程文件与当前使用的文件存在字节差异,则视为“新建服务工作线程”;新的服务工作者线程将启动并触发安装事件;此时,旧的ServiceWorker线程仍会控制当前页面,因此新的ServiceWorker线程会进入等待状态;当网站当前页面关闭时,旧的ServiceWorker线程会终止,新的ServiceWorker线程会得到Control;新的serviceworker线程接管后,会触发activate事件;监听激活事件的回调函数中的共同任务是管理缓存。我之前也提到过,这是管理旧缓存的绝佳时机,因为如果在安装步骤中,旧缓存被清理干净。因为旧的serviceworker线程仍然控制着页面,所以不会从缓存中取文件,但是旧的serviceworker线程在activate时已经终止了对页面的控制,所以这里清理旧的缓存。合适;//新的serviceworker线程被激活(其实和离线包一样有“二次验证”机制)self.addEventListener('activate',function(event){console.log('serviceworker:runintoactivate');event.waitUntil(caches.keys().then(function(cacheNames){returnPromise.all(cacheNames.map(function(cacheName){//这里注意cacheVersion也可以是一个数组if(cacheName!==cacheVersion){console.log('serviceworker:clearcache'+cacheName);返回缓存。删除(缓存名称);}}));}));});这里监听通过监听fetch事件来代理响应,进而实现自定义前端资源缓存;在event.respondWith()中,我们传入了一个来自caches.match()的promise,它会拦截请求并从serviceworker线程创建它在任意缓存中查找缓存的结果,如果找到匹配的响应则返回缓存的值,否则,将调用fetch来代理网络请求,并返回从网络中检索到的数据作为结果;如果要不断缓存新的请求,那么注意注释的代码部分,是通过cache.put将请求的响应添加到缓存中实现的;fetch请求中then()添加回调,获取response后进行校验,并克隆response,注意这个处理原因是response是stream,body只能使用一次。我们需要返回一个浏览器可以使用的响应,并传递给缓存使用,所以我们需要克隆一份;//拦截请求并响应self.addEventListener('fetch',function(event){console.log('serviceworker:runintofetch');event.respondWith(caches.match(event.request).then(function(response){//找到匹配的响应缓存if(response){console.log('serviceworker匹配并读取缓存:'+event.request.url);returnresponse;}console.log('Nomatch:'+event.request.url);returnfetch(event.要求);/*varfetchRequest=event.request.clone();returnfetch(fetchRequest).then(function(response){if(!response||response.status!==200||response.type!=='basic'){返回响应;}varresponseToCache=response.clone();caches.open(cacheVersion).then(function(cache){console.log(cache);cache.put(fetchRequest,responseToCache);});返回响应;});*/}));});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的本质是一个缓存manifest文件(xx.manifest),然后在html标签中设置manifest属性为xx.manifest,这种缓存方案也存在“二次更新”的问题。该方案需要注意的问题是xx.manifest文件本身不能被webview缓存,manifest文件的缓存部分不能使用通配符,必须手动指定。现在可以通过构建工具来解决,主流浏览器也对该方案有很好的支持。与ServiceWorker相比,其业务JS代码无法感知缓存更新的时机,因此ServiceWorker的方案更具想象力;serviceworker通过独立的JS线程实现资源的可编程缓存;4、如何快速访问项目的serviceworker在访问之前,有两个问题摆在我们面前。ServiceWorker可以帮助我们解决资源缓存的问题。有缓存就一定有更新机制。service-worker.js本身也会被浏览器缓存,后续产品迭代过程如何解决文件本身的更新问题,否则其他资源的缓存更新无从谈起(老serviceworker线程会一直控制页面),可以理解service-worker.js每次构建部署时都需要携带版本号(比如?v=201801021721),当然你也可以控制cache-control:服务器运维层不缓存文件,避免浏览器缓存问题,但是这样太麻烦;我们引入service-worker.js,那么问题就变成了如何在注册serviceworker线程的位置引入版本号。我们可以通过sw-register-webpack-plugin来解决这个问题。思路是将serviceworker线程的注册放在一个单独的文件中(sw-register.js),然后在页面入口处自动写一个JS脚本(如index.html)动态加载sw-register。js文件,其中sw-register.js的加载路径带有实时Timestamp,生成的sw-register.js文件内容中注册service-worker.js的位置自动携带构建版本号参数(默认为当前构建时间)。插件配置如下(基于webpack搭建的项目):../src/sw-register.js')})]构建后新增的部分html如图:构建后生成的sw-register.js文件变化如图:经过这次处理,sw-register.js该文件不会被浏览器缓存,即每次刷新都会多请求一次sw-register.js文件。由于只是注册用,体积不会太大,可以接受。关键是前端能不能自己控制缓存资源文件的更新呢?上面的插件只是解决了service-worker.js文件本身的更新问题(保证每次构建部署后都会启动一个新的serviceworker线程),但是对于service-worker.js中定义的cacheFiles文件,当我们修改缓存文件缓存完成后如何更新缓存?我的项目基于vue.js+webpack。打包后的JS文件格式为[name].[hash].[ext]。从前面的介绍我们可以看出,资源缓存也是基于url(作为key)的,不可能在每次build之后手动调整service-worker.js文件内容中cacheFiles的path值。应该是将构建好的文件名(包括路径)直接放到service-worker.js文件中。在worker.js的内容中,看到这里,你应该想到有一个webpack插件已经帮我们做好了,那就是sw-precache-webpack-plugin,这个插件会自动生成service-worker。dist目录下的js文件,提供serviceworker运行,也就是说service-worker.js文件本身不需要手动添加,但问题是我们如何自定义需要缓存的文件呢?插件的配置参数会告诉你,我的项目的插件配置如下://生成service-worker.js并配置缓存清单newSwPrecacheWebpackPlugin({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参数的使用。我们配置的缓存文件的路径是项目中的路径,但是对于部署行,我们可能需要过滤部分前缀的路径(我的项目行部署文件的根目录是静态的等等,所以dist路径需要过滤),最后插件生成的service-worker.js文件如图所示(只截取缓存文件列表的部分代码)4.调试serviceworker通过以上两个插件,我们的service-worker的接入工作就基本完成了,接下来就是验证serviceworker线程是否运行ok了。通过chromedevTools(Application项),我们可以查看当前serviceworker线程的运行状态,以及缓存了哪些文件。如何查看这里就不重复介绍了;当我们第一次运行serviceworker的时候,我们会发现要缓存的文件还是经过正常的网络请求,缓存存储下看不到我们的缓存项,因为服务工程线程还有一个“二次验证””机制(即使延迟加载需要缓存的资源),如下图:通过刷新访问,我们可以看到serviceworker缓存文件已经生效,网络下的自定义缓存文件大小项panel显示为“fromServiceWorker”,耗时也明显很低。还可以在缓存存储下看到缓存文件列表,如下图所示:接下来我们更新service-worker.js文件,看看新的serviceworker线程是如何工作的,就像上面说的新serviceworkerthread会开始安装,但是由于老的serviceworker线程控制着页面,新的serviceworker线程会进入等待状态,当当前打开的页面关闭时,老的serviceworker线程会终止,新的serviceworker线程将获得控制权并触发激活事件。在开发过程中,我们需要使用ChromeDevtools的skipWaiting或者勾选Updatedonreload来强行激活新的serviceworker线程,如下图所示:在开发过程中,我们可以通过上面的了解新的serviceworker线程的更新过程,但在实际项目中,我们可以通过self.skipWaiting()跳过等待过程,安装后直接激活。一般我们在install事件中调用。具体可以参考sw-precache-webpack-plugin-worker源码生成的服务。这将导致新的服务工作线程驱逐当前活动的工作线程。skipWaiting()表示新的serviceworker线程可能会控制使用olderworker线程加载的页面,即页面获取的部分数据由oldworker线程处理。而新的serviceworker线程处理后面获取到的数据。如果有问题,不要使用skipWaiting();手动清除serviceworker缓存并刷新页面。在网络面板中,我们将看到一组应该有缓存文件的初始请求。然后是第二轮请求,前面有一个齿轮图标。这些请求似乎获得了相同的资源。“齿轮”图标表示这些请求来自服务工作者线程。如果serviceworker线程没有注销,我们会发现即使多次刷新页面,Network面板还是老样子,实际上并没有再次缓存资源(因为serviceworker线程已经安装并控制了当前页面,刷新操作不会重新触发install事件,也不会再次向缓存添加资源,除非注销或更新service-worker.js文件),如下图:5.异常回滚(登出)。我们需要紧急回滚(撤销)当前的serviceworker。在开发环境中很容易解决。我们仍然可以通过ChromeDevtools注销。那么,如果线上环境中已经有serviceworker线程在运行,我们需要在新环境中新建一个serviceworker。在线版ServiceWorker注册前,被污染或异常的ServiceWorker会被注销。具体代码如下:localhost/attendance-mobile/dist/'){item.unregister();}}//注销污染ServiceWorker,然后重新注册...});}备注:本文部分内容摘自谷歌开发者文档
