Anthony Fu的VitePWA插件是您的Vite网站的绝佳工具。它可以帮助您添加一个服务人员来处理:
- 离线支持
- 缓存资产和内容
- 当有新内容可用时提示用户
…还有其他好东西!
我们将一起探讨服务工作者的概念,然后直接用VitePWA p制作一个
Service workers, 介绍
在进入VitePWA插件之前,让我们简单地谈谈Service Worker本身。
服务工作者是在web应用程序中的独立线程上运行的后台进程。服务人员有能力拦截网络请求并做任何事情。可能性之大令人惊讶。例如,您可以拦截对TypeScript文件的请求并实时编译它们。或者,您可以拦截对视频文件的请求,并执行浏览器当前不支持的高级转码。不过,更常见的情况是,服务工作者用于缓存资产,既可以提高网站的性能,也可以使其在离线时做一些事情。
当有人第一次登录您的网站时,VitePWA插件创建的服务人员将安装并通过利用Cache Storage API缓存您的所有HTML、CSS和JavaScript文件。结果是,在随后访问您的网站时,浏览器将从缓存中加载这些资源,而不需要发出网络请求。即使是在第一次访问您的网站时,由于服务人员刚刚预缓存了所有内容,您的用户单击的下一个位置可能已经预缓存,从而允许浏览器完全绕过网络请求。
Versioning 和manifests
您可能想知道当您的代码更新时,服务人员会发生什么。如果你的服务人员正在缓存一个foo.js文件,并且你修改了该文件,你希望服务人员在下次用户访问该网站时下拉更新的版本。
但在实践中,您没有foo.js文件。通常,构建系统会创建类似foo-ABC123.js的东西,其中“ABC123”是文件的散列。如果你更新了foo.js,你的站点的下一次部署可能会通过foo-XYZ987.js发送。服务人员是如何处理的?
结果发现服务工作者API是一个非常低级的原语。如果您正在寻找它和缓存API之间的本地交钥匙解决方案,您会很失望。基本上,服务工作者的创建在一定程度上需要自动化,并连接到构建系统。您需要查看构建创建的所有资产,将这些文件名硬编码到服务工作者中,并有代码预缓存它们,更重要的是,跟踪缓存的文件。
如果代码更新,服务工作者文件也会发生更改,其中包含新的文件名和散列。当用户下次访问该应用程序时,新的服务工作者将需要安装新的文件清单,并将其与当前缓存中的清单进行比较,在缓存新内容的同时弹出不再需要的文件。
这是一个荒谬的工作量和难以置信的难以做到正确。虽然这可能是一个有趣的项目,但在实践中,你会想使用一个成熟的产品来培养你的服务人员——最好的产品是Workbox,它来自谷歌的员工。
甚至Workbox也是一个低级的原语。它需要有关预缓存文件的详细信息,这些文件隐藏在构建工具中。这就是我们使用VitePWA插件的原因。它在后台使用Workbox,并使用Vite创建的捆绑包的所有信息对其进行配置。毫不奇怪,如果你碰巧喜欢使用这些捆绑器,也有webpack和Rollup插件。
我们的第一个service worker
我假设你已经有了一个基于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中提供等效内容(如果它在那里的话)。
但一个简单得多的选项是检测用户是否离线,显示关于离线的消息,然后绕过数据请求。这是一个独立的主题,我已经写了更详细的内容。
在向您展示如何编写和集成您自己的服务工作者内容之前,让我们仔细看看我们现有的服务工作者。特别是,让我们看看它是如何管理更新/更改内容的。即使使用VitePWA插件,这也非常棘手且容易出错。
在继续之前,一定要告诉Chrome DevTools让你重新上线。
如何更新?
仔细看看当我们更改内容时,我们的网站会发生什么。我们将继续删除现有的服务人员,这可以在DevTools的“应用程序”选项卡的“存储”下进行。
单击“清除站点数据”按钮以获得全新的记录。当我在做的时候,我会删除我自己网站的大部分路线,这样资源就会更少,然后让Vite重建应用程序。
在生成的sw.js中查看生成的Workbox服务工作者。里面应该有一个预缓存清单。我的清单看起来是这样的:
现在让我们运行该网站,看看我们的缓存中有什么:
让我们关注settings.js文件。Vite根据其内容的哈希生成了assets/settings.cb0800c2.js。Workbox独立于Vite,生成了相同文件的自己的哈希。如果相同的文件名是用不同的内容生成的,那么将重新生成一个新的服务工作者,使用不同的预缓存清单(相同的文件,但不同的修订版),Workbox将知道缓存新版本,并在不再需要时删除旧版本。
同样,文件名总是不同的,因为我们使用的是将哈希代码注入文件名的bundler,但Workbox支持不这样做的开发环境。
如果我们更新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();
}
});
}
我使用toast-ify js
库来显示一个toast UI
组件,让用户知道何时有新版本的服务工作者可用并等待。如果用户单击toast
,我将调用VitePWA提供的函数来重新加载页面,并运行新的服务工作者。
这里需要记住的一件事是,在部署代码以显示toast之后,下次加载网站时,toast组件将不会显示。这是因为旧的服务工作者(在我们添加toast组件之前的那个)仍在运行。这需要手动关闭所有选项卡,然后重新打开web应用程序,由新的服务人员接管。然后,下次更新某些代码时,服务人员应该显示toast,提示您进行更新。
为什么服务工作者在刷新页面时不更新?我之前提到过刷新页面不会更新或激活等待的服务人员,那么为什么这样做有效呢?调用此方法不仅可以刷新页面,还可以调用一些低级的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模式匹配的任何内容。如果你想深入了解细节,这些文档会提供更多信息。
现在,在更新生效后,我们可以看到这些资源由我们的服务人员提供服务。
添加你自己的Service worker内容
比方说,你想和你的服务人员一起进步。您想添加一些代码来与IndexedDB同步数据,添加提取处理程序,并在用户离线时使用Indexed数据库数据进行响应(同样,我之前的文章介绍了IndexedDB的来龙去脉)。但是,您如何将自己的代码放入Vite为我们创建的服务人员中呢?
我们可以使用另一个Workbox选项:importScripts。
VitePWA({
workbox: {
importScripts: ["sw-code.js"],
在这里,服务工作者将在运行时请求sw-code.js。在这种情况下,请确保应用程序可以提供sw-code.js文件。实现这一点的最简单方法是将其放在公共文件夹中(有关详细说明,请参阅Vite文档)。
如果此文件开始增长到需要使用JavaScript导入进行分解的大小,请确保将其捆绑在一起,以防止您的服务工作者尝试执行导入语句(可能无法执行,也可能无法执行)。您可以创建一个单独的Vite构建。
总结
2021年底,CSS Tricks
询问了一群前端人员,有一件事可以让他们的网站变得更好。Chris Ferdinandi
建议找一位服务人员。好吧,这正是我们在这篇文章中完成的,它相对简单,不是吗?这要归功于向Workbox
和Cache API
提供帽子提示的VitePWA
。
利用Cache API的服务人员能够极大地提高web应用程序的性能。虽然一开始可能看起来有点可怕或令人困惑,但很高兴知道我们有像VitePWA插件这样的工具可以大大简化事情。安装插件,让它完成繁重的工作。当然,服务人员可以做更高级的事情,VitePWA
可以用于更复杂的功能,但离线网站是一个极好的起点!