我在 Reddit 和 Y Combinator 上经常遇到讨论,新开发者寻求技术栈建议。不可避免地,有人声称如果不使用像 React 或 AngularJS 这样的单页应用程序 (SPA) 框架,就不可能构建高质量应用程序。这让我感到奇怪,因为即使在 SPA 革命之前,许多流行多页 Web 应用程序也提供了出色的用户体验。
两年前,我着手构建一个可观测性平台,并选择使用 HTMX 实验多页应用程序 (MPA) 方法。我想知道:对于数据密集型应用程序,服务器渲染的 MPA 是否不足,因为大多数可观测性平台都是基于 ReactJS 构建的?
我发现,如果注意某些细节,您可以创建出色的服务器渲染应用程序。
以下是一些常见的 MPA 神话以及我对它们的了解。
MPA 页面过渡缓慢的看法很普遍——并非完全没有根据——因为这是浏览器的默认行为。然而,浏览器在过去十年中做出了重大改进来缓解这个问题。
为了说明,在下面的视频中,禁用缓存的全页面重新加载直到 DOMContentLoaded 事件触发需要 2.90 秒。我在 Wi-Fi 差的咖啡馆录制了这个,但让我们以此作为参考点。请记住这个数字。
在 MPA 中使用像 PJAX、Turbolinks 甚至 HTMX Boost 这样的库来减少加载时间是很常见的。这些库使用 JavaScript 劫持页面重新加载,并在过渡之间仅交换 HTML body 元素。这样,大多数页面 head 部分的资产就不需要重新加载或重新下载。
但是,有一种不太知名的减少资产重新下载或评估的方式。
使用 SPA 框架构建渐进式 Web 应用程序 (PWA) 的前端开发者可能知道服务工作者。
对于我们这些不是前端或 PWA 开发者的人来说,服务工作者是浏览器的内置功能。它们让您编写 JavaScript 代码,位于用户和网络之间,拦截请求并决定浏览器如何处理它们。

由于与 PWA 趋势的关联,服务工作者在 SPA 开发者中很常见,而开发者需要意识到这项技术也可以用于常规多页应用程序。
在视频演示中,我们启用服务工作者来缓存并刷新当前页面。您会注意到,点击链接重新加载页面时没有闪烁,从而带来更平滑的用户体验。
此外,与之前传输超过 2 MB 的静态资产相比,浏览器现在仅获取 84 KB 的 HTML 内容——实际的页面数据。此优化将 DOMContentLoaded 事件时间从 2.9 秒减少到不到 500 毫秒。令人印象深刻的是,这一改进无需使用 HTMX Boost、PJAX 或 Turbolinks 即可实现。
您可能想知道如何在自己的 MPA 中复制这些性能提升。以下是一个简单指南:
sw.js 文件:这是您的服务工作者脚本,将管理缓存和网络请求。通过实现服务工作者,您有效地告诉浏览器如何处理网络请求和缓存,从而导致更快的加载时间和更无缝的用户体验。
虽然可以手动编写服务工作者——并且有优秀的资源如这篇 MDN 文章来帮助您——但我更喜欢使用 Google 的 Workbox 库来自动化这个过程。
安装 Workbox:通过 npm 或您首选的包管理器安装 Workbox:
npm install workbox-cli --global
生成 Workbox 配置文件:运行以下命令创建配置文件:
workbox wizard
配置资产处理:在生成的 workbox-config.js 文件中,定义不同资产应如何缓存。使用 urlPattern 属性——一个正则表达式——来匹配特定的 HTTP 请求。对于每个匹配的请求,指定缓存策略,例如 CacheFirst 或 NetworkFirst。

构建服务工作者:运行 Workbox 构建命令,根据您的配置生成 sw.js 文件:
workbox generateSW workbox-config.js
在您的应用程序中注册服务工作者:在您的 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 浏览器控制台中注册的服务工作者的图像。
Speculation Rules API:预渲染页面以实现即时页面导航。如果您使用过 htmx-preload 或 instantpage.js,您就熟悉预渲染以及 “Speculation Rules API” 旨在解决的问题。Speculation Rules API 旨在改善未来导航的性能。它具有表达性语法,用于指定当前页面上应预取或预渲染哪些链接。

推测规则配置示例
上面的脚本是推测规则配置的示例。它是一个 JavaScript 对象,不详细说明,您可以看到它使用诸如“where”、“and”、“not”等关键字来描述应预取或预渲染哪些元素。
从上一节,您知道服务工作者可以缓存一切并使我们的应用程序完全离线运行。但如果我们想保存离线 POST 请求并在有互联网时重试它们呢?

上面的配置 JavaScript 文件显示了如何配置 Workbox 以支持两种常见的离线场景。在这里,您看到后台同步,我们要求服务工作者缓存由于互联网问题而失败的任何请求,并重试长达 24 小时。
下面,我们定义了一个离线捕获处理程序,当请求在离线时触发。我们可以返回带有 HTML 或 JSON 响应的模板部分,或基于请求输入动态构建响应。这里天空是极限。
在服务工作者视频中,我们已经看到,如果我们配置缓存和预渲染,这不会发生。然而,这个神话直到 2019 年才不是普遍真实的。自 2019 年以来,大多数浏览器会延迟绘制下一屏,直到下一页所需的所有资产可用或达到超时,从而在两个页面之间过渡时没有白色闪烁。这仅在同一来源/域内导航时有效。
单页应用程序框架的出现使页面之间的自定义过渡更受欢迎。不同导航样式的吸引力来自于完全从浏览器中接管页面导航控制。在实践中,此类过渡主要在 Web 开发会议演讲的演示中流行。
这仍是单页应用程序的常见论点,尤其是在 Reddit 和 Hacker News 评论区。然而,浏览器在过去几年中一直在努力本地解决这个问题。Chrome 126 推出了跨文档视图过渡。这意味着我们可以构建 MPA 以使用仅 CSS 或 CSS 和 JavaScript 在页面之间包含那些花哨的动画和过渡。
我最喜欢的部分是我们可能能够仅使用 CSS 创建可爱的跨文档过渡:

您可以快速了解更多信息,请参阅 Google Chrome 公告页面
此链接托管了一个多页应用程序演示,您可以在其中使用跨文档视图过渡 API 玩弄一个基本的服务器渲染应用程序,以模拟基于栈的动画。
当讨论 HTMX 时,我经常听到这个。所以,可能有一些由 HTMX 定位引起的混淆。但您不必将一切都做在服务器端。许多 HTMX 和常规 MPA 用户继续在适当的地方使用 JavaScript、Alpine 或 Hyperscript。
在需要健壮交互性的情况下,您可以采用组件岛屿架构,使用 WebComponents 或您选择的任何 JavaScript 框架(React、Angular 等)。这样,您的整个应用程序不是 SPA,而是专门利用这些框架来处理应用程序中需要交互性的部分。
上面的示例显示了 APItoolkit 中的一个非常交互式的搜索组件。它是一个使用 lit-element 实现的 Web 组件,lit-element 是一个零编译步骤的库,用于编写 Web 组件。因此,整个 Web 组件事件适合一个 JavaScript 文件。
直接 DOM 操作的速度是构建 ReactJS 并普及虚拟 DOM 技术的主要动力。虽然虚拟 DOM 操作可能比直接 DOM 操作更快,但这仅适用于执行许多复杂操作并在毫秒内刷新的应用程序,在这些情况下,这种性能可能很明显。但我们大多数人并没有构建这样的软件。
Svelte 团队写了一篇优秀的文章,标题为 “虚拟 DOM 是纯粹的开销。” 我推荐阅读它,因为它更好地解释了为什么虚拟 DOM 对于大多数应用程序并不重要。
随着浏览器技术的进步,您可以避免编写大量的客户端 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。下面是一个视频,显示该代码用于在日志浏览器中隐藏或显示日志项。
这可能正确,也可能不正确。
我喜欢争辩说,Web 上一些最多产的日子是 PHP 和 JQuery 意大利面条代码的日子。那时构建了很多软件,包括我们今天知道的许多流行互联网品牌。其中大多数都是作为所谓的意大利面条代码构建的,这帮助它们及早发布产品,并存活足够长的时间来重构而不再是意大利面条。
这次演讲的整个要点是向您展示 2024 年的浏览器有很多可能。虽然我们没有注意,浏览器已经缩小了差距,并从单页应用程序革命中借鉴了最好的想法。例如,WebComponents 的存在要归功于我们从单页应用程序中学到的教训。
所以现在,我们可以使用主要是浏览器工具——HTML、CSS,也许一些 JavaScript——来构建非常交互式的、甚至离线的 Web 应用程序,而不会在用户体验方面牺牲太多。