你不能构建交互式 Web 应用程序除非作为单页应用程序... 以及其他神话

Tony Alaribe

颂扬浏览器的进步。

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

两年前,我开始构建一个可观测性平台,并决定尝试使用 多页面应用(MPA)方法配合 HTMX。我想知道:对于一个数据密集型的应用程序,服务器端渲染的 MPA 是否足够好,考虑到大多数可观测性平台都是基于 ReactJS 构建的?

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

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

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

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

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

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

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

通过 Service Workers 进行客户端缓存

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

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

service-worker-chart.png

由于它与 PWA 趋势相关联,service workers 只在 SPA 开发者中比较普遍,开发者需要 意识到这项技术也可以用于常规的多页面应用程序。

在视频演示中,我们启用一个 service worker 来缓存并刷新当前页面。你会注意到点击链接重新加载页面时没有闪烁,从而带来更流畅的用户体验。

此外,与之前传输超过 2 MB 的静态资源不同,浏览器现在只获取 84 KB 的 HTML 内容——实际的页面数据。这种优化将 DOMContentLoaded 事件时间从 2.9 秒减少到 500 毫秒以下。令人印象深刻的是,这个改进是在没有使用 HTMX Boost、PJAX 或 Turbolinks 的情况下实现的。

如何在你的多页面应用中实现 Service workers

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

  1. 创建一个 sw.js 文件:这是你的 service worker 脚本,它将管理缓存和网络请求。
  2. 列出要缓存的文件:在 service worker 中,指定所有应该被缓存的资源(HTML、CSS、JavaScript、图片)。
  3. 定义缓存策略:指示每种资源应该如何被缓存——例如,是永久缓存还是定期刷新。

通过实现 service worker,你实际上是告诉浏览器如何处理网络请求和缓存,从而带来 更快的加载时间和更流畅的用户体验。

使用 Workbox 生成 service workers

虽然可以手动编写 service workers——并且有像 这篇 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. 构建 Service Worker:运行 Workbox 构建命令,根据你的配置生成 sw.js 文件:

    workbox generateSW workbox-config.js
    
  5. 在你的应用中注册 Service Worker:将以下脚本添加到你的 HTML 页面中以注册 service worker:

    <script>
      if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
          navigator.serviceWorker.register('/sw.js').then(function(registration) {
            console.log('ServiceWorker 注册成功,作用域: ', registration.scope);
          }, function(err) {
            console.log('ServiceWorker 注册失败: ', err);
          });
        });
      }
    </script>
    

按照这些步骤,你指示浏览器尽可能提供缓存资源,从而大幅减少加载 时间并提高多页面应用程序的整体性能。

Chrome 浏览器控制台显示的已注册 service worker 图片。

图片显示的是 Chrome 浏览器控制台中注册的 service worker。

推测规则 API (Speculation Rules API):预渲染页面以实现即时页面导航。

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

推测规则配置示例

推测规则配置示例

上面的脚本是推测规则如何配置的示例。它是一个 Javascript 对象,在不深入细节的情况下,你可以看到它使用了诸如“where”、“and”、“not”等关键字来描述哪些元素应该被预取或预渲染。

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

神话 2:MPA 无法离线运行并在有网络时保存更新进行重试

从上一节中,你知道 service workers 可以缓存所有内容,使我们的应用程序完全离线运行。 但如果我们想保存离线的 POST 请求并在有互联网时重试它们呢?

workbox-offline-cfg.png

上面的配置 javascript 文件展示了如何配置 Workbox 以支持两种常见的离线场景。在这里, 你看到后台同步(background Sync),我们要求 service worker 缓存任何因互联网问题而失败的请求,并在最多 24 小时内重试它。

在下面,我们定义了一个离线捕获处理程序,在离线发出请求时触发。我们可以返回一个包含 HTML 或 JSON 响应的模板部分,或者根据请求输入动态构建响应。可能性是无限的。

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

在 service worker 视频中,我们已经看到如果配置了缓存和预渲染就不会发生这种情况。 然而,这个神话在 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。例如,网页上的标准操作是根据按钮点击或切换来显示和隐藏内容。如今,你可以仅使用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将变成意大利面条式代码且无法维护

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

谁在乎?我就爱意大利面条

我认为网络开发最高效的时期正是PHP和JQuery的"面条代码"时代。那时构建了大量软件,包括当今许多知名互联网品牌。它们大多以所谓的面条代码构建,这帮助它们快速推出产品并存活到重构阶段。

结论

本文的核心是展示2024年浏览器的强大能力。在我们未曾察觉时,浏览器已弥合差距并吸收了单页应用革命的最佳创意。例如,WebComponents正是从单页应用经验中诞生的。

现在,我们可以使用主要浏览器工具——HTML、CSS及少量JavaScript——构建高度交互甚至离线的Web应用,同时几乎不牺牲用户体验。

浏览器已走过了漫漫长路,给它一个机会吧!

</>