PWA:
通過上一節(jié)的例子js13kPWA,我們了解了PWA的基本結(jié)構(gòu),并利用最基本的shell成功運行了程序,現(xiàn)在讓我們看看如果利用Service Worker實現(xiàn)離線功能。在這篇文章中,我們將繼續(xù)結(jié)合
js13kPWA(github源碼)這個例子來講述。
解釋Service worker
Service worker是一個瀏覽器和網(wǎng)站間的虛擬協(xié)議(virtual proxy)。這項技術(shù)解決了困擾前端工程師多年的問題——特別是如何有效的緩存頁面資源使得用戶離線時也可以訪問這樣的問題。
Service worker在運行時獨立于JavaScript主線程,且不對DOM進行訪問。這和傳統(tǒng)的網(wǎng)站開發(fā)不同——它的API是非阻塞式的,并且可以在不同的上下文(context)中相互通信。你可以讓Service Worker運行起來,然后在其準(zhǔn)備完畢時通過Promise-based的方式獲得結(jié)果。
Service worker能實現(xiàn)的可不僅僅是提供離線功能,還包括了處理消息推送,在單獨的線程上處理耗時大的計算等等。Service worker非常的強大,因為他們可以接管網(wǎng)絡(luò)請求(request),并通過緩存來定制化響應(yīng)(response),甚至是合成響應(yīng)。
安全性
因為這項技術(shù)過于強大,所以Service Worker只能運行在安全的上下文中(這里指HTTPS)。如果你僅僅是想試驗一下而不是部署生產(chǎn)環(huán)境,你也可以在localhost或者GitHub Pages上進行測試——都支持HTTPS。
離線優(yōu)先(Offline First)
“離線優(yōu)先”——或者說“緩存優(yōu)先”——這是提供內(nèi)容給用戶的最流行策略。如果有資源被緩存或者離線可用,記得從服務(wù)器下載內(nèi)容之前先返回這些資源。如果尚未緩存,那么先下載再緩存以便將來可用。
PWA中的漸進式
如果通過漸進式的方式來實現(xiàn)你的網(wǎng)站,那么service workers可以讓那些使用現(xiàn)代瀏覽器(支持Service worker API)的用戶享受到離線緩存好處,同時不會讓那些使用了舊瀏覽器的用戶有糟糕的體驗。
漸進式表示要從最基本的功能開始實現(xiàn),需要假設(shè)用戶的瀏覽器的版本最低(通常是IE6?),在滿足了基本需要的情況下,一步步向現(xiàn)代瀏覽器靠近,實現(xiàn)更高級的功能。
js13kPWA應(yīng)用中的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()來進行注冊。具體的內(nèi)容我們放在sw.js中,當(dāng)注冊成功后就會被執(zhí)行。所有關(guān)于Service Worker的代碼都會存放在sw.js中,所以app.js中只有這么一小段關(guān)于Service Worker的代碼。
Service Worker的生命周期
當(dāng)注冊完成,sw.js會被自動下載,安裝和激活。
安裝
API允許我們給某些特定的事件增加監(jiān)聽器——第一個就是install事件:
self.addEventListener('install', function(e) {
console.log('[Service Worker] Install');
});
在install監(jiān)聽器中,我們可以初始化緩存,將文件添加進緩存已備離線時使用。正如js13kPWA所做的那樣。
首先,需要一個緩存名,app shell的文件以數(shù)組的形式存儲。
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,用這些信息生成第二個數(shù)組。然后通過Array.prototype.concat()合并兩個數(shù)組。
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內(nèi)的代碼全部執(zhí)行完才進行安裝。其返回了一個promise——這是必要的,因為通常安裝需要花費一些時間,所以我們需要稍作等待才行。
caches是一個特殊的CacheStorage對象,其在Service Worker的作用域中均可用——注意由于web storage是同步的,所以保存到web storage是不行的。因此在Service Worker中,我們使用Cache API。
這樣,我們通過一個cache名打開了一個緩存,并將所有應(yīng)用將用到的文件都添加進了緩存,這樣下次加載時這些緩存就可以派上用場了(由request URL來識別)。
激活
還有一個叫做activate的事件,和install的用法類似。通常這個事件用來清理一些不再需要的文件。這次我們先跳過。
響應(yīng)fetches
我們還需要處理一個叫做fetch的事件,每當(dāng)有HTTP請求到來時就會被觸發(fā)。這非常有用,因為它允許我們打斷請求,并用自定義的響應(yīng)來回復(fù)。一個簡單的用例:
self.addEventListener('fetch', function(e) {
console.log('[Service Worker] Fetched resource '+e.request.url);
});
響應(yīng)可以是任意的形式:請求的文件,緩存的拷貝,一段執(zhí)行特殊任務(wù)的JavaScript代碼——我們實際上擁有了無限的可能性。
在我們的例子中,只要緩存中存在內(nèi)容,我們就返回緩存中的內(nèi)容。無論在線還是離線返回值都是如此。如果文件不在緩存中,則先將其加入緩存再進行處理。
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事件,如果在緩存中有對應(yīng)的資源則直接返回其內(nèi)容。否則,我們向服務(wù)器發(fā)起請求,并將得到的響應(yīng)存入緩存中,這樣下次有請求時緩存就可以派上用場了。
FetchEvent.respondWith方法獲取了控制權(quán)——這相當(dāng)于創(chuàng)建了一個應(yīng)用和網(wǎng)絡(luò)之間的代理服務(wù)器。對于每一個請求,我們都可以返回任何我們希望的值:由Service Worker準(zhǔn)備,從緩存中取得,必要時進行修改。
就是這些了!我們的應(yīng)用在安裝時緩存資源,然后當(dāng)用戶請求時直接從緩存獲取資源來響應(yīng)請求,這樣就保證了即使用戶在離線狀態(tài)下也可以使用應(yīng)用。當(dāng)然,我們也可以在之后緩存新的內(nèi)容。
更新
還有一點需要說明的:當(dāng)有新版本的app可用時如何升級Service Worker?緩存名中的版本號是這里的關(guān)鍵所在:
var cacheName = 'js13kPWA-v1';
當(dāng)升級為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將正常工作到?jīng)]有任何頁面使用它為止——這時新的Service Worker將被激活并從舊的service worker那里獲得控制權(quán)。
清除緩存
還記得我們之前跳過的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中處理這些計算,并在計算完成時獲取結(jié)果。在性能方面,你也可以預(yù)加載尚未用到的資源,這樣在未來用到這些資源時應(yīng)用可以快速的響應(yīng)結(jié)果。
總結(jié)
在這篇文章中我們用service workers讓你的PWA應(yīng)用實現(xiàn)了離線工作。如果你想學(xué)習(xí)更多關(guān)于Service Worker API的內(nèi)容,請查看后續(xù)的文章。
Service Worker通常也用于處理消息推送——隨后介紹。