使用VitePWA插件使网站离线工作的方法

1,230 阅读14分钟

来自Anthony FuVitePWA插件是一个为你的Vite-powered网站提供的奇妙工具。它可以帮助你添加一个服务工作者,处理。

  • 离线支持
  • 缓存资产和内容
  • 在有新内容时提示用户
  • ...以及其他好处

我们将一起了解服务工作者的概念,然后直接用VitePWA插件制作一个服务工作者。

刚接触Vite?请看我之前的文章的介绍。

目录

  1. 服务工作者,介绍
  2. 版本管理和清单
  3. 我们的第一个服务工作者
  4. 离线功能如何?
  5. 服务工作者如何更新
  6. 更新内容的更好方法
  7. 运行时缓存
  8. 添加您自己的服务工作者内容
  9. 收尾工作

服务工作者,介绍

在进入VitePWA插件之前,让我们简单地谈谈服务工作者本身。

服务工作者是一个后台进程,在你的Web应用程序中的一个独立线程上运行。服务工作者有能力拦截网络请求并做...任何事情。其可能性之大令人惊讶。例如,你可以拦截对TypeScript文件的请求,并在运行中编译它们。或者你可以拦截视频文件的请求,并执行浏览器目前不支持的高级转码。但更常见的是,服务工作者被用来缓存资产,这既是为了提高网站的性能,也是为了让网站在离线时能有所作为

当有人第一次登陆您的网站时,VitePWA插件创建的服务工作者会安装,并利用缓存存储API缓存您的所有HTML、CSS和JavaScript文件。其结果是,在随后访问您的网站时,浏览器将从缓存中加载这些资源,而不是需要进行网络请求。甚至在第一次访问您的网站时,由于服务工作者刚刚预先缓存了所有内容,您的用户点击的下一个地方可能已经预先缓存了,允许浏览器完全绕过网络请求。

版本管理和清单

你可能想知道当你的代码被更新时,服务工作者会发生什么。如果你的服务工作者正在缓存,例如,一个foo.js 文件,而你修改了该文件,你希望服务工作者在用户下次访问网站时拉下更新的版本。

但在实践中,你并没有一个foo.js 文件。通常,构建系统会创建类似foo-ABC123.js 的文件,其中 "ABC123 "是该文件的哈希值。如果你更新了foo.js ,你的网站的下一次部署可能会发过来foo-XYZ987.js 。服务工作者如何处理这个问题?

事实证明,服务工作者API是一个极其低级的原语。如果你想在它和缓存API之间寻找一个原生的交钥匙解决方案,你会失望的。基本上,你的服务工作者的创建需要部分自动化,并与构建系统相连。你需要看到你构建的所有资产,将这些文件名硬编码到服务工中,有代码来预先缓存它们,更重要的是,跟踪被缓存的文件。

如果代码更新,服务工作者的文件也会改变,其中包含新的文件名,并附有哈希值。当用户下次访问该应用时,新的服务工作者需要安装,并将新的文件清单与当前缓存中的清单进行比较,弹出不再需要的文件,同时缓存新的内容。

这是一个荒谬的工作量,而且非常难做好。虽然这可能是一个有趣的项目,但在实践中,你会想使用一个成熟的产品来生成你的服务工作者--而最好的产品是Workbox,它来自谷歌公司的员工。

即使是Workbox也是一个低级别的原始产品。它需要你预先缓存的文件的详细信息,这些信息埋藏在你的构建工具中。这就是我们使用VitePWA插件的原因。它在引擎盖下使用Workbox,并且用它所需要的关于Vite创建的捆绑文件的所有信息对其进行配置。不出所料,如果你碰巧喜欢使用这些捆绑工具,也有webpackRollup插件。

我们的第一个服务工作者

我假设你已经有一个基于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,导航到网络标签,并运行网络应用。一切都应该像你通常期望的那样加载。不同的是,你应该在DevTools中看到一连串的网络请求。

离线功能如何?

因此,我们的服务工作者正在预先缓存我们所有的捆绑资产。这意味着它将从缓存中为这些资产提供服务,甚至不需要访问网络。这是否意味着我们的服务工作者可以在用户没有网络访问的情况下提供资产?事实上,它是这样的

而且,不管你信不信,它已经做到了。通过在DevTools中打开网络选项卡,告诉Chrome浏览器模拟离线模式,就像这样,试一试吧。

Screenshot of the DevTools UO to simulate an offline connection with the select menu open. The No throttling option is currently checked but the Offline option is highlighted in light blue.

"无节流 "选项是默认选择。点击该选项,并选择 "离线 "选项来模拟离线连接。

让我们刷新一下页面。你应该看到一切都在加载。当然,如果你正在运行任何网络请求,你会看到它们永远挂起,因为你是离线的。不过,即使在这里,你也可以做一些事情。现代浏览器都有自己的内部持久性数据库,叫做IndexedDB。没有什么能阻止你编写自己的代码将一些数据同步到那里,然后编写一些自定义的服务工作者代码来拦截网络请求,确定用户是否离线,然后从 IndexedDB 提供相应的内容,如果它在那里的话。

但一个更简单的选择是检测用户是否离线,显示一条关于离线的信息,然后绕过数据请求。这本身就是一个话题,我已经写得很详细了。

在向你展示如何编写和整合你自己的服务工作者内容之前,让我们仔细看看我们现有的服务工作者。特别是,让我们看看它是如何管理更新/改变内容的。这是一个令人惊讶的棘手问题,即使使用VitePWA插件也很容易出错。

在继续前进之前,请确保告诉Chrome DevTools让你重新上线。

服务工作者如何更新

仔细看看,当我们改变内容时,我们的网站会发生什么。我们将继续删除我们现有的服务工,我们可以在DevTools的应用程序选项卡中的 "存储 "下进行。

Screenshot showing the Storage panel of DevTools. The DevTools menu is a panel on the left and the app usage is displayed in a panel on the right, showing that 508 kilobytes of data total is used, where 392 kilobytes are cached and 16.4 are service workers. A button to clear site data is below the Usage stats with a deep blue label and a light gray background.

点击 "清除网站数据 "按钮,以获得一个干净的板块。当我这样做的时候,我将删除我自己网站的大部分路由,这样就会有更少的资源,然后让Vite重建这个应用程序。

在生成的sw.js ,看一下生成的Workbox服务工作者。它里面应该有一个预缓存清单。我的看起来像这样。

A dark mode screenshot showing a list of eight asset urls inside of a precacheAndRoute function.

如果sw.js ,通过Prettier运行它,使其更容易阅读。

现在让我们运行该网站,看看我们的缓存里有什么。

让我们关注一下settings.js file 。Vite根据其内容的哈希值生成了assets/settings.ccb080c2.js 。Workbox是独立于Vite的,它对同一文件生成了自己哈希值。如果这个相同的文件名被生成了不同的内容,那么就会重新生成一个新的服务工作者,用不同的预缓存清单(相同的文件,但不同的修订版),Workbox就知道要缓存新的版本,并在不再需要时删除旧的。

同样,由于我们使用的是在文件名中注入哈希代码的捆绑器,所以文件名总是不同的,但Workbox支持不这样做的开发环境。

在写作时,VitePWA插件已经更新,不再注入这些修订哈希值。如果你试图按照本文的步骤进行操作,这个具体步骤可能与你的实际经验略有不同。请看这个GitHub问题以了解更多情况。

如果我们更新我们的settings.js 文件,那么 Vite 将在我们的构建中创建一个新的文件,有一个新的哈希代码,Workbox 将把它当作一个新文件。让我们看看这个动作。在修改文件并重新运行Vite构建后,我们的预缓存清单看起来像这样。

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

A DevTools screenshot showing a table of pre-cached assets processed by the VitePWA plugin and Workbox.

新的预缓存清单会显示在缓存资产列表中。请注意,我们的设置文件的两个版本都在那里(还有其他一些资产的两个版本也受到了影响):旧版本,因为它仍然在运行,新版本,因为新的服务工作者已经预先缓存了它。

请注意这里的推论:我们的旧内容仍然被提供给用户,因为旧的服务工作者仍在运行。用户无法看到我们刚刚所做的改变,即使他们刷新,因为服务工作者在默认情况下,保证这个网络应用的任何和所有标签都运行相同的版本。如果你想让浏览器显示更新的版本,请关闭你的标签(以及该网站的任何其他标签),然后重新打开它。

The same DevTools screenshot of pre-cached assets, but now only displaying new assets instead of duplicates.

缓存现在应该只包含新的资产。

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库来显示一个toast UI组件,让用户知道新版本的服务工作者何时可用并等待。如果用户点击烤面包,我就调用VitePWA给我的函数来重新加载页面,同时运行新的服务工作者。

A toast component screenshot with white text and a slight background gradient that goes from light blue on the left to bright blue on the right. It reads: an update is available! Click to update and reload.

现在,当我们有待处理的更新时,一个漂亮的烤面包组件会在前端弹出。点击它就会重新加载页面,并在其中加入新的内容。

这里需要记住的一点是,在你部署了显示吐司的代码后,在你下次加载你的网站时,吐司组件将不会显示出来。这是因为旧的服务工作者(在我们添加吐司组件之前的那个)还在运行。这需要手动关闭所有标签,并重新打开Web应用程序,让新的服务工作者接管。然后,在你下次更新一些代码时,服务工作者应该显示烤面包,提示你进行更新。

为什么刷新页面时服务工作者不更新?我在前面提到,刷新页面并不更新或激活等待的服务工作者,那么为什么这个方法能发挥作用呢?调用这个方法不仅刷新了页面,而且还调用了一些底层的服务工作者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模式相匹配的任何东西。如果你想深入了解具体情况,文档中提供了更多信息。

现在,在更新生效后,我们可以看到这些资源被我们的服务工作者所服务。

DevTools screenshot showing the resources that are loaded by the browser. There are four jpeg images.

我们还可以看到所创建的相应的缓存。

DevTools screenshot showing the new cache instance that is stored in Cache Storage. It includes all of the cached images.

添加您自己的服务工作者内容

比方说,你想让你的服务工作者变得更高级。你想添加一些代码来与 IndexedDB 同步数据,添加获取处理程序,并在用户离线时使用 IndexedDB 数据进行响应(再次,我之前的文章介绍了 IndexedDB 的内涵和外延)。但你如何把你自己的代码放到Vite为我们创建的服务工作者中呢?

为此,我们可以使用另一个工作箱选项: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的服务工作者能够大大改善你的网络应用的性能。虽然一开始看起来有点吓人或令人困惑,但很高兴知道我们有像VitePWA插件这样的工具来大大地简化事情。安装该插件,让它来完成繁重的工作。当然,服务工作者可以做一些更高级的事情,VitePWA可以用来实现更复杂的功能,但一个离线网站是一个很好的起点