Progressive Web Apps 是快速且可安裝的,這意味着它能在在線、離線、斷斷續續或者緩慢的網絡環境下使用。為了實現這個目標,我們需要使用一個 service worker 來緩存應用外殼,以保證它能始終迅速可用且可靠。
如果你對 service workers 不熟悉,你可以通過閱讀 介紹 Service Workers 來了解關於它能做什么,它的生命周期是如何工作的等等知識。
service workers 提供的是一種應該被理解為漸進增強的特性,這些特性僅僅作用於支持service workers 的瀏覽器。比如,使用 service workers 你可以緩存應用外殼和你的應用所需的數據,所以這些數據在離線的環境下依然可以獲得。如果瀏覽器不支持 service workers ,支持離線的 代碼沒有工作,用戶也能得到一個基本的用戶體驗。使用特性檢測來漸漸增強有一些小的開銷,它不會在老舊的不支持 service workers 的瀏覽器中產生破壞性影響。
注冊 service worker
為了讓應用離線工作,要做的第一件事是注冊一個 service worker,一段允許在后台運行的腳本,不需要 用戶打開 web 頁面,也不需要其他交互。
這只需要簡單兩步:
- 創建一個 JavaScript 文件作為 service worker
- 告訴瀏覽器注冊這個 JavaScript 文件為 service worker
第一步,在你的應用根目錄下創建一個空文件叫做 service-worker.js
。這個 service-worker.js
文件必須放在跟目錄,因為 service workers 的作用范圍是根據其在目錄結構中的位置決定的。
接下來,我們需要檢查瀏覽器是否支持 service workers,如果支持,就注冊 service worker,將下面代碼添加至app.js
中。
if('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then(function() { console.log('Service Worker Registered'); });
}
緩存站點的資源
當 service worker 被注冊以后,當用戶首次訪問頁面的時候一個 install
事件會被觸發。在這個事件的回調函數中,我們能夠緩存所有的應用需要再次用到的資源。
當 service worker 被激活后,它應該打開緩存對象並將應用外殼需要的資源存儲進去。將下面這些代碼加入你的service-worker.js
(你可以在your-first-pwapp-master/work
中找到) :
var cacheName = 'weatherPWA-step-6-1';
var filesToCache = [];
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
})
);
});
首先,我們需要提供一個緩存的名字並利用 caches.open()
打開 cache 對象。提供的緩存名允許我們給 緩存的文件添加版本,或者將數據分開,以至於我們能夠輕松地升級數據而不影響其他的緩存。
一旦緩存被打開,我們可以調用 cache.addAll()
並傳入一個 url 列表,然后加載這些資源並將響應添加至緩存。不幸的是 cache.addAll()
是原子操作,如果某個文件緩存失敗了,那么整個緩存就會失敗!
好的。讓我們開始熟悉如何使用DevTools並學習如何使用DevTools來調試service workers。在刷新你的網頁前,開啟DevTools,從 Application 的面板中打開 Service Worker 的窗格。它應該是這樣的:
當你看到這樣的空白頁,這意味着當前打開的頁面沒有已經被注冊的Service Worker。
現在,重新加載頁面。Service Worker的窗格應該是這樣的:
當你看到這樣的信息,這意味着頁面有個Service Worker正在運行。
現在讓我們來示范你在使用Service Worker時可能會遇到的問題。為了演示, 我們將把service-worker.js里的install 的事件監聽器的下面添加在activate 的事件監聽器。
self.addEventListener('activate', function(e) {
console.log('[ServiceWorker] Activate');
});
當 service worker 開始啟動時,這將會發射activate事件。
打開DevTools並刷新網頁,切換到應用程序面板的Service Worker窗格,在已被激活的Service Worker中單擊inspect。理論上,控制台將會出現[ServiceWorker] Activate的信息,但這並沒有發生。現在回去Service Worker窗格,你會發現到新的Service Worker是在“等待”狀態。
簡單來說,舊的Service Worker將會繼續控制該網頁直到標簽被關閉。因此,你可以關閉再重新打開該網頁或者點擊 skipWaiting 的按鈕,但一個長期的解決方案是在DevTools中的Service Worker窗格啟用 Update on Reload 。當那個復選框被選擇后,當每次頁面重新加載,Service Worker將會強制更新
啟用 update on reload 復選框並重新加載頁面以確認新的Service Worker被激活。
Note: 您可能會在應用程序面板里的Service Worker窗格中看到類似於下面的錯誤信息,但你可以放心的忽略那個錯誤信息。
Ok, 現在讓我們來完成activate 的事件處理函數的代碼以更新緩存。
self.addEventListener('activate', function(e) {
console.log('[ServiceWorker] Activate');
e.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
console.log('[ServiceWorker] Removing old cache', key);
if (key !== cacheName) {
return caches.delete(key);
}
}));
})
);
});
確保在每次修改了 service worker 后修改 cacheName,這能確保你永遠能夠從緩存中獲得到最新版本的文件。過一段時間清理一下緩存刪除掉沒用的數據也是很重要的。
最后,讓我們更新一下 app shell 需要的緩存的文件列表。在這個數組中,我們需要包括所有我們的應用需要的文件,其中包括圖片、JavaScript以及樣式表等等。
var filesToCache = [
'/',
'/index.html',
'/scripts/app.js',
'/styles/inline.css',
'/images/clear.png',
'/images/cloudy-scattered-showers.png',
'/images/cloudy.png',
'/images/fog.png',
'/images/ic_add_white_24px.svg',
'/images/ic_refresh_white_24px.svg',
'/images/partly-cloudy.png',
'/images/rain.png',
'/images/scattered-showers.png',
'/images/sleet.png',
'/images/snow.png',
'/images/thunderstorm.png',
'/images/wind.png'
];
我么的應用目前還不能離線工作。我們緩存了 app shell 的組件,但是我們仍然需要從本地緩存中加載它們。
從緩存中加載 app sheel
Service workers 可以截獲 Progressive Web App 發起的請求並從緩存中返回響應。這意味着我們能夠 決定如何來處理這些請求,以及決定哪些網絡響應能夠成為我們的緩存。
比如:
self.addEventListener('fetch', function(event) {
// Do something interesting with the fetch here
});
讓我們來從緩存中加載 app shell。將下面代碼加入 service-worker.js 中:
self.addEventListener('fetch', function(e) {
console.log('[ServiceWorker] Fetch', e.request.url);
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
});
從內至外,caches.match() 從網絡請求觸發的 fetch 事件中得到請求內容,並判斷請求的資源是 否存在於緩存中。然后以緩存中的內容作為響應,或者使用 fetch 函數來加載資源(如果緩存中沒有該資源)。 response 最后通過 e.respondWith() 返回給 web 頁面。
測試
你的應用程序現在可以在離線下使用了! 讓我們來試試吧!
先刷新那個網頁, 然后去DevTools里的 Cache Storage 窗格中的 Application 面板上。展開該部分,你應該會在左邊看到您的app shell緩存的名稱。當你點擊你的appshell緩存,你將會看到所有已經被緩存的資源。
現在,讓我們測試離線模式。回去DevTools中的 Service Worker 窗格,啟用 Offline 的復選框。啟用之后,你將會在 Network 窗格的旁邊看到一個黃色的警告圖標。這表示您處於離線狀態。
刷新網頁,然后你會發現你的網頁仍然可以正常操作!
下一步驟是修改該應用程序和service worker的邏輯,讓氣象數據能夠被緩存,並能在應用程序處於離線狀態,將最新的緩存數據顯示出來。
Tip: 如果你要清除所有保存的數據(localStoarge,IndexedDB的數據,緩存文件),並刪除任何的service worker,你可以在DevTools中的Application 面板里的Clear storage清除。
當心邊緣問題
之前提到過,這段代碼 一定不要用在生產環境下 ,因為有很多沒有處理的邊界情況。
緩存依賴於每次修改內容后更新緩存名稱
比如緩存方法需要你在每次改變內容后更新緩存的名字。否則,緩存不會被更新,舊的內容會一直被緩存返回。 所以,請確保每次修改你的項目后更新緩存名稱。
每次修改后所有資源都需要被重新下載
另一個缺點是當一個文件被修改后,整個緩存都需要被重新下載。這意味着即使你修改了一個簡單的拼寫錯誤 也會讓整個緩存重新下載。這不太高效。
瀏覽器的緩存可能阻礙 service worker 的緩存的更新
另外一個重要的警告。首次安裝時請求的資源是直接經由 HTTPS 的,這個時候瀏覽器不會返回緩存的資源, 除此之外,瀏覽器可能返回舊的緩存資源,這導致 service worker 的緩存不會得到 更新。
在生產環境中當下 cache-first 策略
我們的應用使用了優先緩存的策略,這導致所有后續請求都會從緩存中返回而不詢問網絡。優先緩存的策略是 很容易實現的,但也會為未來帶來諸多挑戰。一旦主頁和注冊的 service worker 被緩存下來,將會很難 去修改 service worker 的配置(因為配置依賴於它的位置),你會發現你部署的站點很難被升級。
我該如何避免這些邊緣問題
我們該如何避免這些邊緣問題呢? 使用一個庫,比如 sw-precache, 它對資源何時過期提供了 精細的控制,能夠確保請求直接經由網絡,並且幫你處理了所有棘手的問題。
實時測試 service workers 提示
調試 service workers 是一件有調整性的事情,當涉及到緩存后,當你期望緩存更新,但實際上它並沒有的時候,事情更是變得像一場惡夢。在 service worker 典型的生命周期和你的代碼之間,你很快就會受挫。但幸運的是,這里有一些工具可以讓你的生活更加簡單。
其他的提示:
一旦 service worker 被注銷(unregistered)。它會繼續作用直到瀏覽器關閉。
如果你的應用打開了多個窗口,新的 service worker 不會工作,直到所有的窗口都進行了刷新,使用了 新的 service worker。
注銷一個 service worker 不會清空緩存,所以如果緩存名沒有修改,你可能繼續獲得到舊的數據。
如果一個 service worker 已經存在,而且另外一個新的 service worker 已經注冊了,這個新的 service worker 不會接管控制權,知道該頁面重新刷新后,除非你使用立刻控制的方式。
注:使用例程final或者其他service worker會出現serviceworker failed to install的錯誤,是因為路徑原因導致緩存文件無法加載,請修改js中的文件路徑或者將images 、scripts、styles三個文件夾復制到網站根目錄下。