使用 Webpack 和 Workbox 構建 PWA

已發表: 2022-03-10
快速總結↬本教程將幫助您將無法離線工作的應用程序轉換為離線工作並顯示更新可用圖標的 PWA。 您將學習如何使用工作箱預緩存資產、處理動態緩存以及處理 PWA 更新。 跟隨並了解如何在您的網站上應用這些技術。

漸進式 Web 應用程序 (PWA) 是一個使用現代技術在 Web 上提供類似應用程序體驗的網站。 它是“網絡應用清單”、“服務工作者”等新技術的總稱。 當這些技術結合在一起時,您可以通過您的網站提供快速且引人入勝的用戶體驗。

本文是向現有單頁網站添加 Service Worker 的分步教程。 Service Worker 將允許您使您的網站離線工作,同時通知您的用戶您的網站有更新。 請注意,這是基於與 Webpack 捆綁的一個小項目,因此我們將使用 Workbox Webpack 插件(Workbox v4)。

推薦使用工俱生成 Service Worker,因為它可以讓您有效地管理緩存。 在本教程中,我們將使用 Workbox(一組可輕鬆生成 Service Worker 代碼的庫)來生成我們的 Service Worker。

根據您的項目,您可以通過三種不同的方式使用 Workbox:

  1. 提供了一個命令行界面,可讓您將工作箱集成到您擁有的任何應用程序中;
  2. 一個可用的Node.js 模塊可以讓您將工作箱集成到任何 Node 構建工具中,例如 gulp 或 grunt;
  3. 提供了一個webpack 插件,它可以讓您輕鬆地與使用 Webpack 構建的項目集成。

Webpack 是一個模塊打包器。 為簡化起見,您可以將其視為管理 JavaScript 依賴項的工具。 它允許您從庫中導入 JavaScript 代碼,並將您的 JavaScript 捆綁到一個或多個文件中。

跳躍後更多! 繼續往下看↓

首先,在您的計算機上克隆以下存儲庫:

 git clone [email protected]:jadjoubran/workbox-tutorial-v4.git cd workbox-tutorial-v4 npm install npm run dev

接下來,導航到https://localhost:8080/ 。 您應該能夠看到我們將在本教程中使用的貨幣應用程序:

我們在本文中構建的貨幣應用程序的屏幕截圖。
“貨幣”是一種 PWA,列出了貨幣兌歐元 (€) 貨幣的兌換費用。 (大預覽)

從 App Shell 開始

應用程序外殼(或“應用程序外殼”)是一種受本機應用程序啟發的模式。 這將有助於為您的應用程序提供更原生的外觀。 它只是為應用程序提供了一個沒有任何數據的佈局和結構——一個旨在改善 Web 應用程序加載體驗的過渡屏幕。

以下是本機應用程序的一些應用程序外殼示例:

谷歌收件箱應用外殼
Google Inbox App Shell:電子郵件加載到 App Shell 之前的幾毫秒。 (大預覽)
Twitter 原生 App Shell
Twitter 在 Android 上的本機應用程序:顯示導航欄、選項卡和加載程序的應用程序外殼。 (大預覽)

以下是來自 PWA 的應用程序外殼示例:

Twitter PWA 的 App Shell
Twitter 的 PWA 的 app shell(大預覽)
Flipkart PWA 的 App Shell
Flipkart 的 PWA 的 app shell(大預覽)

用戶喜歡 app shell 的加載體驗,因為他們鄙視空白屏幕。 空白屏幕使用戶感覺網站沒有加載。 這讓他們感覺好像網站被卡住了。

應用程序外殼嘗試盡快繪製應用程序的導航結構,例如導航欄、標籤欄以及表示正在加載您請求的內容的加載器。

那麼如何構建 App Shell?

應用程序外殼模式優先加載將首先呈現的 HTML、CSS 和 JavaScript。 這意味著我們需要給予這些資源全部優先級,因此您必須內聯這些資產。 因此,要構建應用程序外殼,您只需內聯負責應用程序外殼的 HTML、CSS 和 JavaScript。 當然,您不應該將所有內容都內聯,而應將總大小控制在 30 到 40KB 左右。

您可以在index.html中看到內聯的應用程序外殼。 您可以通過檢查index.html文件來檢查源代碼,您可以通過刪除開發工具中的<main>元素在瀏覽器中預覽它:

帶有導航欄和加載器的貨幣 PWA App Shell
我們在本文中構建的 App Shell。 (大預覽)

它可以離線工作嗎?

讓我們模擬下線! 打開 DevTools,導航到網絡選項卡並勾選“離線”複選框。 當您重新加載頁面時,您會看到我們將獲取瀏覽器的離線頁面。

瀏覽器的離線錯誤頁面
對主頁的請求失敗了,所以我們顯然看到了這個結果。 (大預覽)

這是因為對/的初始請求(將加載index.html文件)將失敗,因為 Internet 處於脫機狀態。 我們從請求失敗中恢復的唯一方法是擁有一個服務工作者。

讓我們可視化沒有 service worker 的請求:

從客戶端瀏覽器到 Internet 的網絡請求動畫。
網絡請求從瀏覽器到 Internet 並返回。 (來自 flaticon.com 的圖標)(大預覽)

Service Worker 是一個可編程的網絡代理,這意味著它位於您的網頁和 Internet 之間。 這使您可以控制傳入和傳出的網絡請求。

服務工作者截獲的網絡請求動畫。
網絡請求被服務工作者攔截。 (來自 flaticon.com 的圖標)(大預覽)

這是有益的,因為我們現在可以將此失敗的請求重新路由到緩存(假設我們在緩存中有內容)。

服務工作者攔截並重定向到緩存的網絡請求的動畫。
當網絡請求已經存在於緩存中時,它會被重定向到緩存。 (來自 flaticon.com 的圖標)(大預覽)

Service Worker 也是一種 Web Worker,這意味著它與您的主頁分開運行,並且無法訪問windowdocument對象。

預緩存 App Shell

為了使我們的應用程序離線工作,我們將從預緩存應用程序外殼開始。

因此,讓我們從安裝 Webpack Workbox 插件開始:

 npm install --save-dev workbox-webpack-plugin

然後我們將打開我們的index.js文件並註冊 service worker:

 if ("serviceWorker" in navigator){ window.addEventListener("load", () => { navigator.serviceWorker.register("/sw.js"); }) }

接下來,打開webpack.config.js文件,讓我們配置 Workbox webpack 插件:

 //add at the top const WorkboxWebpackPlugin = require("workbox-webpack-plugin"); //add inside the plugins array: plugins: [ … , new WorkboxWebpackPlugin.InjectManifest({ swSrc: "./src/src-sw.js", swDest: "sw.js" }) ]

這將指示 Workbox 使用我們的./src/src-sw.js文件作為基礎。 生成的文件將被稱為sw.js並將位於dist文件夾中。

然後在根級別創建一個./src/src-sw.js文件,並在其中寫入以下內容:

 workbox.precaching.precacheAndRoute(self.__precacheManifest);

注意 self.__precacheManifest變量將從工作箱動態生成的文件中導入。

現在您已準備好使用npm run build構建代碼,Workbox 將在dist文件夾中生成兩個文件:

  • precache-manifest.66cf63077c7e4a70ba741ee9e6a8da29.js
  • sw.js

sw.js從 CDN 以及precache-manifest.[chunkhash].js 導入工作箱

 //precache-manifest.[chunkhash].js file self.__precacheManifest = (self.__precacheManifest || []).concat([ "revision": "ba8f7488757693a5a5b1e712ac29cc28", "url": "index.html" }, "url": "main.49467c51ac5e0cb2b58e.js" ]);

預緩存清單列出了由 webpack 處理並最終在您的dist文件夾中的文件的名稱。 我們將使用這些文件在瀏覽器中預緩存它們。 這意味著當你的網站第一次加載並註冊 service worker 時,它會緩存這些資產,以便下次使用。

您還可以注意到某些條目有“修訂”,而其他條目則沒有。 那是因為有時可以從文件名中的 chunkhash 中推斷出修訂。 例如,讓我們仔細看看文件名main.49467c51ac5e0cb2b58e.js 。 它在文件名中有一個修訂版,即 chunkhash 49467c51ac5e0cb2b58e

這允許 Workbox 了解您的文件何時更改,以便它只清理或更新已更改的文件,而不是在您每次發布新版本的 Service Worker 時轉儲所有緩存。

第一次加載頁面時,Service Worker 將安裝。 您可以在 DevTools 中看到這一點。 首先,請求sw.js文件,然後請求所有其他文件。 它們清楚地標有齒輪圖標。

安裝服務工作者時 DevTools Network 選項卡的屏幕截圖。
標有 ️ 圖標的請求是由 service worker 發起的請求。 (大預覽)

因此 Workbox 將初始化,並將預緩存 precache-manifest 中的所有文件。 請務必仔細檢查precache-manifest文件中是否沒有任何不必要的文件,例如.map文件或不屬於應用程序外殼的文件。

在網絡選項卡中,我們可以看到來自服務工作者的請求。 現在,如果您嘗試離線,應用程序外殼已經預先緩存,因此即使我們離線也能正常工作!

顯示 API 調用失敗的 Dev Tools Network 選項卡的屏幕截圖。
當我們離​​線時 API 調用失敗。 (大預覽)

緩存動態路由

您是否注意到當我們離線時,應用程序外殼可以工作,但我們的數據卻不能工作? 這是因為這些 API 調用不是預緩存的 app shell的一部分。 當沒有 Internet 連接時,這些請求將失敗,用戶將無法看到貨幣信息。

但是,這些請求不能被預緩存,因為它們的值來自 API。 此外,當您開始擁有多個頁面時,您不希望一次性緩存所有 API 請求。 相反,您希望在用戶訪問該頁面時緩存它們。

我們稱這些為“動態數據”。 它們通常包括當用戶在您的網站上執行特定操作時(例如,當他們瀏覽到新頁面時)請求的 API 調用以及圖像和其他資產。

您可以使用 Workbox 的路由模塊緩存這些。 這是如何做:

 //add in src/src-sw.js workbox.routing.registerRoute( /https:\/\/api\.exchangeratesapi\.io\/latest/, new workbox.strategies.NetworkFirst({ cacheName: "currencies", plugins: [ new workbox.expiration.Plugin({ maxAgeSeconds: 10 * 60 // 10 minutes }) ] }) );

這將為與 URL https://api.exchangeratesapi.io/latest匹配的任何請求 URL 設置動態緩存。

我們在這裡使用的緩存策略稱為NetworkFirst ; 還有另外兩個經常使用的:

  1. CacheFirst
  2. StaleWhileRevalidate

CacheFirst將首先在緩存中查找它。 如果沒有找到,那麼它將從網絡中獲取。 StaleWhileRevalidate將同時進入網絡和緩存。 返回緩存對頁面的響應(在後台時),它將使用新的網絡響應來更新緩存以供下次使用。

對於我們的用例,我們必須使用NetworkFirst ,因為我們要處理經常變化的貨幣匯率。 但是,當用戶離線時,我們至少可以向他們顯示 10 分鐘前的費率——這就是我們使用過期插件並將maxAgeSeconds設置為10 * 60秒的原因。

管理應用更新

每次用戶加載您的頁面時,瀏覽器都會運行navigator.serviceWorker.register代碼,即使服務工作者已經安裝並正在運行。 這允許瀏覽器檢測是否有新版本的 service worker。 當瀏覽器注意到文件沒有改變時,它只是跳過註冊調用。 一旦該文件發生更改,瀏覽器就會知道有一個新版本的 service worker,因此它會將新的 service worker 與當前正在運行的 service worker 並行安裝

但是,它會在installed/waiting階段暫停,因為只能同時激活一個服務工作者。

Service Worker 的生命週期:已解析、已安裝/等待、已激活和冗餘
Service Worker 的簡化生命週期(大預覽)

只有當之前的 Service Worker 控制的所有瀏覽器窗口都關閉時,新的 Service Worker 才能安全激活。

您還可以通過調用skipWaiting() (或self.skipWaiting()來手動控制它,因為self是服務工作者中的全局執行上下文)。 但是,大多數情況下,您應該只在詢問用戶是否想要獲得最新更新後才這樣做。

值得慶幸的是, workbox-window幫助我們實現了這一目標。 它是 Workbox v4 中引入的一個新窗口庫,旨在簡化窗口側的常見任務。

讓我們從安裝它開始:

 npm install workbox-window

接下來,在index.js文件的頂部導入Workbox

 import { Workbox } from "workbox-window";

然後我們將用下面的代碼替換我們的註冊碼:

 if ("serviceWorker" in navigator) { window.addEventListener("load", () => { const wb = new Workbox("/sw.js"); wb.register(); }); }

然後我們將找到具有 ID 的更新按鈕應用更新並監聽workbox-waiting事件:

 //add before the wb.register() const updateButton = document.querySelector("#app-update"); // Fires when the registered service worker has installed but is waiting to activate. wb.addEventListener("waiting", event => { updateButton.classList.add("show"); updateButton.addEventListener("click", () => { // Set up a listener that will reload the page as soon as the previously waiting service worker has taken control. wb.addEventListener("controlling", event => { window.location.reload(); }); // Send a message telling the service worker to skip waiting. // This will trigger the `controlling` event handler above. wb.messageSW({ type: "SKIP_WAITING" }); }); });

此代碼將在有新更新時顯示更新按鈕(因此當服務工作人員處於等待狀態時)並將向服務工作人員發送SKIP_WAITING消息。

我們需要更新 service worker 文件並處理SKIP_WAITING事件,以便它調用skipWaiting

 //add in src-sw.js addEventListener("message", event => { if (event.data && event.data.type === "SKIP_WAITING") { skipWaiting(); });

現在運行npm run dev然後重新加載頁面。 進入您的代碼並將導航欄標題更新為“Navbar v2”。 再次重新加載頁面,您應該能夠看到更新圖標。

包起來

我們的網站現在可以離線運行,並且能夠告訴用戶新的更新。 但請記住,構建 PWA 時最重要的因素是用戶體驗。 始終專注於構建易於用戶使用的體驗。 作為開發人員,我們往往對技術過於興奮,最終常常忘記我們的用戶。

如果您想更進一步,您可以添加一個 Web 應用程序清單,它允許您的用戶將站點添加到他們的主屏幕。 如果您想了解更多關於 Workbox 的信息,您可以在 Workbox 網站上找到官方文檔。

關於 SmashingMag 的進一步閱讀

  • 您可以通過移動應用程序或 PWA 賺更多的錢嗎?
  • 漸進式 Web 應用程序的詳盡指南
  • Native 和 PWA:選擇,而不是挑戰者!
  • 使用 Angular 6 構建 PWA