VitePWA 插件 来自 Anthony Fu 是一个适用于 Vite 驱动的网站的绝佳工具。 它可以帮助您添加一个处理以下内容的服务工作者:
- 离线支持
- 缓存资产和内容
- 在有新内容可用时提示用户
- …以及其他好处!
我们将一起了解服务工作者的概念,然后直接使用 VitePWA 插件创建一个服务工作者。
不熟悉 Vite? 请查看 我之前的文章 以获取介绍。
内容目录
服务工作者简介
在深入了解 VitePWA 插件之前,让我们简要讨论一下 Service Worker 本身。
一个 服务工作者 是一个在 Web 应用程序中独立线程上运行的后台进程。 服务工作者能够拦截网络请求并执行…任何操作。 可能性出奇地广泛。 例如,您可以拦截对 TypeScript 文件的请求并在运行时编译它们。 或者,您可以拦截对视频文件的请求并执行浏览器当前不支持的高级转码。 不过,更常见的是,服务工作者用于缓存资产,以提高网站性能并使其能够在离线时执行某些操作。
当某人第一次访问您的网站时,VitePWA 插件创建的服务工作者会安装并通过利用 Cache Storage API 缓存所有 HTML、CSS 和 JavaScript 文件。 结果是,在后续访问您的网站时,浏览器将从缓存中加载这些资源,而无需发出网络请求。 即使在第一次访问您的网站时,由于服务工作者刚刚预缓存了所有内容,用户点击的下一个位置可能已经预缓存了,从而允许浏览器完全绕过网络请求。
版本控制和清单
您可能想知道当您的代码更新时服务工作者会发生什么。 如果您的服务工作者正在缓存例如 foo.js
文件,并且您修改了该文件,您希望服务工作者在下一次用户访问网站时下载更新的版本。
但在实践中,您没有 foo.js
文件。 通常,构建系统会创建类似 foo-ABC123.js
的内容,其中“ABC123”是文件的哈希值。 如果您更新 foo.js
,则网站的下一个部署可能会发送 foo-XYZ987.js
。 服务工作者如何处理这种情况?
事实证明,Service Worker API 是一个非常底层的原语。 如果您正在寻找它与缓存 API 之间的原生一键式解决方案,您会失望的。 **基本上,您的服务工作者的创建需要部分自动化,并且与构建系统连接。** 您需要查看构建创建的所有资产,将这些文件名硬编码到服务工作者中,编写代码来预缓存它们,更重要的是,跟踪已缓存的文件。
如果代码更新,服务工作者文件也会更改,其中包含新文件名,以及哈希值。 当用户下次访问应用程序时,新的服务工作者将需要安装,并将新的文件清单与当前缓存中的清单进行比较,弹出不再需要的文件,同时缓存新内容。
这是一项繁琐的工作,并且非常难以正确完成。 虽然它可能是一个有趣的项目,但在实践中,您需要使用成熟的产品来生成您的服务工作者——而最好的产品是 Workbox,它来自 Google 的团队。
即使是 Workbox 也有些底层。 它需要有关您正在预缓存的文件的详细信息,这些文件隐藏在您的构建工具中。 这就是我们使用 VitePWA 插件的原因。 它在后台使用 Workbox,并使用 Vite 创建的捆绑包的所有必要信息对其进行配置。 毫不奇怪,如果您碰巧更喜欢使用这些捆绑器,还有 webpack 和 Rollup 插件。
我们的第一个服务工作者
我假设您已经有一个基于 Vite 的网站。 如果没有,请随时从任何 可用模板 中 创建一个。
首先,我们安装 VitePWA 插件
npm i vite-plugin-pwa
我们将插件导入到 Vite 配置中
import { VitePWA } from "vite-plugin-pwa"
然后我们也在配置中使用它
plugins: [
VitePWA()
我们稍后会添加更多选项,但这就是创建功能强大的服务工作者所需的一切。 现在让我们使用以下代码在应用程序的入口处注册它
import { registerSW } from "virtual:pwa-register";
if ("serviceWorker" in navigator) {
// && !/localhost/.test(window.location)) {
registerSW();
}
不要让注释掉的代码让您困惑。 事实上,它非常重要,因为它可以防止服务工作者在开发过程中运行。 我们只希望在开发以外的任何地方安装服务工作者,即除非我们正在开发服务工作者本身,在这种情况下,我们可以取消注释该检查(并在将代码推送到主分支之前恢复)。
让我们打开一个新的浏览器,启动 DevTools,导航到“网络”选项卡,并运行 Web 应用程序。 所有内容都应按预期加载。 不同之处在于,您应该在 DevTools 中看到大量网络请求。

那是 Workbox 预缓存捆绑包。 事情正在按计划进行!
离线功能如何实现?
因此,我们的服务工作者正在预缓存我们所有捆绑的资产。 这意味着它将从缓存中提供这些资产,甚至无需访问网络。 这是否意味着我们的服务工作者即使在用户没有网络访问权限的情况下也能提供资产? 的确如此!
并且,信不信由你,它已经完成了。 通过在 DevTools 中打开“网络”选项卡并告诉 Chrome 模拟离线模式来尝试一下,如下所示。

让我们刷新页面。 您应该会看到所有内容都已加载。 当然,如果您正在运行任何网络请求,您会看到它们永远挂起,因为您处于离线状态。 即使在这里,您也可以做一些事情。 现代浏览器附带了自己的内部持久数据库,称为 IndexedDB。 没有什么可以阻止您编写自己的代码来将一些数据同步到那里,然后编写一些自定义服务工作者代码来拦截网络请求,确定用户是否处于离线状态,然后如果数据在 IndexedDB 中,则从 IndexedDB 中提供等效的内容。
但是,一个更简单的选项是检测 用户是否处于离线状态,显示一条关于离线的消息,然后绕过数据请求。 这是一个独立的话题,我在 另一篇文章 中更详细地介绍了它。
在向您展示如何编写和集成您自己的服务工作者内容之前,让我们仔细看看我们现有的服务工作者。 特别是,让我们看看它如何管理更新/更改内容。 即使使用 VitePWA 插件,这也很棘手,也很容易出错。
在继续之前,请确保您告诉 Chrome DevTools 将您重新连接到线上。
服务工作者如何更新
仔细查看当我们更改内容时网站会发生什么。 我们将继续删除现有的服务工作者,我们可以在 DevTools 的“应用程序”选项卡下的“存储”中执行此操作。

点击“清除站点数据”按钮以获得一个干净的状态。趁此机会,我将删除自己站点的大部分路由,以便减少资源,然后让 Vite 重新构建应用程序。
查看生成的sw.js
以查看生成的 Workbox 服务工作者。其中应该包含一个预缓存清单。我的看起来像这样

如果sw.js
已压缩,请通过 Prettier 运行它,以便更容易阅读。
现在让我们运行站点并查看缓存中的内容

让我们关注settings.js 文件
。Vite 基于其内容的哈希值生成了assets/settings.ccb080c2.js
。Workbox 独立于 Vite,生成了它自己对同一文件的哈希值。如果生成具有不同内容的相同文件名,则会重新生成新的服务工作者,并具有不同的预缓存清单(相同的文件,但版本不同),并且 Workbox 会知道缓存新版本,并在不再需要时删除旧版本。
同样,文件名将始终不同,因为我们使用的是将哈希代码注入文件名中的捆绑器,但 Workbox 支持不执行此操作的开发环境。
自撰写本文以来,VitePWA 插件已更新,不再注入这些修订版哈希值。如果您尝试按照本文中的步骤操作,此特定步骤可能与您的实际体验略有不同。请参阅此 GitHub 问题以获取更多上下文。
如果我们更新settings.js
文件,则 Vite 将在我们构建中创建一个新文件,并使用新的哈希代码,Workbox 将将其视为新文件。让我们看看它的实际效果。在更改文件并重新运行 Vite 构建后,我们的预缓存清单如下所示

现在,当我们刷新页面时,先前服务工作者仍然在运行并加载先前的文件。然后,下载并预缓存具有新预缓存清单的新服务工作者。

请注意此处的推论:我们的旧内容仍在提供给用户,因为旧的服务工作者仍在运行。即使用户刷新页面,也无法看到我们刚刚所做的更改,因为服务工作者默认情况下保证具有此 Web 应用程序的所有选项卡都运行相同的版本。如果您希望浏览器显示更新的版本,请关闭您的选项卡(以及站点的所有其他选项卡),然后重新打开它。

Workbox 做了所有使这一切都正确的工作!我们几乎没有做任何事情来启动它。
更新内容的更好方法
您不太可能在用户碰巧关闭所有浏览器选项卡之前就能够使用陈旧的内容。幸运的是,VitePWA 插件提供了更好的方法。registerSW
函数接受一个对象,该对象具有一个onNeedRefresh
方法。每当有新的服务工作者等待接管时,都会调用此方法。registerSW
还会返回一个函数,您可以调用该函数来重新加载页面,并在过程中激活新的服务工作者。
内容很多,所以让我们看看一些代码
if ("serviceWorker" in navigator) {
// && !/localhost/.test(window.location) && !/lvh.me/.test(window.location)) {
const updateSW = registerSW({
onNeedRefresh() {
Toastify({
text: `<h4 style='display: inline'>An update is available!</h4>
<br><br>
<a class='do-sw-update'>Click to update and reload</a> `,
escapeMarkup: false,
gravity: "bottom",
onClick() {
updateSW(true);
}
}).showToast();
}
});
}
我正在使用toastify-js 库来显示一个吐司 UI 组件,以让用户知道何时有新的服务工作者版本可用并正在等待。如果用户点击吐司,我会调用 VitePWA 提供给我的函数来重新加载页面,并运行新的服务工作者。

这里要记住的一件事是,在您部署代码以显示吐司后,下次加载站点时,吐司组件不会显示。这是因为旧的服务工作者(我们在添加吐司组件之前的那个)仍在运行。这需要手动关闭所有选项卡并重新打开 Web 应用程序才能让新的服务工作者接管。然后,下次更新一些代码时,服务工作者应该显示吐司,提示您更新。
为什么刷新页面时服务工作者不更新?我之前提到过刷新页面不会更新或激活等待中的服务工作者,那么为什么这起作用呢?调用此方法不仅会刷新页面,还会调用一些低级 Service Worker API(特别是skipWaiting
),从而为我们提供我们想要的结果。
运行时缓存
我们已经看到了使用 VitePWA 免费获得的构建资产的捆绑预缓存。那么在运行时我们可能请求的任何其他内容的缓存呢?Workbox 通过其runtimeCaching
功能支持此功能。
方法如下。VitePWA 插件可以接受一个对象,其中一个属性是workbox
,它采用 Workbox 属性。
const getCache = ({ name, pattern }: any) => ({
urlPattern: pattern,
handler: "CacheFirst" as const,
options: {
cacheName: name,
expiration: {
maxEntries: 500,
maxAgeSeconds: 60 * 60 * 24 * 365 * 2 // 2 years
},
cacheableResponse: {
statuses: [200]
}
}
});
// ...
plugins: [
VitePWA({
workbox: {
runtimeCaching: [
getCache({
pattern: /^https:\/\/s3.amazonaws.com\/my-library-cover-uploads/,
name: "local-images1"
}),
getCache({
pattern: /^https:\/\/my-library-cover-uploads.s3.amazonaws.com/,
name: "local-images2"
})
]
}
})
],
// ...
我知道,代码很多。但它实际上只是告诉 Workbox 缓存它看到的所有与这些 URL 模式匹配的内容。文档提供了更多信息,如果您想深入了解细节。
现在,在该更新生效后,我们可以看到服务工作者正在提供这些资源。

并且我们可以看到创建的相应缓存。

添加您自己的服务工作者内容
假设您想更高级地使用服务工作者。您想添加一些代码以将数据与 IndexedDB 同步,添加获取处理程序,并在用户离线时使用 IndexedDB 数据进行响应(同样,我之前的帖子介绍了 IndexedDB 的来龙去脉)。但是,您如何将自己的代码放入 Vite 为我们创建的服务工作者中呢?
我们可以为此使用另一个 Workbox 选项:importScripts
。
VitePWA({
workbox: {
importScripts: ["sw-code.js"],
在这里,服务工作者将在运行时请求sw-code.js
。在这种情况下,请确保有一个sw-code.js
文件可以由您的应用程序提供服务。实现此目的最简单的方法是将其放在public
文件夹中(有关详细说明,请参阅Vite 文档)。
如果此文件开始增长到需要使用 JavaScript 导入来分解内容的大小,请确保将其捆绑起来,以防止服务工作者尝试执行导入语句(它可能能够或不能够执行)。您可以创建单独的 Vite 构建来代替。
总结
在 2021 年底,CSS-Tricks 询问了许多前端人员,为了使他们的网站变得更好,一个人可以做的一件事是什么。Chris Ferdinandi 建议使用服务工作者。好吧,这正是我们在本文中完成的事情,而且相对简单,不是吗?这要归功于 VitePWA,并向 Workbox 和 Cache API 致敬。
利用 Cache API 的服务工作者能够极大地提高 Web 应用程序的性能。虽然一开始它可能看起来有点吓人或令人困惑,但很高兴知道我们有像 VitePWA 插件这样的工具来大大简化事情。安装插件并让它完成繁重的工作。当然,服务工作者可以执行更多高级操作,并且 VitePWA 可用于更复杂的功能,但离线站点是一个极好的起点!
Adam,感谢您的帖子。继续努力。