你无法构建交互式 Web 应用程序,除非是单页面应用程序……以及其他神话

托尼·阿拉里贝

#献给浏览器进步的一首赞歌。

我经常在 Reddit 和 YCombinator 上看到关于新开发人员寻求技术栈建议的讨论。不可避免地,有人会声称如果不使用单页面应用程序 (SPA) 框架,例如 React 或 AngularJS,就无法构建高质量的应用程序。这让我觉得很奇怪,因为即使在 SPA 革命之前,许多流行的多页面 Web 应用程序也提供了极佳的用户体验。

两年前,我开始着手构建一个 可观察性平台,并选择使用 HTMX 尝试多页面应用程序 (MPA) 方法。我很好奇:考虑到大多数可观察性平台都是基于 ReactJS 构建的,那么对于一个数据密集型应用程序来说,服务器端渲染的 MPA 是否足够?

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

以下是一些常见的 MPA 神话,以及我从中学到的教训。

#神话 1:MPA 页面过渡很慢,因为 JavaScript 和 CSS 会在每次页面导航时下载

MPA 页面过渡很慢的看法很普遍——而且并非毫无根据——因为这是浏览器的默认行为。但是,浏览器在过去十年中做出了重大改进,以缓解此问题。

为了说明这一点,在下面的视频中,在禁用缓存的情况下,完全页面重新加载需要 2.90 秒才能触发 DOMContentLoaded 事件。我在一家 Wi-Fi 信号很差的咖啡馆录制了这段视频,但我们可以以此作为参考点。记住这个数字。

通常使用诸如 PJAX、Turbolinks,甚至 HTMX Boost 之类的库来减少 MPA 中的加载时间。这些库使用 Javascript 劫持页面重新加载,并在过渡之间只替换 HTML 主体元素。这样一来,大多数页面头部部分的资产就不需要重新加载或重新下载。

但是,还有一种鲜为人知的方法可以减少页面过渡期间重新下载或评估的资产数量。

#通过服务工作者进行客户端缓存

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

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

service-worker-chart.png

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

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

此外,浏览器现在只获取 84 KB 的 HTML 内容(实际页面数据),而不是像以前那样传输超过 2 MB 的静态资产。这种优化将 DOMContentLoaded 事件时间从 2.9 秒缩短到不到 500 毫秒。令人印象深刻的是,这种改进是在不使用 HTMX Boost、PJAX 或 Turbolinks 的情况下实现的。

#如何在你的多页面应用程序中实现服务工作者

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

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

通过实现服务工作者,你实际上是在告诉浏览器如何处理网络请求和缓存,从而缩短加载时间并改善整体用户体验。

#使用 Workbox 生成服务工作者

虽然可以手动编写服务工作者——并且有一些优秀的资源,例如 这篇文章 可以帮助你——但我更喜欢使用 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>
    

通过遵循这些步骤,你指示浏览器尽可能地提供缓存的资产,从而大幅缩短加载时间并提高多页面应用程序的整体性能。

Image showing the registered service worker from the chrome browser console.

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

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

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

Speculation rules configuration example

Speculation 规则配置示例

上面的脚本是 Speculation 规则配置方式的示例。它是一个 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/Virtual DOM。

直接 DOM 操作的速度是 ReactJS 建立和推广虚拟 DOM 技术的主要动力。虽然虚拟 DOM 操作可能比直接 DOM 操作更快,但这只对执行许多复杂操作并在毫秒内刷新的应用程序适用,在这种情况下,这种性能差异可能很明显。但我们大多数人并没有开发此类软件。

Svelte 团队写了一篇很棒的文章,名为“虚拟 DOM 纯粹是开销。” 我建议阅读它,因为它更好地解释了为什么虚拟 DOM 对大多数应用程序并不重要。

#迷思 7:您仍然需要为每个微不足道的交互编写 JavaScript。

随着浏览器技术的进步,您可以完全避免编写大量客户端 Javascript。例如,网络上一个标准操作是根据按钮点击或切换来显示和隐藏内容。如今,您可以仅使用 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 交叉结合起来,在点击按钮时从端点获取内容。

<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 将会变成意大利面条代码,难以维护

这可能是真的,也可能不是真的。

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

我想说,网络上最富有成效的日子中,有一些是 PHP 和 JQuery 意大利面条代码的时代。当时开发了许多软件,包括我们今天所知的许多流行的互联网品牌。其中大部分都是作为所谓的意大利面条代码构建的,这帮助它们尽早发布产品,并生存下来足够长的时间进行重构,不再是意大利面条代码。

#结论

这次演讲的全部重点是向您展示,浏览器在 2024 年能够实现很多功能。在我们没有注意到的时候,浏览器已经缩小了差距,借鉴了单页应用程序革命中的最佳理念。例如,WebComponents 存在得益于我们从单页应用程序中学到的经验教训。

所以现在,我们可以使用大多数浏览器工具(HTML、CSS,可能还有一些 Javascript)构建非常交互式的,甚至是离线的 Web 应用程序,而不会在用户体验方面牺牲太多。

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

</>