使用 Webpack 和 Workbox 构建 PWA
已发表: 2022-03-10渐进式 Web 应用程序 (PWA) 是一个使用现代技术在 Web 上提供类似应用程序体验的网站。 它是“网络应用清单”、“服务工作者”等新技术的总称。 当这些技术结合在一起时,您可以通过您的网站提供快速且引人入胜的用户体验。
本文是向现有单页网站添加 Service Worker 的分步教程。 Service Worker 将允许您使您的网站离线工作,同时通知您的用户您的网站有更新。 请注意,这是基于与 Webpack 捆绑的一个小项目,因此我们将使用 Workbox Webpack 插件(Workbox v4)。
推荐使用工具生成 Service Worker,因为它可以让您有效地管理缓存。 在本教程中,我们将使用 Workbox(一组可轻松生成 Service Worker 代码的库)来生成我们的 Service Worker。
根据您的项目,您可以通过三种不同的方式使用 Workbox:
- 提供了一个命令行界面,可让您将工作箱集成到您拥有的任何应用程序中;
- 一个可用的Node.js 模块可以让您将工作箱集成到任何 Node 构建工具中,例如 gulp 或 grunt;
- 提供了一个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/
。 您应该能够看到我们将在本教程中使用的货币应用程序:
从 App Shell 开始
应用程序外壳(或“应用程序外壳”)是一种受本机应用程序启发的模式。 这将有助于为您的应用程序提供更原生的外观。 它只是为应用程序提供了一个没有任何数据的布局和结构——一个旨在改善 Web 应用程序加载体验的过渡屏幕。
以下是本机应用程序的一些应用程序外壳示例:
以下是来自 PWA 的应用程序外壳示例:
用户喜欢 app shell 的加载体验,因为他们鄙视空白屏幕。 空白屏幕使用户感觉网站没有加载。 这让他们感觉好像网站被卡住了。
应用程序外壳尝试尽快绘制应用程序的导航结构,例如导航栏、标签栏以及表示正在加载您请求的内容的加载器。
那么如何构建 App Shell?
应用程序外壳模式优先加载将首先呈现的 HTML、CSS 和 JavaScript。 这意味着我们需要给予这些资源全部优先级,因此您必须内联这些资产。 因此,要构建应用程序外壳,您只需内联负责应用程序外壳的 HTML、CSS 和 JavaScript。 当然,您不应该将所有内容都内联,而应将总大小控制在 30 到 40KB 左右。
您可以在index.html中看到内联的应用程序外壳。 您可以通过检查index.html文件来检查源代码,您可以通过删除开发工具中的<main>
元素在浏览器中预览它:
它可以离线工作吗?
让我们模拟下线! 打开 DevTools,导航到网络选项卡并勾选“离线”复选框。 当您重新加载页面时,您会看到我们将获取浏览器的离线页面。
这是因为对/
的初始请求(将加载index.html文件)将失败,因为 Internet 处于脱机状态。 我们从请求失败中恢复的唯一方法是拥有一个服务工作者。
让我们可视化没有 service worker 的请求:
Service Worker 是一个可编程的网络代理,这意味着它位于您的网页和 Internet 之间。 这使您可以控制传入和传出的网络请求。
这是有益的,因为我们现在可以将此失败的请求重新路由到缓存(假设我们在缓存中有内容)。
Service Worker 也是一种 Web Worker,这意味着它与您的主页分开运行,并且无法访问window
或document
对象。
预缓存 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文件,然后请求所有其他文件。 它们清楚地标有齿轮图标。
因此 Workbox 将初始化,并将预缓存 precache-manifest 中的所有文件。 请务必仔细检查precache-manifest文件中是否没有任何不必要的文件,例如.map文件或不属于应用程序外壳的文件。
在网络选项卡中,我们可以看到来自服务工作者的请求。 现在,如果您尝试离线,应用程序外壳已经预先缓存,因此即使我们离线也能正常工作!
缓存动态路由
您是否注意到当我们离线时,应用程序外壳可以工作,但我们的数据却不能工作? 这是因为这些 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
; 还有另外两个经常使用的:
-
CacheFirst
-
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 才能安全激活。
您还可以通过调用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