PWA之 Service worker
渐进式 Web 应用(Progressive Web Apps,也被称为 PWAs)是 Web 技术方面一项令人兴奋的创新。PWA 混合了多项技术,能够让 Web 应用的功能类似于原生移动应用。它为开发人员和用户带来的收益能够突破纯 Web 解决方案和纯原生解决方案的限制:
-
你只需要一个按照开放、标准 W3C Web 技术开发的应用,不需要开发单独的原生代码库;
-
用户在安装之前就能发现并尝试你的应用;
-
没有必要使用 AppStore,无需遵循复杂的规则或支付费用。应用程序会自动更新,无需用户交互;
-
用户会被提示“安装”,这样会添加一个图标到主屏幕上;
-
当启动的时候,PWA 会展现一个有吸引力的启动画面;
-
如果需要的话,浏览器的 chrome 选项可以进行修改,以便于提供全屏的体验;
-
基本文件会在本地缓存,所以 PWA 要比标准 Web 应用反应更快(它们甚至能够比原生应用更快);
-
安装是轻量级的,可能只需几 KB 的缓存数据;
-
所有的数据交换必须要通过安全的 HTTPS 连接来执行;
-
PWA 支持离线功能,当网络恢复后,数据会进行同步。
service worker 本意为渐进式网络应用(progressive web app, PWA)提供离线支持,使得网络应用可以离线运行。一旦启用 service worker ,则它可以根据开发者的缓存配置为用户缓存网站静态与动态资源,并截获用户的所有网络请求,并根据缓存配置来决定是从缓存还是网络获取相应资源,从而可以提高网页的加载速度。测试结果表明,一般能实现 4-5 倍的加速,最好的时候能够实现 10 倍的加速。使用 service worker 实现网站加速的优势有:
- 可以很轻易地实现静态与动态资源缓存,决定缓存空间的大小与缓存时间期限,可定制性高;
- 不需要服务端支持,只需要在本地生成 service worker 文件并上传就可以使用,特别适用于没有服务端的静态博客或网站;
- 配合 sw-precache ,只需要做好缓存配置, sw-precache 可以自动生成 service woker 文件,不需要自己实现缓存逻辑;
- 可以灵活为动态与静态资源,以及不同网址提供不同的缓存机制,并实现资源的动态更新,同样不需要自己编写代码
Service workers 作为一个独立的线程,运行环境和普通环境不同,本质上充当Web应用程序与浏览器之间的代理服务器。service worker可以:
- 后台消息传递
- 网络代理,转发请求,伪造响应
- 离线缓存
- 消息推送
- … …
在目前阶段,ServiceWorker的主要能力集中在网络代理和离线缓存上。具体的实现上,可以理解为ServiceWorker是一个能在网页关闭时仍然运行的WebWorker。
ServiceWorker的生命周期
一个ServiceWorker会经历:安装、激活、等待、销毁的阶段。
![](https://images2018.cnblogs.com/blog/1478530/201808/1478530-20180831215724159-1214177242.png)
这张图把ServiceWorker的声明周期分为了两部分,主线程中的状态和ServiceWorker子线程中的状态。子线程中的代码处在一个单独的模块中,当我们需要使用ServiceWorker时,按照如下的方式来加载:
Install
这个时候ServiceWorker处于Parsed解析阶段,ServiceWorker处于Installing安装阶段,主线程的registration的installing属性代表正在安装的ServiceWorker实例。如果之前没有serviceWorker实例,或者之前有serviceWorker实例但serviceWorker文件有更新,这时子线程中才会触发install事件,可以在install事件中指定缓存资源。
这里使用了Cache API来将资源缓存起来,同时使用e.waitUntil接手一个Promise来等待资源缓存成功,等到这个Promise状态成功后,ServiceWorker进入installed状态,意味着安装完毕。
const self = this;
const HOST_NAME = location.host;
const VERSION_NAME = 'CACHE-v5';
const CACHE_NAME = HOST_NAME + '-' + VERSION_NAME;
const CACHE_HOST = [HOST_NAME, '0.plus', 'icms.biyong.io'];
var cacheList = [ "./css/common.css", "./css/discover.css", "discover.html", ] const onInstall = function (event) { console.log('触发install事件--------'); // 打开一个缓存空间,将相关需要缓存的资源添加到缓存里面 event.waitUntil( caches .open(CACHE_NAME) .then(function (cache) { self.skipWaiting(); console.log('Install success---------------'); return cache.addAll(cacheList) }) ); }; // 当浏览器解析完sw文件时,serviceworker内部触发install事件 self.addEventListener('install', onInstall);
然而这个时候并不意味着这个ServiceWorker会立马进入Activate阶段,除非之前没有新的ServiceWorker实例,如果之前已有ServiceWorker,新的ServiceWorker安装完成后会延时激活并进入了waiting状态,浏览器仍在运行之前的ServiceWorker,那么需要满足如下任意一个条件,新的ServiceWorker才会进入Activate阶段:
- 在新的ServiceWorker线程代码里,使用了self.skipWaiting()
- 或者当用户导航到别的网页,因此释放了旧的ServiceWorker时候
- 或者指定的时间过去后,释放了之前的ServiceWorker
Activate
这个时候ServiceWorker的生命周期进入Activating阶段,ServiceWorker子线程接收到activate事件,这个时候通常做一些缓存清理工作,当e.waitUntil接收的Promise进入成功状态后,ServiceWorker的生命周期则进入activated状态。这个时候主线程中的registration的active属性代表进入activated状态的ServiceWorker实例。
const onActive = function (event) { event.waitUntil( caches .keys() .then(function (cacheNames) { return Promise.all( cacheNames.map(function (cacheName) { // active事件中通常做一些过期资源释放的工作 if (CACHE_NAME.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) .then(function () { console.log('active claim success 2-------------'); self.clients.claim(); }) ); }; // 如果当前浏览器没有激活的service worker或者已经激活的worker被解雇, // 新的service worker进入active事件 self.addEventListener('activate', onActive);
到此一个ServiceWorker正式进入激活状态,可以拦截网络请求了。如果主线程有fetch方式请求资源,那么就可以在ServiceWorker代码中触发fetch事件。
const onFetch = function (event) { event.respondWith(handleFetchRequest(event.request)); }; const handleFetchRequest = function (req) { const request = req; if (isCORSRequest(req.url, HOST_NAME)) { const request = new Request(req.url, {mode: 'no-cors'}); } return caches.match(request) .then(function (response) { if (response) { return response; } else { console.log('缓存没找到' + request.url); } }) const request = req; if (isCORSRequest(req.url, HOST_NAME)) { const request = new Request(req.url, {mode: 'no-cors'}); } return fetch(request) .then(function (response) { console.log(request.url + '请求回来了'); // online: Return the response if need if (isNeedCache(req) && isValidResponse(response)) { const clonedResponse = response.clone(); caches .open(CACHE_NAME) .then(function (cache) { cache.put(request, clonedResponse); console.log('添加缓存: ' + request.url); }) } return response; }) .catch(error => { // offline: Return the caches console.log('fetch ' + request.url + ' fail: ' + error.message); return caches.match(request) .then(function (response) { if (response) { return response; } else { console.log('缓存没找到' + request.url); } }) }) }; self.addEventListener('fetch', onFetch);
Asw-precache模块
sw-precache也是NODE中的一个模块,可以通过
npm install sw-precache
来进行安装。sw-precache可以配合多个工具使用,这里我主要介绍一下如何配合gulp来使用。我们通过利用sw-precache来生成service-worker.js文件。主要的方式是检测你在staticFileGlobs里面的文件有没有发生变化,如果发生变化就会重新生成hash码,自动更新service-worker.js文件。从而能够使得APP在代码更新之后,浏览器通过字节逐一解析到服务器的service-worker.js不一样了,缓存Cache中的数据才会重新向远程请求新的代码。
![](https://images2018.cnblogs.com/blog/1478530/201808/1478530-20180831215934936-1910767236.png)
缓存资源更新
(缓存问题)如果当前已有serviceWorker实例, 并将某些数据缓存在了cache中 当服务器上这些缓存的数据更新时,浏览器请求到的数据始终是serviceWorker的缓存cache中的旧内容。 只有服务器更新了service-worker.js文件,浏览器通过字节逐一解析到新的service-worker.js不一样了,缓存Cache中的数据才会重新更新。