應用程式殼層是指用於支援使用者介面的最少 HTML、CSS 和 JavaScript。應用程式命令介面應符合下列條件:
- 快速載入
- 快取
- 動態顯示內容
應用程式殼層是確保良好效能的秘訣。假設您正在建構原生應用程式,應用程式殼層就像是您要發布到應用程式商店的程式碼套件。這是啟動應用程式所需的負載,但可能不是整個故事。這個類別會將 UI 保留在本機,並透過 API 動態擷取內容。
背景
Alex Russell 的「漸進式網頁應用程式」文章說明瞭網頁應用程式如何透過使用和使用者同意,漸進式變更,提供更像原生應用程式的體驗,包括離線支援、推播通知和新增至主畫面的功能。這取決於 service worker 的功能和效能優勢,以及其快取能力。這樣一來,您就能專注於速度,讓網頁應用程式提供與原生應用程式相同的即時載入和定期更新功能。
為了充分運用這些功能,我們需要以全新的方式思考網站:應用程式殼架構。
讓我們深入探討如何使用Service Worker 擴充應用程式殼架構來建構應用程式。我們將同時介紹用戶端和伺服器端算繪,並分享您今天就能試用的端對端範例。
為強調這一點,以下範例說明使用此架構的應用程式首次載入作業。請注意畫面底部的「應用程式可供離線使用」快訊。如果殼層稍後推出更新版本,我們可以通知使用者重新整理新版本。
請再提醒我,Service Worker 是什麼?
Service Worker 是指在背景中執行的獨立於網頁的指令碼。它會回應事件,包括由其服務的網頁提出的網路要求,以及來自伺服器的推播通知。服務工作站的生命週期會刻意縮短。這類服務會在收到事件時喚醒,並只在需要處理事件時執行。
與一般瀏覽情境中的 JavaScript 相比,服務工作者也只有有限的 API 可用。這是網路上工作站的標準做法。服務工作者無法存取 DOM,但可以存取 Cache API 等項目,並可使用 Fetch API 提出網路要求。IndexedDB API 和 postMessage() 也可用於服務工作者與其控制的網頁之間的資料持久性和訊息傳遞。伺服器傳送的推送事件可叫用 Notification API,以提升使用者參與度。
Service Worker 可以攔截網頁發出的網路要求 (會在 Service Worker 上觸發擷取事件),並傳回從網路擷取的回應,或是從本機快取擷取的回應,甚至是透過程式設計建構的回應。這項功能實際上是瀏覽器中的可程式 Proxy。最棒的是,無論回應來自何處,網頁都會以沒有 Service Worker 參與的情況顯示。
如要進一步瞭解 Service Worker,請參閱「Service Worker 簡介」。
成效優勢
服務工作者可用於離線快取,但也能為重複造訪網站或網路應用程式時提供即時載入的形式,進而大幅提升效能。您可以快取應用程式殼層,讓其在離線時運作,並使用 JavaScript 填入內容。
這樣一來,即使內容最終來自網路,您仍可在重複造訪時在畫面上取得有意義的像素。這項功能會立即顯示工具列和資訊卡,然後逐步載入其他內容。
為了在實際裝置上測試這個架構,我們在 WebPageTest.org 上執行應用程式殼層範例,並在下方顯示結果。
測試 1: 使用 Chrome 開發人員版在 Nexus 5 上以有線方式測試
應用程式的首個檢視畫面必須從網路擷取所有資源,且在 1.2 秒後才會完成有效繪製。由於有服務工作者快取功能,重複造訪可在 0.5 秒內完成塗色和完全載入。
測試 2: 使用 Nexus 5 和 Chrome 開發人員版在 3G 網路上進行測試
我們也可以使用稍慢的 3G 連線測試範例。這次首次造訪的首次有效顯示所需時間為 2.5 秒。系統需要 7.1 秒才能完全載入網頁。透過服務工作者快取功能,重複造訪可在 0.8 秒內完成塗色和完全載入作業。
「其他觀點」也顯示類似的結果。比較在應用程式殼層中達到首次顯示內容所需的時間 (3 秒):
從服務工作者快取載入相同網頁的時間為 0.9 秒。使用者可節省超過 2 秒的時間。
使用應用程式殼架構,您自家的應用程式也能達到類似且可靠的效能。
服務工作者是否需要我們重新思考應用程式的結構?
服務工作者會在應用程式架構中做出一些微妙的變更。請不要將所有應用程式內容擠壓到 HTML 字串中,而是採用 AJAX 風格執行操作。這裡有殼層 (一律會快取,且無須網路即可啟動),以及定期更新且可個別管理的內容。
這項分割作業的影響相當重大。在首次造訪時,您可以在伺服器上算繪內容,並在用戶端上安裝服務工作者。之後造訪時,您只需要要求資料即可。
那麼漸進增強呢?
雖然目前並非所有瀏覽器都支援服務工作者,但應用程式內容殼層架構會使用漸進式增強功能,確保所有使用者都能存取內容。例如,以我們的範例專案為例。
您可以查看下方在 Chrome、Firefox Nightly 和 Safari 中顯示的完整版本。在最左邊,您可以看到 Safari 版本,其中內容是在伺服器上轉譯,不使用服務工作者。右側顯示由服務工作者支援的 Chrome 和 Firefox Nightly 版本。
什麼時候適合採用這個架構?
應用程式指令介面架構最適合動態應用程式和網站。如果您的網站很小且為靜態網站,您可能不需要應用程式殼層,只要在 Service Worker oninstall
步驟中快取整個網站即可。請使用最適合專案的方法。許多 JavaScript 架構已鼓勵將應用程式邏輯與內容分開,讓您更輕鬆地套用這類模式。
是否有任何正式版應用程式使用這個模式?
只要對整體應用程式的 UI 進行幾項變更,即可實現應用程式殼架構,而且這種架構也適用於大型網站,例如 Google 的 I/O 2015 Progressive Web App 和 Google 的 Inbox。
離線應用程式殼是主要的效能勝出者,Jake Archibald 的 離線 Wikipedia 應用程式和 Flipkart Lite 的漸進式網頁應用程式也充分展現了這一點。
說明架構
在首次載入體驗期間,您的目標是盡快將有意義的內容顯示在使用者螢幕上。
首次載入和載入其他網頁
一般而言,應用程式殼架構會:
優先處理初始載入作業,但讓服務工作程將應用程式殼層快取,這樣重複造訪時就不需要從網路重新擷取殼層。
延遲載入或背景載入其他所有內容。一個不錯的做法是,針對動態內容使用讀取式快取。
例如使用 sw-precache 等服務工作站工具,可讓您可靠地快取及更新用於管理靜態內容的服務工作站。(稍後會進一步說明 sw-precache)。
如要達成這點,請按照下列步驟操作:
伺服器會傳送用戶端可轉譯的 HTML 內容,並使用遠端 HTTP 快取到期標頭,以便針對不支援服務工作者的瀏覽器進行處理。它會使用雜湊值提供檔案名稱,以便在應用程式生命週期後半段進行「版本管理」和輕鬆更新。
頁面會在文件
<head>
的<style>
標記中加入內嵌 CSS 樣式,以便快速顯示應用程式殼層的首次繪製畫面。每個網頁都會以非同步方式載入目前檢視畫面所需的 JavaScript。由於 CSS 無法以非同步方式載入,我們可以使用 JavaScript 要求樣式,因為 JavaScript 是非同步的,而非由剖析器驅動且同步。我們也可以利用requestAnimationFrame()
,避免快速快取命中,導致樣式意外成為關鍵算繪路徑的一部分。requestAnimationFrame()
會在載入樣式之前強制繪製第一個影格。另一個做法是使用 Filament Group 的 loadCSS 等專案,透過 JavaScript 以非同步方式要求 CSS。Service worker 會儲存應用程式殼層的快取項目,以便在重複造訪時,從 Service worker 快取中載入完整的殼層,除非網路上有可用的更新。
實際實作
我們已使用應用程式殼架構、用於用戶端的純 ES2015 JavaScript,以及用於伺服器的 Express.js,編寫了可正常運作的範例。當然,您也可以使用自己的程式庫來處理用戶端或伺服器部分 (例如 PHP、Ruby、Python)。
服務工作者生命週期
在應用程式殼層專案中,我們使用 sw-precache,提供以下服務工作程生命週期:
活動 | 動作 |
---|---|
安裝 | 快取應用程式殼層和其他單頁應用程式資源。 |
啟用 | 清除舊快取。 |
擷取 | 針對網址提供單頁式網頁應用程式,並使用素材資源和預先定義的部分快取。使用網路來處理其他要求。 |
伺服器位元
在這個架構中,伺服器端元件 (在本例中,以 Express 編寫) 應能分別處理內容和呈現方式。您可以將內容新增至 HTML 版面配置,以便靜態轉譯網頁,也可以單獨提供內容並動態載入。
很明顯,您的伺服器端設定可能與我們用於示範應用程式的設定大不相同。這種網路應用程式模式可透過多數伺服器設定達成,但確實需要進行一些重構。我們發現以下模型的效果相當不錯:
端點可為應用程式的三個部分進行定義:面向使用者的網址 (索引/萬用字元)、應用程式殼層 (服務工作者) 和 HTML 部分。
每個端點都有一個控制器,可拉入 handlebars 版面配置,進而拉入 handlebars 部分和檢視畫面。簡單來說,部分是指複製到最終網頁的 HTML 區塊。注意:執行進階資料同步作業的 JavaScript 架構,通常更容易移植至應用程式殼層架構。這些程式碼通常會使用資料繫結和同步處理,而非部分程式碼。
系統一開始會向使用者提供含有內容的靜態網頁。這個頁面會註冊 Service Worker (如有支援),用於快取應用程式主控台和所有依附項目 (CSS、JS 等)。
應用程式殼層就會充當單頁網頁應用程式,使用 JavaScript 將特定網址的內容傳送至 XHR。XHR 呼叫會傳送至 /partials* 端點,該端點會傳回顯示該內容所需的 HTML、CSS 和 JS 小型程式碼片段。注意:這項操作有很多種方法,XHR 只是其中一種。有些應用程式會將資料內嵌 (可能使用 JSON) 進行初始轉譯,因此在扁平化 HTML 的情況下並非「靜態」。
對於不支援 Service Worker 的瀏覽器,應一律提供備用體驗。在示範中,我們會改為使用基本靜態伺服器端轉譯,但這只是眾多選項之一。服務工作者可讓您使用快取的應用程式殼層,為單頁應用程式風格的應用程式帶來新的效能提升機會。
檔案版本設定
這時就會產生一個問題,就是如何處理檔案版本控制和更新作業。這項設定會依應用程式而異,可用的選項如下:
先使用網路版本,如果沒有則使用快取版本。
僅限網路,離線時會失敗。
將舊版快取,稍後再更新。
針對應用程式主控台本身,應採用快取優先做法來設定服務工作者。如果您沒有快取應用程式殼層,就表示您並未正確採用架構。
工具
我們維護多個不同的服務工作者輔助程式庫,讓您可以更輕鬆地設定應用程式殼層的預快取程序,或處理常見的快取模式。
為應用程式殼層使用 sw-precache
使用 sw-precache 快取應用程式殼層時,應處理與檔案修訂、安裝/啟用問題,以及應用程式殼層擷取情境相關的疑慮。將 sw-precache 加入應用程式的建構程序,並使用可設定的萬用字元來挑選靜態資源。請讓 sw-precache 產生服務工作者指令碼,以便使用快取優先擷取處理常式,安全且有效率地管理快取,而非手動製作服務工作者指令碼。
首次造訪應用程式時,系統會觸發預先快取所需的完整資源。這與從應用程式商店安裝原生應用程式的體驗類似。使用者返回應用程式時,系統只會下載更新的資源。在示範中,我們會在推出新殼層時通知使用者,並顯示「應用程式更新」訊息。重新整理即可取得新版本」這個模式是讓使用者知道可以重新整理至最新版本的低摩擦方式。
使用 sw-toolbox 進行執行階段快取
使用 sw-toolbox 進行執行階段快取,並根據資源採用不同的策略:
圖片的 cacheFirst,以及專屬命名快取,其中包含 N 個 maxEntries 的自訂到期政策。
networkFirst 或最快的 API 要求,取決於所需的內容新鮮度。使用最快的選項可能沒問題,但如果有特定 API 動態饋給經常更新,請使用 networkFirst。
結論
應用程式殼架構具有多項優點,但只適用於某些類型的應用程式。這個模型仍處於初期階段,因此值得評估這項架構的努力程度和整體效益。
在實驗中,我們利用用戶端和伺服器之間的範本共用功能,盡量減少建構兩個應用程式層的工作量。這可確保漸進式強化功能仍是核心功能。
如果您已考慮在應用程式中使用 Service Worker,請查看架構,並評估是否適合您自己的專案。
感謝審查人員:Jeff Posnick、Paul Lewis、Alex Russell、Seth Thompson、Rob Dodson、Taylor Savage 和 Joe Medley。