PWA:
通过上一节的例子js13kPWA,我们了解了PWA的基本结构,并利用最基本的shell成功运行了程序,现在让我们看看如果利用Service Worker实现离线功能。在这篇文章中,我们将继续结合
js13kPWA(github源码)这个例子来讲述。
解释Service worker
Service worker是一个浏览器和网站间的虚拟协议(virtual proxy)。这项技术解决了困扰前端工程师多年的问题——特别是如何有效的缓存页面资源使得用户离线时也可以访问这样的问题。
Service worker在运行时独立于JavaScript主线程,且不对DOM进行访问。这和传统的网站开发不同——它的API是非阻塞式的,并且可以在不同的上下文(context)中相互通信。你可以让Service Worker运行起来,然后在其准备完毕时通过Promise-based的方式获得结果。
Service worker能实现的可不仅仅是提供离线功能,还包括了处理消息推送,在单独的线程上处理耗时大的计算等等。Service worker非常的强大,因为他们可以接管网络请求(request),并通过缓存来定制化响应(response),甚至是合成响应。
安全性
因为这项技术过于强大,所以Service Worker只能运行在安全的上下文中(这里指HTTPS)。如果你仅仅是想试验一下而不是部署生产环境,你也可以在localhost或者GitHub Pages上进行测试——都支持HTTPS。
离线优先(Offline First)
“离线优先”——或者说“缓存优先”——这是提供内容给用户的最流行策略。如果有资源被缓存或者离线可用,记得从服务器下载内容之前先返回这些资源。如果尚未缓存,那么先下载再缓存以便将来可用。
PWA中的渐进式
如果通过渐进式的方式来实现你的网站,那么service workers可以让那些使用现代浏览器(支持Service worker API)的用户享受到离线缓存好处,同时不会让那些使用了旧浏览器的用户有糟糕的体验。
渐进式表示要从最基本的功能开始实现,需要假设用户的浏览器的版本最低(通常是IE6?),在满足了基本需要的情况下,一步步向现代浏览器靠近,实现更高级的功能。
js13kPWA应用中的Service workers
理论知识到此为止,我们来看下源码!
注册Service Worker
第一步,从注册一个新的Service Worker开始,在app.js中:
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('/pwa-examples/js13kpwa/sw.js');
};
如果浏览器支持service worker的API,就会使用ServiceWorkerContainer.register()来进行注册。具体的内容我们放在sw.js中,当注册成功后就会被执行。所有关于Service Worker的代码都会存放在sw.js中,所以app.js中只有这么一小段关于Service Worker的代码。
Service Worker的生命周期
当注册完成,sw.js会被自动下载,安装和激活。
安装
API允许我们给某些特定的事件增加监听器——第一个就是install
事件:
self.addEventListener('install', function(e) {
console.log('[Service Worker] Install');
});
在install
监听器中,我们可以初始化缓存,将文件添加进缓存已备离线时使用。正如js13kPWA所做的那样。
首先,需要一个缓存名,app shell的文件以数组的形式存储。
var cacheName = 'js13kPWA-v1';
var appShellFiles = [
'/pwa-examples/js13kpwa/',
'/pwa-examples/js13kpwa/index.html',
'/pwa-examples/js13kpwa/app.js',
'/pwa-examples/js13kpwa/style.css',
'/pwa-examples/js13kpwa/fonts/graduate.eot',
'/pwa-examples/js13kpwa/fonts/graduate.ttf',
'/pwa-examples/js13kpwa/fonts/graduate.woff',
'/pwa-examples/js13kpwa/favicon.ico',
'/pwa-examples/js13kpwa/img/js13kgames.png',
'/pwa-examples/js13kpwa/img/bg.png',
'/pwa-examples/js13kpwa/icons/icon-32.png',
'/pwa-examples/js13kpwa/icons/icon-64.png',
'/pwa-examples/js13kpwa/icons/icon-96.png',
'/pwa-examples/js13kpwa/icons/icon-128.png',
'/pwa-examples/js13kpwa/icons/icon-168.png',
'/pwa-examples/js13kpwa/icons/icon-192.png',
'/pwa-examples/js13kpwa/icons/icon-256.png',
'/pwa-examples/js13kpwa/icons/icon-512.png'
];
所有需要加载的图片的信息都来自于data/games.js,用这些信息生成第二个数组。然后通过Array.prototype.concat()合并两个数组。
var gamesImages = [];
for(var i=0; i<games.length; i++) {
gamesImages.push('data/img/'+games[i].slug+'.jpg');
}
var contentToCache = appShellFiles.concat(gamesImages);
接下来我们就可以管理install
了:
self.addEventListener('install', function(e) {
console.log('[Service Worker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[Service Worker] Caching all: app shell and content');
return cache.addAll(contentToCache);
})
);
});
这里需要解释两件事:ExtendableEvent.waitUntil做了什么,以及什么是caches
对象。
service worker会在waitUntil
内的代码全部执行完才进行安装。其返回了一个promise——这是必要的,因为通常安装需要花费一些时间,所以我们需要稍作等待才行。
caches
是一个特殊的CacheStorage对象,其在Service Worker的作用域中均可用——注意由于web storage是同步的,所以保存到web storage是不行的。因此在Service Worker中,我们使用Cache API。
这样,我们通过一个cache名打开了一个缓存,并将所有应用将用到的文件都添加进了缓存,这样下次加载时这些缓存就可以派上用场了(由request URL来识别)。
激活
还有一个叫做activate
的事件,和install
的用法类似。通常这个事件用来清理一些不再需要的文件。这次我们先跳过。
响应fetches
我们还需要处理一个叫做fetch
的事件,每当有HTTP请求到来时就会被触发。这非常有用,因为它允许我们打断请求,并用自定义的响应来回复。一个简单的用例:
self.addEventListener('fetch', function(e) {
console.log('[Service Worker] Fetched resource '+e.request.url);
});
响应可以是任意的形式:请求的文件,缓存的拷贝,一段执行特殊任务的JavaScript代码——我们实际上拥有了无限的可能性。
在我们的例子中,只要缓存中存在内容,我们就返回缓存中的内容。无论在线还是离线返回值都是如此。如果文件不在缓存中,则先将其加入缓存再进行处理。
self.addEventListener('fetch', function(e) {
e.respondWith(
caches.match(e.request).then(function(r) {
console.log('[Service Worker] Fetching resource: '+e.request.url);
return r || fetch(e.request).then(function(response) {
return caches.open(cacheName).then(function(cache) {
console.log('[Service Worker] Caching new resource: '+e.request.url);
cache.put(e.request, response.clone());
return response;
});
});
})
);
});
对于一个fetch事件,如果在缓存中有对应的资源则直接返回其内容。否则,我们向服务器发起请求,并将得到的响应存入缓存中,这样下次有请求时缓存就可以派上用场了。
FetchEvent.respondWith方法获取了控制权——这相当于创建了一个应用和网络之间的代理服务器。对于每一个请求,我们都可以返回任何我们希望的值:由Service Worker准备,从缓存中取得,必要时进行修改。
就是这些了!我们的应用在安装时缓存资源,然后当用户请求时直接从缓存获取资源来响应请求,这样就保证了即使用户在离线状态下也可以使用应用。当然,我们也可以在之后缓存新的内容。
更新
还有一点需要说明的:当有新版本的app可用时如何升级Service Worker?缓存名中的版本号是这里的关键所在:
var cacheName = 'js13kPWA-v1';
当升级为v2版时,我们可以将所有的文件(包括新文件)添加进一个新的缓存中:
contentToCache.push('/pwa-examples/js13kpwa/icons/icon-32.png');
// ...
self.addEventListener('install', function(e) {
e.waitUntil(
caches.open('js13kPWA-v2').then(function(cache) {
return cache.addAll(contentToCache);
})
);
});
一个新的service worker将在后台被安装,旧版本(v1)的service worker将正常工作到没有任何页面使用它为止——这时新的Service Worker将被激活并从旧的service worker那里获得控制权。
清除缓存
还记得我们之前跳过的activate
事件吗?它可以用来清除我们不需要的旧缓存:
self.addEventListener('activate', function(e) {
e.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if(cacheName.indexOf(key) === -1) {
return caches.delete(key);
}
}));
})
);
});
这保证了缓存中永远保存有用的信息,浏览器可用缓存空间的限制的存在,促使我们随时清理不要的缓存。
其他使用场景
Service Worker不仅仅可以处理缓存。如果你需要进行耗时的计算,你可以将其从主线程上卸载下来,并在worker中处理这些计算,并在计算完成时获取结果。在性能方面,你也可以预加载尚未用到的资源,这样在未来用到这些资源时应用可以快速的响应结果。
总结
在这篇文章中我们用service workers让你的PWA应用实现了离线工作。如果你想学习更多关于Service Worker API的内容,请查看后续的文章。
Service Worker通常也用于处理消息推送——随后介绍。
网友评论