除了单页应用程序,您无法构建交互式 Web 应用程序……以及其他神话

Tony Alaribe

浏览器进步的颂歌。

我在 Reddit 和 Y Combinator 上经常遇到讨论,新开发者寻求技术栈建议。不可避免地,有人声称如果不使用像 React 或 AngularJS 这样的单页应用程序 (SPA) 框架,就不可能构建高质量应用程序。这让我感到奇怪,因为即使在 SPA 革命之前,许多流行多页 Web 应用程序也提供了出色的用户体验。

两年前,我着手构建一个可观测性平台,并选择使用 HTMX 实验多页应用程序 (MPA) 方法。我想知道:对于数据密集型应用程序,服务器渲染的 MPA 是否不足,因为大多数可观测性平台都是基于 ReactJS 构建的?

我发现,如果注意某些细节,您可以创建出色的服务器渲染应用程序。

以下是一些常见的 MPA 神话以及我对它们的了解。

神话 1:MPA 页面过渡缓慢,因为每次页面导航都会下载 JavaScript 和 CSS

MPA 页面过渡缓慢的看法很普遍——并非完全没有根据——因为这是浏览器的默认行为。然而,浏览器在过去十年中做出了重大改进来缓解这个问题。

为了说明,在下面的视频中,禁用缓存的全页面重新加载直到 DOMContentLoaded 事件触发需要 2.90 秒。我在 Wi-Fi 差的咖啡馆录制了这个,但让我们以此作为参考点。请记住这个数字。

在 MPA 中使用像 PJAX、Turbolinks 甚至 HTMX Boost 这样的库来减少加载时间是很常见的。这些库使用 JavaScript 劫持页面重新加载,并在过渡之间仅交换 HTML body 元素。这样,大多数页面 head 部分的资产就不需要重新加载或重新下载。

但是,有一种不太知名的减少资产重新下载或评估的方式。

通过服务工作者实现客户端缓存

使用 SPA 框架构建渐进式 Web 应用程序 (PWA) 的前端开发者可能知道服务工作者。

对于我们这些不是前端或 PWA 开发者的人来说,服务工作者是浏览器的内置功能。它们让您编写 JavaScript 代码,位于用户和网络之间,拦截请求并决定浏览器如何处理它们。

service-worker-chart.png

由于与 PWA 趋势的关联,服务工作者在 SPA 开发者中很常见,而开发者需要意识到这项技术也可以用于常规多页应用程序。

在视频演示中,我们启用服务工作者来缓存并刷新当前页面。您会注意到,点击链接重新加载页面时没有闪烁,从而带来更平滑的用户体验。

此外,与之前传输超过 2 MB 的静态资产相比,浏览器现在仅获取 84 KB 的 HTML 内容——实际的页面数据。此优化将 DOMContentLoaded 事件时间从 2.9 秒减少到不到 500 毫秒。令人印象深刻的是,这一改进无需使用 HTMX Boost、PJAX 或 Turbolinks 即可实现。

在您的多页应用程序中实现服务工作者

您可能想知道如何在自己的 MPA 中复制这些性能提升。以下是一个简单指南:

  1. 创建 sw.js 文件:这是您的服务工作者脚本,将管理缓存和网络请求。
  2. 列出要缓存的文件:在服务工作者中,指定所有应缓存的资产(HTML、CSS、JavaScript、图像)。
  3. 定义缓存策略:指示每种类型的资产应如何缓存——例如,是否应永久缓存或定期刷新。

通过实现服务工作者,您有效地告诉浏览器如何处理网络请求和缓存,从而导致更快的加载时间和更无缝的用户体验。

使用 Workbox 生成服务工作者

虽然可以手动编写服务工作者——并且有优秀的资源如这篇 MDN 文章来帮助您——但我更喜欢使用 Google 的 Workbox 库来自动化这个过程。

使用 Workbox 的步骤:

  1. 安装 Workbox:通过 npm 或您首选的包管理器安装 Workbox:

    npm install workbox-cli --global
    
  2. 生成 Workbox 配置文件:运行以下命令创建配置文件:

    workbox wizard
    
  3. 配置资产处理:在生成的 workbox-config.js 文件中,定义不同资产应如何缓存。使用 urlPattern 属性——一个正则表达式——来匹配特定的 HTTP 请求。对于每个匹配的请求,指定缓存策略,例如 CacheFirstNetworkFirst

    workbox-cfg.png

  4. 构建服务工作者:运行 Workbox 构建命令,根据您的配置生成 sw.js 文件:

    workbox generateSW workbox-config.js
    
  5. 在您的应用程序中注册服务工作者:在您的 HTML 页面中添加以下脚本来注册服务工作者:

    <script>
      if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
          navigator.serviceWorker.register('/sw.js').then(function(registration) {
            console.log('ServiceWorker registration successful with scope: ', registration.scope);
          }, function(err) {
            console.log('ServiceWorker registration failed: ', err);
          });
        });
      }
    </script>
    

通过遵循这些步骤,您指示浏览器尽可能提供缓存的资产,从而大幅减少加载时间并改善多页应用程序的整体性能。

显示 Chrome 浏览器控制台中注册的服务工作者的图像。

显示 Chrome 浏览器控制台中注册的服务工作者的图像。

Speculation Rules API:预渲染页面以实现即时页面导航。

如果您使用过 htmx-preloadinstantpage.js,您就熟悉预渲染以及 “Speculation Rules API” 旨在解决的问题。Speculation Rules API 旨在改善未来导航的性能。它具有表达性语法,用于指定当前页面上应预取或预渲染哪些链接。

推测规则配置示例

推测规则配置示例

上面的脚本是推测规则配置的示例。它是一个 JavaScript 对象,不详细说明,您可以看到它使用诸如“where”、“and”、“not”等关键字来描述应预取或预渲染哪些元素。

预渲染的影响示例 (Chrome 团队)

神话 2:MPA 无法离线操作并保存更新以在有网络时重试

从上一节,您知道服务工作者可以缓存一切并使我们的应用程序完全离线运行。但如果我们想保存离线 POST 请求并在有互联网时重试它们呢?

workbox-offline-cfg.png

上面的配置 JavaScript 文件显示了如何配置 Workbox 以支持两种常见的离线场景。在这里,您看到后台同步,我们要求服务工作者缓存由于互联网问题而失败的任何请求,并重试长达 24 小时。

下面,我们定义了一个离线捕获处理程序,当请求在离线时触发。我们可以返回带有 HTML 或 JSON 响应的模板部分,或基于请求输入动态构建响应。这里天空是极限。

神话 3:MPA 在页面过渡期间总是闪烁白色

在服务工作者视频中,我们已经看到,如果我们配置缓存和预渲染,这不会发生。然而,这个神话直到 2019 年才不是普遍真实的。自 2019 年以来,大多数浏览器会延迟绘制下一屏,直到下一页所需的所有资产可用或达到超时,从而在两个页面之间过渡时没有白色闪烁。这仅在同一来源/域内导航时有效。

Chrome.com 上的绘制保持文档

神话 4:使用 MPA 无法实现花哨的跨文档页面过渡。

单页应用程序框架的出现使页面之间的自定义过渡更受欢迎。不同导航样式的吸引力来自于完全从浏览器中接管页面导航控制。在实践中,此类过渡主要在 Web 开发会议演讲的演示中流行。

Chrome.com 上的跨文档过渡文档

这仍是单页应用程序的常见论点,尤其是在 Reddit 和 Hacker News 评论区。然而,浏览器在过去几年中一直在努力本地解决这个问题。Chrome 126 推出了跨文档视图过渡。这意味着我们可以构建 MPA 以使用仅 CSS 或 CSS 和 JavaScript 在页面之间包含那些花哨的动画和过渡。

我最喜欢的部分是我们可能能够仅使用 CSS 创建可爱的跨文档过渡:

cross-doc-transitions-css.png

您可以快速了解更多信息,请参阅 Google Chrome 公告页面

此链接托管了一个多页应用程序演示,您可以在其中使用跨文档视图过渡 API 玩弄一个基本的服务器渲染应用程序,以模拟基于栈的动画。

神话 5:使用 htmx 或 MPA 时,每个用户操作都必须在服务器上发生。

当讨论 HTMX 时,我经常听到这个。所以,可能有一些由 HTMX 定位引起的混淆。但您不必将一切都做在服务器端。许多 HTMX 和常规 MPA 用户继续在适当的地方使用 JavaScript、Alpine 或 Hyperscript。

在需要健壮交互性的情况下,您可以采用组件岛屿架构,使用 WebComponents 或您选择的任何 JavaScript 框架(React、Angular 等)。这样,您的整个应用程序不是 SPA,而是专门利用这些框架来处理应用程序中需要交互性的部分。

上面的示例显示了 APItoolkit 中的一个非常交互式的搜索组件。它是一个使用 lit-element 实现的 Web 组件,lit-element 是一个零编译步骤的库,用于编写 Web 组件。因此,整个 Web 组件事件适合一个 JavaScript 文件。

神话 6:直接操作 DOM 很慢。因此,最好使用 React/虚拟 DOM。

直接 DOM 操作的速度是构建 ReactJS 并普及虚拟 DOM 技术的主要动力。虽然虚拟 DOM 操作可能比直接 DOM 操作更快,但这仅适用于执行许多复杂操作并在毫秒内刷新的应用程序,在这些情况下,这种性能可能很明显。但我们大多数人并没有构建这样的软件。

Svelte 团队写了一篇优秀的文章,标题为 “虚拟 DOM 是纯粹的开销。” 我推荐阅读它,因为它更好地解释了为什么虚拟 DOM 对于大多数应用程序并不重要。

神话 7:对于每个小交互,您仍然需要编写 JavaScript。

随着浏览器技术的进步,您可以避免编写大量的客户端 JavaScript。例如,Web 上的一个标准操作是基于按钮点击或切换显示和隐藏内容。这些天,您可以使用仅 CSS 和 HTML 来显示和隐藏元素,例如,使用 HTML 输入复选框来跟踪状态。我们可以将 HTML 标签样式化为按钮,并为其赋予 for="checkboxID" 属性,这样点击标签就会切换复选框。

<input id="published" class="hidden peer" type="checkbox"/>
<label for="published" class="btn">toggle content</label>

<div class="hidden peer-checked:block">
    Content to be toggled when label/btn is clicked
</div>

我们可以将这样的复选框与 HTMX intersect 结合,当按钮被点击时从端点获取内容。

<input id="published" class="peer" type="checkbox" name="status"/>
<div
        class="hidden peer-checked:block"
        hx-trigger="intersect once"
        hx-get="/log-item"
>Shell/Loading text etc
</div>

上面的所有类都是原生 Tailwind CSS 类,但您也可以手动编写 CSS。下面是一个视频,显示该代码用于在日志浏览器中隐藏或显示日志项。

最终神话:没有 “正确” 前端框架,您的客户端 JavaScript 将是意大利面条代码且不可维护

这可能正确,也可能不正确。

谁在乎?我喜欢意大利面条。

我喜欢争辩说,Web 上一些最多产的日子是 PHP 和 JQuery 意大利面条代码的日子。那时构建了很多软件,包括我们今天知道的许多流行互联网品牌。其中大多数都是作为所谓的意大利面条代码构建的,这帮助它们及早发布产品,并存活足够长的时间来重构而不再是意大利面条。

结论

这次演讲的整个要点是向您展示 2024 年的浏览器有很多可能。虽然我们没有注意,浏览器已经缩小了差距,并从单页应用程序革命中借鉴了最好的想法。例如,WebComponents 的存在要归功于我们从单页应用程序中学到的教训。

所以现在,我们可以使用主要是浏览器工具——HTML、CSS,也许一些 JavaScript——来构建非常交互式的、甚至离线的 Web 应用程序,而不会在用户体验方面牺牲太多。

浏览器已经走过了很长的路。给它一个机会!

</>