漸進式 Web 應用程序初學者指南
已發表: 2022-03-10漸進式網絡應用程序可能是移動網絡的下一件大事。 最初由谷歌在 2015 年提出,由於相對容易開發和應用程序的用戶體驗幾乎立竿見影,它們已經引起了很多關注。
關於 SmashingMag 的進一步閱讀:
- 漸進式 Web 應用程序的構建塊
- 對話式設計要點:構建聊天機器人的技巧
- 構建利用您的網站的一流應用程序
- 在 Foundation For Apps 中創建一個完整的 Web 應用程序
漸進式 Web 應用程序利用最新技術將 Web 和移動應用程序的優點結合起來。 把它想像成一個使用網絡技術構建的網站,但它的行為和感覺就像一個應用程序。 瀏覽器和服務工作者可用性以及緩存和推送 API 的最新進展使 Web 開發人員能夠允許用戶將 Web 應用程序安裝到他們的主屏幕、接收推送通知甚至離線工作。
與各自應用商店中的本地應用程序相比,漸進式網絡應用程序利用了更大的網絡生態系統、插件和社區,以及部署和維護網站的相對容易性。 對於那些同時在移動和 Web 上進行開發的人,您會欣賞可以在更短的時間內構建網站,並且 API 不需要保持向後兼容(所有用戶將運行您網站的相同版本)代碼,不像原生應用程序的版本碎片),並且應用程序通常更容易部署和維護。
為什麼選擇漸進式 Web 應用程序?
一項研究表明,從用戶第一次接觸應用到用戶開始使用應用,每走一步,應用就會失去 20% 的用戶。 用戶必須首先在應用商店中找到該應用,下載並安裝它,然後最後打開它。 當用戶找到您的漸進式 Web 應用程序時,他們將能夠立即開始使用它,從而消除不必要的下載和安裝階段。 並且當用戶返回應用程序時,會提示他們安裝應用程序併升級到全屏體驗。
但是,原生應用程序絕對不是壞事。 帶有推送通知的移動應用程序的留存率是沒有推送的移動應用程序的三倍,用戶重新打開移動應用程序的可能性是網站的三倍。 此外,設計良好的移動應用程序消耗的數據更少,速度更快,因為一些資源駐留在設備上。
漸進式 Web 應用程序利用了移動應用程序的特性,從而提高了用戶保留率和性能,而無需維護移動應用程序所涉及的複雜性。
用例
什麼時候應該構建漸進式 Web 應用程序? 對於您希望用戶經常返回的應用程序,通常建議使用 Native,漸進式 Web 應用程序也不例外。 Flipkart 為其流行的電子商務平台 Flipkart Lite 使用漸進式網絡應用程序,而 SBB 在其在線簽到過程中使用漸進式網絡應用程序,允許用戶在沒有互聯網連接的情況下訪問他們的門票。
在評估您的下一個應用程序應該是漸進式 Web 應用程序、網站還是原生移動應用程序時,首先確定您的用戶和最重要的用戶操作。 作為“漸進式”,漸進式 Web 應用程序適用於所有瀏覽器,並且每當用戶的瀏覽器使用新的和改進的功能和 API 進行更新時,體驗都會得到增強。
因此,與傳統網站相比,漸進式 Web 應用程序的用戶體驗沒有任何妥協; 但是,您可能必須決定離線支持哪些功能,並且您必須方便導航(請記住,在獨立模式下,用戶無法訪問後退按鈕)。 如果您的網站已經有類似應用程序的界面,那麼應用漸進式 Web 應用程序的概念只會讓它變得更好。
如果關鍵用戶操作需要某些功能,但由於缺乏跨瀏覽器支持而尚不可用,則本機移動應用程序可能是更好的選擇,可確保所有用戶獲得相同的體驗。
漸進式 Web 應用程序的特徵
在我們進入代碼之前,重要的是要了解漸進式 Web 應用程序具有以下特徵:
- 漸進式。 根據定義,漸進式 Web 應用程序必須在任何設備上運行並逐步增強,利用用戶設備和瀏覽器上可用的任何功能。
- 可發現的。 因為漸進式網絡應用程序是一個網站,所以它應該可以在搜索引擎中發現。 這是與原生應用程序相比的主要優勢,原生應用程序在可搜索性方面仍然落後於網站。
- 可鏈接。 作為從網站繼承的另一個特徵,一個設計良好的網站應該使用 URI 來指示應用程序的當前狀態。 這將使 Web 應用程序能夠在用戶添加書籤或共享應用程序的 URL 時保留或重新加載其狀態。
- 反應靈敏。 漸進式 Web 應用程序的 UI 必須適合設備的外形尺寸和屏幕尺寸。
- 類似應用程序的. 漸進式 Web 應用程序應該看起來像原生應用程序,並且構建在應用程序外殼模型上,並且頁面刷新次數最少。
- 與連接無關。 它應該在連接性低或離線(我們最喜歡的特性)的區域工作。
- 可重新接合。 移動應用程序用戶更有可能重用他們的應用程序,而漸進式 Web 應用程序旨在通過推送通知等功能實現相同的目標。
- 可安裝。 可以在設備的主屏幕上安裝漸進式 Web 應用程序,使其隨時可用。
- 新鮮。 當新內容髮布並且用戶連接到 Internet 時,該內容應該在應用程序中可用。
- 安全。 因為漸進式 Web 應用程序具有更親密的用戶體驗,並且所有網絡請求都可以通過服務工作人員攔截,所以必須通過 HTTPS 託管應用程序以防止中間人攻擊。
讓我們編碼吧!
我們的第一個漸進式網絡應用 Sky High 將模擬機場的到達時間表。 當用戶第一次訪問我們的 Web 應用程序時,我們希望向他們顯示從 API 檢索到的即將到來的航班列表。 如果用戶沒有 Internet 連接並且他們重新加載了 Web 應用程序,我們希望向他們顯示他們上次通過連接下載它時的航班時刻表。

基礎
漸進式 Web 應用程序的第一個特徵是它必須在所有設備上運行,並且必須在允許它的設備和瀏覽器上進行增強。 因此,我們使用傳統的 HTML5 和模擬從模擬 API 檢索數據的 JavaScript 構建了我們的網站。 在整個應用程序中,我們使用少量的 Knockout 來處理我們的 Model-View-ViewModel (MVVM) 綁定——一個輕量級的 JavaScript 框架,允許我們將 JavaScript 模型綁定到我們的 HTML 視圖。 我們選擇使用 Knockout 是因為它比較容易理解並且不會使代碼混亂; 但是,您可以將其替換為任何其他框架,例如 React 或 AngularJS。
我們的網站遵循 Google 的材料設計指南,這是一組指導設計和交互的原則。 材料設計不僅作為跨應用程序和設備的統一標準,而且賦予設計意義。 我們為 Sky High 的到達視圖使用材料設計,為我們的漸進式 Web 應用程序提供原生應用程序的外觀和感覺。
最後,我們測試了我們的應用程序以確保它沒有卡頓並且滾動流暢。 無 Jank 渲染已被證明可以提高用戶參與度。 目標是每秒渲染 60 幀。
對於這個演示,我們將檢索一個靜態 JSON 文件,而不是一個真正的 API。 這只是為了讓事情變得簡單。 在現實世界中,您將查詢 API 或使用 WebSockets。
索引.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sky-High Airport Arrivals</title> <link async rel="stylesheet" href="./css/style.css"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,600,300italic,600italic" rel="stylesheet" type="text/css"> </head> <body> <header> <div class="content"> <h3>Arrivals</h3> </div> </header> <div class="container"> <div class="content"> <ul class="arrivals-list" data-bind="foreach: arrivals"> <li class="item"> <span class="title" data-bind="html: title"></span> <span class="status" data-bind="html: status"></span> <span class="time" data-bind="html: time"></span> </li> </ul> </div> </div> <script src="./js/build/vendor.min.js"></script> <script src="./js/build/script.min.js"></script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sky-High Airport Arrivals</title> <link async rel="stylesheet" href="./css/style.css"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,600,300italic,600italic" rel="stylesheet" type="text/css"> </head> <body> <header> <div class="content"> <h3>Arrivals</h3> </div> </header> <div class="container"> <div class="content"> <ul class="arrivals-list" data-bind="foreach: arrivals"> <li class="item"> <span class="title" data-bind="html: title"></span> <span class="status" data-bind="html: status"></span> <span class="time" data-bind="html: time"></span> </li> </ul> </div> </div> <script src="./js/build/vendor.min.js"></script> <script src="./js/build/script.min.js"></script> </body> </html>
index.html
文件是比較標準的。 我們創建了一個 HTML 列表,並使用 Knockout 通過屬性data-bind=“foreach: arrivals”
我們的視圖模型屬性arrivals
綁定到它。 視圖模型arrivals
在下面的page.js
文件中聲明,並在Page
模塊中公開。 在我們的 HTML 頁面上,對於arrivals
數組中的每個項目,我們已將title
、 status
和time
屬性綁定到 HTML 視圖。
page.js
(var Page = (function() { // declare the view model used within the page function ViewModel() { var self = this; self.arrivals = ko.observableArray([]); } // expose the view model through the Page module return { vm: new ViewModel(), hideOfflineWarning: function() { // enable the live data document.querySelector(".arrivals-list").classList.remove('loading') // remove the offline message document.getElementById("offline").remove(); // load the live data }, showOfflineWarning: function() { // disable the live data document.querySelector(".arrivals-list").classList.add('loading') // load html template informing the user they are offline var request = new XMLHttpRequest(); request.open('GET', './offline.html', true); request.onload = function() { if (request.status === 200) { // success // create offline element with HTML loaded from offline.html template var offlineMessageElement = document.createElement("div"); offlineMessageElement.setAttribute("id", "offline"); offlineMessageElement.innerHTML = request.responseText; document.getElementById("main").appendChild(offlineMessageElement); } else { // error retrieving file console.warn('Error retrieving offline.html'); } }; request.onerror = function() { // network errors console.error('Connection error'); }; request.send(); } } })();
這個page.js
文件公開了Page
模塊,其中包含我們的 ViewModel vm
和兩個函數hideOfflineWarning
和showOfflineWarning
。 View Model ViewModel
是一個簡單的 JavaScript 文字,將在整個應用程序中使用。 arrivals
上的屬性是 Knockout 的observableArray
,它自動將我們的 HTML 綁定到 JavaScript 數組,允許我們在 JavaScript 中將項目推送和彈出到我們的數組中,並自動更新頁面的 HTML。
函數hideOfflineWarning
和showOfflineWarning
使我們的應用程序的其餘部分能夠調用這些函數來更新顯示我們是否在線連接的頁面 UI。 showOfflineWarning
向我們的arrivals arrivals-list
HTML 元素添加了一個loading
類以淡化列表,然後它通過XHR 檢索HTML 文件offline.html
。 假設文件已成功檢索( response.status === 200
),我們將其附加到我們的 HTML 中。 當然,如果我們不使用 service workers 並且用戶沒有連接到互聯網,那麼就無法檢索到offline.html
,因此用戶會看到瀏覽器的離線頁面。

我們從 API 檢索數據並將其綁定到我們的視圖模型和視圖的業務邏輯可以在arrivals.js
中找到,並且是使用 Knockout 的標準 MVVM 功能。 在arrivals.js
文件中,我們簡單地初始化將在整個應用程序中使用的服務和視圖模型,並公開一個函數Arrivals.loadData()
——檢索數據並將其綁定到視圖模型。
網絡應用清單
讓我們的網絡應用程序更像應用程序。 Web 應用清單文件是遵循 W3C 規範的簡單 JSON 文件。 有了它,可以將 Web 應用程序作為獨立應用程序以全屏模式運行,分配在設備上安裝應用程序時將顯示的圖標,並為應用程序分配主題和背景顏色。 此外,Android 版 Chrome 將通過網絡應用安裝橫幅主動建議用戶安裝網絡應用。 要顯示安裝提示,您的 Web 應用程序需要:
- 有一個有效的網絡應用清單文件,
- 通過 HTTPS 提供服務,
- 註冊了有效的服務人員,
- 已訪問過兩次,每次訪問間隔至少五分鐘。

清單.json
{ "short_name": "Arrivals", "name": "Arrivals at Sky High", "description": "Progressive web application demonstration", "icons": [ { "src": "launcher-icon.png", "sizes": "48x48", "type": "image/png" }, { "src": "launcher-icon-96.png", "sizes": "96x96", "type": "image/png" }, { "src": "launcher-icon-144.png", "sizes": "144x144", "type": "image/png" }, { "src": "launcher-icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "launcher-icon-256.png", "sizes": "256x256", "type": "image/png" } ], "start_url": "./?utm_source=web_app_manifest", "display": "standalone", "orientation": "portrait", "theme_color": "#29BDBB", "background_color": "#29BDBB" }
讓我們分解這個清單文件:
-
short_name
是應用程序的人類可讀名稱。 在 Android 版 Chrome 中,這也是伴隨主屏幕圖標的名稱。 -
name
也是應用程序的可讀名稱,並定義應用程序的列出方式。 -
description
提供 Web 應用程序的一般描述。 -
icons
定義了一系列不同大小的圖像,這些圖像將用作應用程序的圖標集。 在 Android 版 Chrome 中,該圖標將用於初始屏幕、主屏幕和任務切換器。 -
start_url
是應用程序的起始 URL。 -
display
定義了 web 應用程序的默認顯示模式:fullscreen
、standalone
、minimal-ui
或browser
。 -
orientation
定義 Web 應用程序的默認方向:portrait
或landscape
。 -
theme_color
是應用程序的默認主題顏色。 在 Android 上,這也用於為狀態欄著色。 -
background_color
定義 Web 應用程序的背景顏色。 在 Chrome 中,它還定義了啟動畫面的背景顏色。 -
related_applications
在我們的示例中未實現,但用於指定各種應用商店中的本地應用程序替代方案。
將manifest.json
引用添加到index.html
文件的head
標籤:
<link rel="manifest" href="./manifest.json">
用戶將 Web 應用程序添加到他們的主屏幕後,他們將能夠立即從他們的設備重新使用您的應用程序,而無需直接打開瀏覽器。 您可以看到這不僅僅是一個網絡書籤。
從 Vimeo 上的 Smashing Magazine 添加到 Android 版 Chrome 上的主屏幕。
服務人員
漸進式網絡應用程序更令人興奮的方面之一是它們可以離線工作。 使用服務工作者,可以顯示在應用程序的先前會話中檢索到的數據(使用 IndexedDB),或者,顯示應用程序外殼並通知用戶他們沒有連接到互聯網(我們已經在這個演示中拍攝)。 一旦用戶重新連接,我們就可以從服務器檢索最新數據。
所有這一切都可以通過服務工作者實現,服務工作者是事件驅動的腳本(用 JavaScript 編寫),可以訪問域範圍的事件,包括網絡獲取。 有了它們,我們可以緩存所有靜態資源,這可以大大減少網絡請求並顯著提高性能。
應用程序外殼
應用程序外殼是驅動用戶界面所需的最低 HTML、CSS 和 JavaScript。 原生移動應用程序包括應用程序外殼作為其可分發的一部分,而網站通常通過網絡請求它。 漸進式 Web 應用程序通過將應用程序外殼的資源和資產放在瀏覽器的緩存中來彌補這一差距。 在我們的 Sky High 應用程序中,我們可以看到我們的應用程序外殼由頂部標題欄、字體和優雅呈現這些所需的任何 CSS 組成。
要開始使用 Service Worker,我們首先需要創建我們的 Service Worker 的 JavaScript 文件sw.js
,放在根目錄中。
sw.js
// Use a cacheName for cache versioning var cacheName = 'v1:static'; // During the installation phase, you'll usually want to cache static assets. self.addEventListener('install', function(e) { // Once the service worker is installed, go ahead and fetch the resources to make this work offline. e.waitUntil( caches.open(cacheName).then(function(cache) { return cache.addAll([ './', './css/style.css', './js/build/script.min.js', './js/build/vendor.min.js', './css/fonts/roboto.woff', './offline.html' ]).then(function() { self.skipWaiting(); }); }) ); }); // when the browser fetches a URL… self.addEventListener('fetch', function(event) { // … either respond with the cached object or go ahead and fetch the actual URL event.respondWith( caches.match(event.request).then(function(response) { if (response) { // retrieve from cache return response; } // fetch as normal return fetch(event.request); }) ); });
讓我們更仔細地看看我們的服務工作者。 首先,我們設置一個cacheName
變量。 這用於確定是否對我們的緩存資產進行了任何更改。 對於本例,我們將使用靜態名稱,這意味著我們的資產不會更改或需要更新。
self.addEventListener('install', function(e) { // declare which assets to cache }
install
事件在 Service Worker 的安裝階段觸發,如果 Service Worker 已經安裝,則只會觸發一次。 因此,刷新頁面不會再次觸發安裝階段。 在安裝階段,我們可以聲明將緩存哪些資產。 在上面的示例中,我們緩存了一個 CSS 文件、兩個 JavaScript 文件、我們的字體文件、我們的離線 HTML 模板,當然還有應用程序根目錄。 self.skipWaiting()
強制等待的服務工作者變得活躍。
到目前為止,我們已經聲明了我們的 service worker,但是在我們看到它生效之前,我們需要在我們的 JavaScript 中引用它。 在我們的應用程序中,我們在main.js
中註冊它
// Register the service worker if available. if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js').then(function(reg) { console.log('Successfully registered service worker', reg); }).catch(function(err) { console.warn('Error whilst registering service worker', err); }); } window.addEventListener('online', function(e) { // Resync data with server. console.log("You are online"); Page.hideOfflineWarning(); Arrivals.loadData(); }, false); window.addEventListener('offline', function(e) { // Queue up events for server. console.log("You are offline"); Page.showOfflineWarning(); }, false); // Check if the user is connected. if (navigator.onLine) { Arrivals.loadData(); } else { // Show offline message Page.showOfflineWarning(); } // Set Knockout view model bindings. ko.applyBindings(Page.vm);
我們還包括了兩個事件監聽器來檢查會話的狀態是否從online
變為offline
,反之亦然。 然後,事件處理程序調用不同的函數以通過Arrivals.loadData()
檢索數據,並分別通過Page.showOfflineWarning
和Page.hideOfflineWarning
啟用或禁用離線消息。 我們的應用程序還使用 navigator.onLine 檢查用戶當前是否在線,並相應地檢索數據或顯示離線警告。 在main.js
的最後一行,我們將 Knockout 綁定應用到 View Model Page.vm
如果我們第一次加載我們的應用程序(使用 Chrome 開發者工具),我們將看不到任何新東西。 但是,在重新加載時,我們會看到已經從 service worker 中檢索了一些網絡資源。 這是我們的應用程序外殼。

離線測試
在沒有 Internet 連接的情況下運行應用程序的用戶(假設他們已經在該頁面上)只會導致應用程序外殼和顯示離線警告——這是對 Chrome 的潛行 t-rex 的改進。 一旦用戶建立了網絡連接,我們就會禁用警告並檢索最新數據。

當離線用戶訪問其網站時,衛報採取了一種特別有趣的方法,提供了一個填字遊戲:

推送通知
推送通知允許用戶選擇及時更新他們信任的應用程序,幫助他們重新使用這些應用程序。 即使瀏覽器關閉,網絡上的推送通知也能讓您與觀眾互動。

Push API 在 Chrome、Opera 和三星的瀏覽器中受支持,並且正在 Firefox 和 Microsoft Edge 中開發。 不幸的是,沒有跡象表明該功能將在 Safari 中實現。
表現
服務工作者最簡單的勝利之一是我們可以毫不費力地提高性能。 在實施服務工作者之前,在我們在頁面加載時檢索超過 200 KB 之前,將我們的網站與自身進行比較; 現在減少到 13 KB。 在常規 3G 網絡上,頁面加載需要 3.5 秒; 現在需要 500 毫秒。
這些性能改進是巨大的,因為應用程序本身非常小並且功能有限。 然而,通過正確使用緩存,可以顯著提高性能和感知性能,尤其是對於連接性較低的地方的用戶。
燈塔
谷歌的 Chrome 團隊已經整合了一個用於測試漸進式網絡應用程序的工具。 Lighthouse 在 Node.js 中運行或作為 Chrome 插件運行,也可以在 GitHub 上找到。
要運行 Lighthouse 測試,您的網站需要在線可用,這意味著您無法在localhost
上進行測試。
首先,下載 npm 包:
npm install -g GoogleChrome/lighthouse
安裝後,運行 Chrome(52 及以上版本):
npm explore -g lighthouse -- npm run chrome lighthouse https://incredibleweb.github.io/pwa-tutorial/
Lighthouse 運行的輸出將在命令行中可見,並將根據您已實現的漸進式 Web 應用程序功能和屬性對您的網站進行評分 - 例如,您是否使用manifest.json
文件或您的頁面是否可以離線使用.
結論
本文只是漸進式 Web 應用程序的開胃菜。 我們可以做更多的事情來創造用戶正在尋找的類似應用程序的體驗,無論是通過使用 Push API 支持推送通知,使應用程序可重新參與,還是使用 IndexedDB 和後台同步來改善離線體驗。
跨瀏覽器支持
對於漸進式 Web 應用程序而言,這些仍處於早期階段,跨瀏覽器支持仍然有限,尤其是在 Safari 和 Edge 中。 但是,Microsoft 公開支持漸進式 Web 應用程序,並且應該在今年年底前實現更多功能。
- 服務工作者和緩存 API 。 在 Chrome、Firefox、Opera 和三星的瀏覽器中支持。 正在 Microsoft Edge 中開發,預計將於 2016 年底推出。正在考慮用於 Safari。
- 添加到主屏幕。 支持 Chrome、Firefox、Opera、Android 瀏覽器和三星的瀏覽器。 微軟似乎表示漸進式網絡應用程序將作為商店列表提供。 目前還沒有 Safari 的計劃。
- 推送 API 。 主要支持 Chrome、Firefox、Opera 和三星的瀏覽器。 在 Microsoft Edge 中進行開發。 目前還沒有 Safari 的計劃。
如果更多開發人員利用漸進式 Web 應用程序提供的功能(這些功能相對容易實現並提供即時獎勵),那麼用戶將更喜歡在受支持的瀏覽器中使用這些 Web 應用程序,希望能說服其他瀏覽器供應商適應。
源代碼
本教程的完整源代碼可在 Github 存儲庫中獲得,演示可在 GitHub Pages 上獲得。