Progressive Web Apps 是結合了 Web 和 原生應用中最好功能的一種體驗。對於首次訪問的使用者它是非常有利的, 使用者可以直接在瀏覽器中進行訪問,不需要安裝應用。隨著時間的推移當使用者漸漸地和應用建立了聯絡,它將變得越來越強大。它能夠快速地載入,即使在比較糟糕的網路環境下,能夠推送相關訊息, 也可以像原生應用那樣新增至主屏,能夠有全屏瀏覽的體驗。
什麼是 Progressive Web App?
Progressive Web Apps 是:
- 漸進增強 – 能夠讓每一位使用者使用,無論使用者使用什麼瀏覽器,因為它是始終以漸進增強為原則。
- 響應式使用者介面 – 適應任何環境:桌面電腦,智慧手機,膝上型電腦,或者其他裝置。
- 不依賴網路連線 – 通過 service workers 可以在離線或者網速極差的環境下工作。
- 類原生應用 – 有像原生應用般的互動和導航給使用者原生應用般的體驗,因為它是建立在 app shell model 上的。
- 持續更新 – 受益於 service worker 的更新程式,應用能夠始終保持更新。
- 安全 – 通過 HTTPS 來提供服務來防止網路窺探,保證內容不被篡改。
- 可發現 – 得益於 W3C manifests 後設資料和 service worker 的登記,讓搜尋引擎能夠找到 web 應用。
- 再次訪問 – 通過訊息推送等特性讓使用者再次訪問變得容易。
- 可安裝 – 允許使用者保留對他們有用的應用在主螢幕上,不需要通過應用商店。
- 可連線性 – 通過 URL 可以輕鬆分享應用,不用複雜的安裝即可執行。
這引導指南將會引導你完成你自己的 Progressive Web App,包括設計時需要考慮的因素,也包括實現細節,以確保你的應用程式符合 Progressive Web App 的關鍵原則。
我們將要做什麼?
你將會學到
- 如何使用 “app shell” 的方法來設計和構建應用程式。
- 如何讓你的應用程式能夠離線工作。
- 如何儲存資料以在離線時使用。
你需要
- Chrome 52 或以上
- Web Server for Chrome 或其他的網路伺服器。
- 示例程式碼
- 程式碼編輯器
HTML,CSS 和 JavaScript 的基本知識 這份引導指南的重點是 Progressive Web Apps。其中有些概念的只是簡單的解釋 而有些則是隻提供示例程式碼(例如 CSS 和其他不相關的 Javascipt ),你只需複製和貼上即可。
設定
下載示例程式碼
你可以下載本 progressive web app 引導指南需要的所有程式碼。
將下載好的zip檔案進行解壓縮。這將會解壓縮一個名為(your-first-pwapp-master
)的根資料夾。這資料夾包含了這指南你所需要的資源。
名為 step-NN
的資料夾則包含了這指南每個步驟的完整的程式碼。你可以把他當成參考。
安裝及校驗網路伺服器
你可以選擇其他的網路伺服器,但在這個指南我們將會使用Web Server for Chrome。如果你還沒有安裝,你可以到 Chrome 網上應用店下載。
安裝完畢後,從書籤欄中選擇Apps的捷徑:
接下來點選Web Server的圖示
你將會看到以下的視窗,這讓你配置你的本地網路伺服器:
點選 choose folder 的按鈕,然後選擇名為 work
的資料夾。這會把目錄和檔案都以HTTP的方式展示出來。URL地址可以在視窗裡的 Web Server URL(s) 找到。
在選項中,選擇”Automatically show index.html” 的選擇框:
然後在 “Web Server: STARTED” 的按鈕拉去左邊,在拉去右邊,以將本地網路伺服器關閉並重啟。
現在你可以使用遊覽器來訪問那個資料夾(點選視窗內的Web Server URL下的連結即可)。你將會看到以下的畫面:
很明顯的,這個應用程式並沒有什麼功能。現在只有一個載入圖示在那裡轉動,這只是來確保你的網路伺服器能正常操作。在接下來的步驟,我們將會新增更多東西。
基於應用外殼的架構
什麼是應用外殼(App Shell)
App Shell是應用的使用者介面所需的最基本的 HTML、CSS 和 JavaScript,也是一個用來確保應用有好多效能的元件。它的首次載入將會非常快,載入後立刻被快取下來。這意味著應用的外殼不需要每次使用時都被下載,而是隻載入需要的資料。
應用外殼的結構分為應用的核心基礎元件和承載資料的 UI。所有的 UI 和基礎元件都使用一個 service worker 快取在本地,因此在後續的載入中 Progressive Web App 僅需要載入需要的資料,而不是載入所有的內容。
換句話說,應用的殼相當於那些釋出到應用商店的原生應用中打包的程式碼。它是讓你的應用能夠執行的核心元件,只是沒有包含資料。
為什麼使用基於應用外殼的結構?
使用基於應用外殼的結構允許你專注於速度,給你的 Progressive Web App 和原生應用相似的屬性:快速的載入和靈活的更新,所有這些都不需要用到應用商店。
設計應用外殼
第一步是設計核心元件
問問自己:
- 需要立刻顯示什麼在螢幕上?
- 我們的應用需要那些關鍵的 UI 元件?
- 應用外殼需要那些資源?比如圖片,JavaScript,樣式表等等。
我們將要建立一個天氣應用作為我們的第一個 Progressive Web App 。它的核心元件包括:
在設計一個更加複雜的應用時,內容不需要在首次全部載入,可以在之後按需載入,然後快取下來供下次使用。比如,我們能夠延遲載入新增城市的對話方塊,直到完成對首屏的渲染且有一些空閒的時間。
實現應用外殼
任何專案都可以有多種起步方式,通常我們推薦使用 Web Starter Kit。但是,這裡為了保持我們的專案 足夠簡單並專注於 Progressive Web Apps,我們提供了你所需的全部資源。
為應用外殼編寫 HTML 程式碼
為了保證我們的起步程式碼儘可能清晰,我們將會開始於一個新的 index.html
檔案並新增在 構建應用外殼中談論過的核心元件的程式碼
請記住,核心元件包括:
- 包含標題的頭部,以及頭部的 新增/重新整理 按鈕
- 放置天氣預報卡片的容器
- 天氣預報卡片的模板
- 一個用來新增城市的對話方塊
- 一個載入指示器
12345678910111213141516171819202122232425262728293031323334<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Weather PWA</title><link rel="stylesheet" type="text/css" href="styles/inline.css"></head><body><header class="header"><h1 class="header__title">Weather PWA</h1><button id="butRefresh" class="headerButton"></button><button id="butAdd" class="headerButton"></button></header><main class="main"><div class="card cardTemplate weather-forecast" hidden>. . .</div></main><div class="dialog-container">. . .</div><div class="loader"><svg viewBox="0 0 32 32" width="32" height="32"><circle id="spinner" cx="16" cy="16" r="14" fill="none"></circle></svg></div><!-- Insert link to app.js here --></body></html>
需要注意的是,在預設情況下載入指示器是顯示出來的。這是為了保證 使用者能在頁面載入後立刻看到載入器,給使用者一個清晰的指示,表明頁面正在載入。
為了節省你的時間,我們已經已建立了 stylesheet。
新增關鍵的 JavaScript 啟動程式碼
現在我們的 UI 已經準備好了,是時候來新增一些程式碼讓它工作起來了。像搭建應用外殼的時候那樣,注意 考慮哪些程式碼是為了保持使用者體驗必須提供的,哪些可以延後載入。
在啟動程式碼中,我們將包括(你可以在(scripts/app.js
)的資料夾中找到):
- 一個
app
物件包含一些和應用效果的關鍵資訊。 - 為頭部的按鈕(
add/refresh
)和新增城市的對話方塊中的按鈕(add/cancel
)新增事件監聽 函式。 - 一個新增或者更新天氣預報卡片的方法(
app.updateForecastCard
)。 - 一個從 Firebase 公開的天氣 API 上獲取資料的方法(
app.getForecast
)。 - 一個迭代當前所有卡片並呼叫
app.getForecast
獲取最新天氣預報資料的方法 (app.updateForecasts
)。 - 一些假資料 (
fakeForecast
) 讓你能夠快速地測試渲染效果。
測試
現在,你已經新增了核心的 HTML、CSS 和 JavaScript,是時候測試一下應用了。這個時候它能做的可能還不多,但要確保在控制檯沒有報錯資訊。
為了看看假的天氣資訊的渲染效果,從index.html
中取消註釋以下的程式碼:
1 |
<!--<script src="scripts/app.js" async></script>--> |
接下來,從 app.js
中取消註釋以下的程式碼:
1 |
// app.updateForecastCard(initialWeatherForecast); |
重新整理你的應用程式,你將會看到一個比較整齊漂亮的天氣預報的卡片:
嘗試並確保他能正常運作之後,將 app.updateForecastCard
清除。
從快速的首次載入開始
Progressive Web Apps 應該能夠快速啟動並且立即可用。目前,我們的天氣應用能夠快速啟動,但是還不能使用,因為還沒有資料。我們能夠發起一個 AJAX 請求來獲取資料,但是額外的請求會讓初次載入時間變長。取而代之的方法是,在初次載入時提供真實的資料。
插入天氣預報資訊
在本例項中,我們將會靜態地插入天氣預報資訊,但是在一個投入生產環境的應用中,最新的天氣預報資料會由伺服器根據使用者的 IP 位置資訊插入。
這程式碼已經包括了所需的資料,那就是我們在前個步驟所用的 initialWeatherForecast
。
區分首次執行
但我們如何知道什麼時候該展示這些資訊,那些資料需要存入快取供下次使用?當使用者下次使用的時候,他們所在城市可能已經發生了變動,所以我們需要載入目前所在城市的資訊,而不是之前的城市。
使用者首選項(比如使用者訂閱的城市列表),這類資料應該使用 IndexedDB 或者其他快速的儲存方式存放在本地。 為了儘可能簡化,這裡我們使用 localStorage 進行儲存,在生產環境下這並不是理想的選擇,因為它是阻塞型同步的儲存機制,在某些裝置上可能很緩慢。
首先,讓我們新增用來儲存使用者首選項的程式碼。從程式碼中尋找以下的TODO註解:
1 |
// TODO add saveSelectedCities function here |
然後將以下的程式碼貼上在TODO註解的下一行。
1 2 3 4 5 |
// 將城市裂變存入 localStorage. app.saveSelectedCities = function() { var selectedCities = JSON.stringify(app.selectedCities); localStorage.selectedCities = selectedCities; }; |
接下來,新增一些啟動程式碼來檢查使用者是否已經訂閱了某些城市,並渲染它們,或者使用插入的天氣資料來渲染。從程式碼中尋找以下的TODO註解:
1 |
// TODO add startup code here |
然後將以下的程式碼貼上在TODO註解的下一行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/**************************************************************************** * * 用來啟動應用的程式碼 * * 注意: 為了簡化入門指南, 我們使用了 localStorage。 * localStorage 是一個同步的 API,有嚴重的效能問題。它不應該被用於生產環節的應用中! * 應該考慮使用, IDB (https://www.npmjs.com/package/idb) 或者 * SimpleDB (https://gist.github.com/inexorabletash/c8069c042b734519680c) * ****************************************************************************/ app.selectedCities = localStorage.selectedCities; if (app.selectedCities) { app.selectedCities = JSON.parse(app.selectedCities); app.selectedCities.forEach(function(city) { app.getForecast(city.key, city.label); }); } else { app.updateForecastCard(initialWeatherForecast); app.selectedCities = [ {key: initialWeatherForecast.key, label: initialWeatherForecast.label} ]; app.saveSelectedCities(); } |
儲存已被選擇的城市
現在,你需要修改”add city”按鈕的功能。這將會把已被選擇的城市儲存進local storage。
更新butAddCity
中的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
document.getElementById('butAddCity').addEventListener('click', function() { // Add the newly selected city var select = document.getElementById('selectCityToAdd'); var selected = select.options[select.selectedIndex]; var key = selected.value; var label = selected.textContent; if (!app.selectedCities) { app.selectedCities = []; } app.getForecast(key, label); app.selectedCities.push({key: key, label: label}); app.saveSelectedCities(); app.toggleAddDialog(false); }); |
測試
- 在首次允許時,你的應用應該立刻向使用者展示 initialWeatherForecast 中的天氣資料。
- 新增一個新城市確保會展示兩個卡片。
- 重新整理瀏覽器並驗證應用是否載入了天氣預報並展示了最新的資訊。
使用 Service Workers 來預快取應用外殼
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中。
1 2 3 4 5 |
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
中找到) :
1 2 3 4 5 6 7 8 9 10 11 12 |
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
的事件監聽器。
1 2 3 |
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 的事件處理函式的程式碼以更新快取。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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以及樣式表等等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 發起的請求並從快取中返回響應。這意味著我們能夠 決定如何來處理這些請求,以及決定哪些網路響應能夠成為我們的快取。
比如:
1 2 3 |
self.addEventListener('fetch', function(event) { // Do something interesting with the fetch here }); |
讓我們來從快取中載入 app shell。將下面程式碼加入 service-worker.js
中:
1 2 3 4 5 6 7 8 |
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 不會接管控制權,知道該頁面重新重新整理後,除非你使用立刻控制的方式。
使用 Service Workers 來快取應用資料
選擇一個正確的快取策略是很重要的,並且這取決於你應用中使用的資料的型別。比如像天氣資訊、股票資訊等對實時性要求較高的資料,應該時常被重新整理,但是使用者的頭像或者文字內容應該以較低的頻率進行更新。
先使用快取後使用請求結果 的策略對於我們的應用是非常理想的選擇。應用從快取中獲取資料,並立刻顯示在螢幕上,然後在網路請求返回後再更新頁面。如果使用 先請求網路後快取 的策略,使用者可能不會等到資料從網路上載入回來便離開了應用。
先使用快取後使用請求結果 意味著我們需要發起兩個非同步的請求,一個從請求快取,另一個請求網路。我們應用中的網路請求不需要進行修改,但我們需要修改一下 service worker 的程式碼來快取網路請求的響應並返回響應內容。
通常情況下,應該立刻返回快取的資料,提供應用能夠使用的最新資訊。然後當網路請求返回後應用應該使用最新載入的資料來更新。
截獲網路請求並快取響應結果
我麼需要修改 service worker 來截獲對天氣 API 的請求,然後快取請求的結果,以便於以後使用。先使用快取後使用請求結果 的策略中,我們希望請求的響應是真實的資料來源,並始終提供給我們最新的資料。如果它不能做到,那也沒什麼,因為我們已經從快取中給應用提供了最新的資料。
在 service worker 中,我們新增一個 dataCacheName
變數,以至於我們能夠將應用資料和應用外殼資源分開。當應用外殼更新了,應用外殼的快取就沒用了,但是應用的資料不會受影響,並時刻保持能用。記住,如果將來你的資料格式改變了,你需要一種能夠讓應用外殼和應用資料能後保持同步的方法。
將下面程式碼新增至你的 service-worker.js
中:
1 |
var dataCacheName = 'weatherData-v1'; |
接下來,我麼需要更新activate
事件的回撥函式,以它清理應用程式的外殼(app shell)快取,並不會刪除資料快取。
1 |
if (key !== cacheName && key !== dataCacheName) { |
最後,我麼需要修改 fetch
事件的回撥函式,新增一些程式碼來將請求資料 API 的請求和其他請求區分開來。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
self.addEventListener('fetch', function(e) { console.log('[ServiceWorker] Fetch', e.request.url); var dataUrl = 'https://publicdata-weather.firebaseio.com/'; if (e.request.url.indexOf(dataUrl) === 0) { // Put data handler code here } else { e.respondWith( caches.match(e.request).then(function(response) { return response || fetch(e.request); }) ); } }); |
這段程式碼對請求進行攔截,判斷請求的 URL 的開頭是否為該天氣 API,如果是,我們使用 fetch
來發起請求。一旦有響應返回,我們的程式碼就開啟快取並將響應存入快取,然後將響應返回給原請求。
接下來,使用下面程式碼替換 // Put data handler code here
1 2 3 4 5 6 7 8 9 10 |
e.respondWith( fetch(e.request) .then(function(response) { return caches.open(dataCacheName).then(function(cache) { cache.put(e.request.url, response.clone()); console.log('[ServiceWorker] Fetched&Cached Data'); return response; }); }) ); |
我們的應用目前還不能離線工作。我們已經實現了從快取中返回應用外殼,但即使我們快取了資料,依舊需要依賴網路。
發起請求
之前提到過,應用需要發起兩個非同步請求,一個從請求快取,另一個請求網路。應用需要使用 window
上的 caches
物件,並從中取到最新的資料。這是一個關於漸進增強 極佳 的例子,因為 caches 物件可能並不是在任何瀏覽器上都存在的,且就算它不存在,網路請求依舊能夠工作,只是沒有使用快取而已。
為了實現該功能,我們需要:
- 檢查 cahces 物件是否存在在全域性 window 物件上。
- 向快取發起請求
- 如果向伺服器發起的請求還沒有返回結果,使用快取中返回的資料更新應用。
- 向伺服器發起請求
- 儲存響應結果便於在之後使用
- 使用從伺服器上返回的最新資料更新應用
從快取中獲取資料
接下來,我們需要檢查 caches
物件是否存在,若存在,就向它請求最新的資料。將下面這段程式碼新增至 app.getForecast()
方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
if ('caches' in window) { /* * Check if the service worker has already cached this city's weather * data. If the service worker has the data, then display the cached * data while the app fetches the latest data. */ caches.match(url).then(function(response) { if (response) { response.json().then(function updateFromCache(json) { var results = json.query.results; results.key = key; results.label = label; results.created = json.query.created; app.updateForecastCard(results); }); } }); } |
我們的天氣應用現在發起了兩個非同步請求,一個從快取中,另一個經由 XHR。如果有資料存在於快取中,它將會很快地(幾十毫秒)被返回並更新顯示天氣的卡片,通常這個時候 XHR 的請求還沒有返回來。之後當 XHR 的請求響應了以後,顯示天氣的卡片將會使用直接從天氣 API 中請求的最新資料來更新。
如果因為某些原因,XHR 的響應快於 cache 的響應,hasRequestPending
標誌位會阻止快取中資料覆蓋從網路上請求的資料。
1 2 3 4 5 6 7 8 9 |
var cardLastUpdatedElem = card.querySelector('.card-last-updated'); var cardLastUpdated = cardLastUpdatedElem.textContent; if (cardLastUpdated) { cardLastUpdated = new Date(cardLastUpdated); // Bail if the card has more recent data then the data if (dataLastUpdated.getTime() < cardLastUpdated.getTime()) { return; } } |
親自嘗試
現在應用應該能夠離線工作了。嘗試關閉裡本地啟動的伺服器,並切斷網路,然後重新整理頁面。
然後去DevTools的 Application 皮膚上的 Cache Storage 窗格。 展開該部分,你應該會在左邊看到您的app shell快取的名稱。當你點選你的appshell快取,你將會看到所有已經被快取的資源。
支援整合入原生應用
沒有人喜歡在手機的鍵盤上輸入一長串的 URL,有了新增至主螢幕的功能,你的使用者可以選擇新增一個圖示在他們的螢幕上,就像從應用商店安裝一個原生應用那樣。而且這兒新增一個圖示是更加容易的。
Web 應用安裝橫幅和新增至主屏
Web 應用安裝橫幅給你能夠讓使用者快速地將 Web 應用新增至他們的主屏的能力,讓他們能夠很容易地再次進入你的應用。新增應用安裝橫幅是很簡單的,Chrome 處理了幾乎所有事情,我麼只需要簡單地包含一個應用程式清單(manifest)來說明你的應用的一些細節。
Chrome 使用了一系列標準包括對 service worker 的使用,加密連線狀態以及使用者的訪問頻率決定了什麼時候展示這個橫幅。除此之外,使用者可以手動地通過 Chrome 中 “新增至主屏” 這個選單按鈕來新增。
使用 manifest.json 檔案來宣告一個應用程式清單
Web 應用程式清單是一個簡單的 JSON 檔案,它給你了控制你的應用如何出現在使用者期待出現的地方(比如使用者手機主螢幕),這直接影響到使用者能啟動什麼,以及更重要的,使用者如何啟動它。
使用 web 應用程式清單,你的應用可以:
- 能夠真實存在於使用者主螢幕上
- 在 Android 上能夠全屏啟動,不顯示位址列
- 控制螢幕方向已獲得最佳效果
- 定義啟動畫面,為你的站點定義主題
- 追蹤你的應用是從主螢幕還是 URL 啟動的
1234567891011121314151617181920212223242526272829{"name": "Weather","short_name": "Weather","icons": [{"src": "images/icons/icon-128x128.png","sizes": "128x128","type": "image/png"}, {"src": "images/icons/icon-144x144.png","sizes": "144x144","type": "image/png"}, {"src": "images/icons/icon-152x152.png","sizes": "152x152","type": "image/png"}, {"src": "images/icons/icon-192x192.png","sizes": "192x192","type": "image/png"}, {"src": "images/icons/icon-256x256.png","sizes": "256x256","type": "image/png"}],"start_url": "/index.html","display": "standalone","background_color": "#3E4EB8","theme_color": "#2F3BA2"}
追蹤你的應用是從哪兒啟動的最簡單方式是在 start_url
引數後面新增一個查詢字串,然後使用工具來分析查詢欄位。如果你使用這個方法,記得要更新應用外殼快取的檔案,確保含有查詢欄位的檔案被快取。
告訴瀏覽器你的程式清單檔案
將這段程式碼新增至你的 index.html
的 <head>
部分:
1 |
<link rel="manifest" href="/manifest.json"> |
最佳實踐
- 將程式清單的連結新增至你站點的所有頁面上,這樣在使用者第一次訪問的時候它能夠被 Chrome 正確檢索到,且不管使用者從哪個頁面訪問的。
- 如果同時提供了
name
和short_name
,short_name
是 Chrome 的首選。 - 為不同解析度的螢幕提供不同的 icon。Chrome 會嘗試使用最接近 48dp 的圖示,比如在
2x
屏上使用96px
的,在3x
屏上使用144px
的。 - 記得要包含一個適合在啟動畫面上顯示的圖示,另外別忘了設定
background_color
。
擴充套件閱讀:使用應用安裝橫幅
iOS Safari 的新增至主螢幕元素
在 index.html
中,將下面程式碼新增至 <head>
中:
1 2 3 4 5 |
<!-- Add to home screen for Safari on iOS --> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-title" content="Weather PWA"> <link rel="apple-touch-icon" href="images/icons/icon-152x152.png"> |
Windows 上的貼片圖示
在 index.html
中,將下面程式碼新增至 <head>
中:
1 2 |
<meta name="msapplication-TileImage" content="images/icons/icon-144x144.png"> <meta name="msapplication-TileColor" content="#2F3BA2"> |
親自嘗試
- 嘗試將應用在你的 Android Chrome 上新增至首屏,並確認啟動畫面上使用了正確的圖示。
- 檢查一下 Safari 和 Internet Explorer 確認圖示正確地出現了。
部署在安全的主機上
最後一步是將我們的天氣應用部署在一個支撐 HTTPs 的伺服器上。如果你目前還沒有一個這樣的主機,那麼最簡單(且免費)的方法絕對是使用我們的靜態資源部署服務 Firebase。它非常容易使用,通過 HTTPs 來提供服務且在全球 CDN 中。
可優化的地方:壓縮並內聯 CSS 樣式
還有一些你需要考慮的事情,壓縮關鍵的 CSS 樣式並將其內聯在 index.html
中。Page Speed Insights 建議以上內容要在 15k
以內。
看看當所有內容都內聯後,首次載入資源有多大。
擴充套件閱讀: PageSpeed Insight Rules
部署到 Firebase
如果你首次使用 Firebase,那麼你需要使用你的 Google 賬號登入 Firebase 並安裝一些工具。
- 使用你的 Google 賬號登入 Firebase
- 通過
npm
安裝 Firebase 工具 :npm install -g firebase-tools
你的賬號被建立且已經登入後,你就可以開始部署了!
- 建立一個新的應用,在這兒
- 如果你最近沒有登入過 Firebase 工具,請更新你的證照:
firebase login
- 初始化你的應用,並提供你完成了應用的目錄位置:
firebase init
- 最後,將應用部署至 Firebase:
firebase deploy
- 祝賀你。你完成了,你的應用將會部署在:
https://YOUR-FIREBASE-APP.firebaseapp.com
擴充套件閱讀: Firebase Hosting Guide
親自嘗試
試著將應用新增至你的主螢幕,然後斷開網路連線,看看它是否能在離線的情況下很好的工作。