HTMX与SPA的默认方案:当超媒体技术胜过JavaScript框架时
HTMX属性如何替代客户端渲染,代码包大小究竟意味着什么,以及一份关于超媒体应用在哪些方面更具优势、React又在哪些方面仍占上风的平衡指南。
许多团队在每个新项目中都会选择 React,包括那些主要由表单和表格构成的管理面板及 CRUD 工具。htmx 认为,这类应用中有很大一部分其实根本不需要客户端框架:服务器可以直接发送 HTML,再通过少量属性即可让任意元素获取并替换其部分内容。本指南将解释 htmx 的工作原理,对比其与 React 的资源占用情况,并阐述其真正的优势以及支持者往往忽略的局限性,帮助你能够有意识地选择架构而非凭习惯行事。
本文假设你已经开发过网页应用,并且大部分时间都在使用 React 或类似的框架。
为何单页应用会成为所有项目的默认选择
大约在2013年左右,该行业改变了其核心架构。服务器不再返回HTML页面,而是开始返回JSON数据,浏览器中的JavaScript则负责将这些JSON转换为界面。
对于某些产品而言,单页应用确实代表了进步。Gmail、Figma和Google Maps会在浏览器中存储大量状态,因此它们的交互体验必须做到即时响应。
问题在于这种架构被应用到了所有领域。原本只是用于列出数据行并允许编辑的内部控制面板,最终也采用了与设计工具相同的架构;而一个仅有联系表单的营销网站,则不得不引入代码打包工具、路由器、状态管理库以及数据加载机制。
这种默认架构会带来实际成本:
- 需要维护两个代码库,且往往使用不同的语言
- 数据模型需要在两端都进行定义
- 还需要配套的构建工具链来进行维护和升级
htmx的核心观点是,许多应用程序都在承担这些成本,却得不到任何回报。
htmx是什么
htmx是一个小型JavaScript库,它通过为HTML添加属性来扩展其功能。借助这些属性,任何元素都可以发起HTTP请求,并将响应内容放置在页面的某个位置。这基本上就是该库的全部功能。
实时搜索框便体现了这一理念。输入部分会指定要调用的URL、触发调用的事件,以及结果应显示的位置:
<input type="text"
name="q"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results">
<div id="results"></div>
将属性值视为一条语句来处理:当按键被释放且数值确实发生变化时,等待300毫秒,向/search发送GET请求,然后将返回的内容放入#results中。delay:300ms参数可用于抑制频繁输入,避免每次按键都发送请求。
服务器不会返回JSON格式的数据,而是直接返回一个现成的HTML片段:
<ul>
<li>First result</li>
<li>Second result</li>
</ul>
htmx会将该HTML片段插入到目标元素中。无需进行JSON解析,也没有客户端模板,更不存在可能与服务器数据不一致的客户端状态。
最常使用的属性
hx-get、hx-post、hx-put、hx-delete用于选择HTTP方法及URL地址。
hx-trigger用于设置触发请求的事件,例如click、keyup、load、every 2s或revealed(当元素滚动进入可视区域时)。hx-target用于指定接收响应的元素。hx-swap控制如何插入响应内容:innerHTML、outerHTML、beforeend或delete。hx-indicator用于指定在请求处理期间显示的元素。hx-confirm会在发送请求前要求用户确认。这些元素组合得很好。接下来的代码片段用于删除表格中的一行:按钮会发送一个DELETE请求,定位到最近的tr元素,用(通常为空的)响应内容替换整行,并在操作前先请求确认。swap:1s修饰符会将替换操作延迟一秒,这样就有足够的CSS过渡时间让该行逐渐消失,从而使操作显得更加流畅。
<tr>
<td>Widget</td>
<td>
<button hx-delete="/items/42"
hx-target="closest tr"
hx-swap="outerHTML swap:1s"
hx-confirm="Delete this item?">
Delete
</button>
</td>
</tr>
注意这里缺少了什么:没有独立的JavaScript文件,没有构建步骤,也没有客户端状态。删除项目以及决定该行应变为何种状态的职责仍由服务器承担。
两种架构并列展示
通过流程对比来理解两者的差异会比通过工具对比更为清晰。
- SPA模型:服务器发送JSON,客户端代码对其进行渲染并管理状态;用户操作后,客户端更新状态、重新渲染,然后将JSON发回服务器。
- 超媒体模型:服务器发送HTML,浏览器将其显示;用户操作后,服务器返回新的HTML,浏览器再替换显示内容。
在超媒体模型中,唯一的真实数据源是服务器。这避免了诸多因客户端持有的信息与数据库不一致而产生的错误,比如过期的缓存或从未同步的乐观更新问题。
创建了htmx的Carson Gross将其视为对REST本源理念的回归。Roy Fielding提出的REST标准包含了HATEOAS原则(将超媒体作为应用状态的控制机制):服务器会返回包含后续可用操作的表示数据。而典型的JSON API并不具备这一功能,客户端必须事先知晓存在的端点。HTML则天生具备此能力,因为链接代表状态转换,表单则代表操作。你是否认为这种观点有深度或仅属学术探讨,往往能预示你整体上对htmx的接受程度。
测量代码包大小及其无法提供的信息
不同来源给出的大小数值存在差异,因此进行测量很有帮助。在撰写本文时,压缩后的htmx代码包大小如下:
htmx.min.js: 51,238 bytes raw
16,576 bytes gzipped (16.2 KB)
React 19.2.8的最终版本,即包含React包及DOM客户端的完整版本,其大小则为:
react.production.js: 4,446 bytes gzipped
react-dom-client.production.js: 94,757 bytes gzipped
combined: 98,420 bytes gzipped (96 KB)
这大约是六倍的差异,而且仅针对 React 运行时而言。一个真正的 React 应用还会包含路由器、状态管理库、数据获取层以及自定义组件,因此最终打包后的文件大小通常可达数百千字节。相比之下,htmx 的数值指的是完整的客户端依赖项总和。具体数字会随着每次版本更新而变化,因此需根据实际部署的版本重新测量。
不过在得出结论时需谨慎。文件大小影响的是首次加载速度,而非之后的交互响应速度。在网络状况良好的情况下,加载时间差距很小,且再次访问时这两个文件都会从缓存中读取。更有说服力的性能优势在于,htmx 没有水合阶段——在那个阶段,服务器渲染的 React 页面看似已准备好,但在事件处理程序被绑定之前会忽略所有点击操作。
htmx 真正能发挥作用的地方
- 一种语言,一个代码库。使用 Go、Python、Ruby 或 C# 开发的团队可以构建整个应用程序。验证逻辑集中在一处,数据模型也只需定义一次。
- 无需构建流水线。一个
script标签就足够了。没有打包器配置,没有前端node_modules,也无需处理前端依赖的升级问题。 - 行为具有局部性。元素的功能直接写在元素本身上。无需通过多个文件和存储层来追踪点击操作,只需读取标记即可。这是该技术的最大优势之一,且关乎可维护性而非速度。
- 状态集中管理。无需清除客户端缓存,没有过时数据,也不需要回滚乐观更新操作。
htmx的不足之处
这些往往是人们容易忽略的问题。
丰富的连续交互体验
拖放编辑、画布编辑器、电子表格以及支持刷选和缩放功能的交互式图表都需要持续性的交互,而非独立的请求。在 htmx 中,每一次交互都意味着与服务器之间的往返通信。对于文本编辑器这类应用而言,这并非可接受的折衷方案,因此 htmx 便不再适用。
网络状况不佳时的延迟问题
支持者指出,边缘计算托管确实大幅降低了往返时间,这一点没错。不过,使用弱质农村移动网络的用户可能要等待几百毫秒才能完成 React 能在几毫秒内本地处理的任务。htmx 应用在网络良好的情况下体验极佳,而在网络较差时则明显变差,这与通常的论述方向正好相反。
没有移动端或离线应用的优势
htmx仅适用于网页环境。而React Native能让团队与原生应用共享设计理念及大量代码,因此如果项目计划涉及原生移动端开发,这一优势便能抵消其他所有因素。此外由于所有操作都需要服务器支持,因此也不支持离线使用。
复杂的客户端状态管理
包含相互依赖字段的多步骤向导、具有实时跨字段验证功能的表单,或是需要多个组件对单一变化作出响应的界面,都会带来使用上的不便。团队通常会选择添加Alpine.js或hypescript,到那时他们其实是在自行组合框架组件,而非使用现成的完整框架。
较小的生态系统与人才储备
React 几乎为所有组件都提供了成熟的解决方案。而使用 htmx 时,要么自行开发日期选择器,要么嵌入独立的 JavaScript 库并自行处理相关边界问题。使用经验也很重要:据相关调查,在撰写本文时,使用 React 的开发者占比超过 40%,而 htmx 的比例约为 7%,npm 下载量之比为约 560 比 1。这会影响新员工的上手速度,以及遇到问题时寻找解决方案的难易程度。
仔细解读采用率数据
实际情况比任何一方所说的都要复杂。
- React 的使用率并未下降。从绝对数值来看,它依然占据着极大的优势。
可以得出的结论是:React并未被取代,但它作为无可争议的默认选择的地位正在减弱。需注意,网上很多关于htmx与React的讨论都是为吸引搜索流量而写的,诸如代码缩减比例或各种速度指标之类的数据往往被未经注明出处地反复引用。应将这些数据视为参考性信息,在引用之前务必核实最新来源。
如何在这两者之间选择
选择 htmx 的情况:
- 应用主要以表单和列表形式存在:管理面板、内部工具、CRUD 应用、内容网站,以及仅用于显示数据而非对其进行操作的仪表板。
- 您的团队擅长后端开发。
- 您希望拥有一个任何团队成员在未来几年都能维护的单一代码库。
选择 React 的情况:
- 浏览器需要真正承载大量状态,比如编辑器、设计工具、实时协作功能或复杂的数据处理场景。
- 您需要原生移动应用或离线支持功能。
- 您依赖成熟的组件生态系统。
还有一种常被忽视的第三种选择:两者皆用。htmx可以处理CRUD界面,而React则负责那些真正需要它的两三个视图。没有规定整个产品必须采用某种架构,将htmx与少量React代码结合使用比混合两个SPA框架更为简单。如果您已经在使用htmx,htmx 4升级指南介绍了下一个主要版本中的变化。
React Server Components通过将渲染任务移至服务器,借鉴了超媒体理念的部分思想。不过它们仍然需要完整的React运行时以及支持Node的服务器,因此并非轻量级方案;它只是让React在保持原有体量的同时获得部分相同优势。
关键要点
- htmx允许任何元素发送请求并替换为服务器渲染的HTML,这样就能让服务器成为唯一的数据来源,同时避免客户端状态的同步问题。
- 在添加任何React应用代码之前,它的运行时间大约仅为React的六分之一,但实际好处更多体现在避免数据同步过程而非缩短加载时间。
- 它非常适合用于CRUD应用、内部工具和内容网站,但在需要持续交互、网络条件不佳、移动端使用、离线场景以及处理复杂客户端状态时表现较差。
- 将两者结合使用是一种合理的架构设计,而非折中方案。
这场争论背后更重要的问题是:为何为 Gmail 设计的架构会成为六字段表单的默认选择。其实并没有谁完全错;只是某个工具被设为默认,而一旦成为默认,人们就不再去审视它了。无论你选择什么,包括 React,在做决定之前都应该经过深思熟虑,而非盲目沿用他人选择的结果。
相关阅读
- React的调度器与事件循环:究竟谁来决定任务何时执行 — 了解 React的协作式调度器如何在JavaScript事件循环中运行,为何某些状态转换会导致线程让出控制权,以及为何没有任何调度器能够拯救被阻塞的线程。