- 前端
【免费下载链接】htmx
htmx - high power tools for HTML
超媒体(Hypermedia)并非适合所有 Web 应用的银弹,但它对大量"以文本与图像为主、交互以 CRUD 为主、更新发生在明确定义区域内"的应用有着压倒性的简化优势。本文以 htmx 官方博客文章《When Should You Use Hypermedia?》为核心骨架,结合当前仓库中的示例与属性文档,系统梳理超媒体架构的适用场景、不适用场景与"过渡式(Transitional)"混合开发策略,帮助你在做功能级技术选型时获得可落地的判断依据。
引言:超媒体能解决什么,代价又是什么
在深入讨论"何时使用超媒体"之前,有必要先明确 htmx 团队(Carson Gross)看待超媒体立场的基础——本文开头引用了 Roy Fielding 博士论文《Architectural Styles and the Design of Network-based Software Architectures》中关于 REST 统一接口的一段话:
The trade-off, though, is that a uniform interface degrades efficiency, since information is transferred in a standardized form rather than one which is specific to an application's needs. The REST interface is designed to be efficient for large-grain hypermedia data transfer, optimizing for the common case of the Web, but resulting in an interface that is not optimal for other forms of architectural interaction.
翻译过来就是:REST 的"统一接口"以牺牲效率换取通用性,它针对"大粒度超媒体数据传输"这一 Web 的常见场景做了优化,因此对其它形态的架构交互并非最优。这段话为全文定下了基调——超媒体有明确的优势边界,选型应当基于权衡(trade-off),而不是信仰。
在此基础上,htmx 团队认为超媒体(配合 htmx 带来的额外 UX 能力)能够解决当前 Web 开发世界面临的许多问题,具体体现为三大优势:
- 复杂度显著更低:对于许多问题,超媒体方案比 SPA 方案简单得多。参见仓库中的真实案例 《A Real World React → htmx Port》,其中 Contexte 团队用 htmx 替换 React 后,整体代码量减少了 67%。
- API 可被更激进地重构与优化:因为超媒体应用通过 HTML 与服务器交互,端点可以被频繁调整而不破坏客户端。这一论点在 《HATEOAS》 一文中有详细的理论展开。
- 降低对特定服务器技术栈的绑定压力:由于没有庞大的 JavaScript 前端代码库,你不需要为了前端而被迫选择 Node.js 之类的后端技术,可以自由使用 Python、Go、Java 等任何你熟悉的后端语言。
正因如此,作者相信:借助 htmx 提供的额外 UX 可能性,许多现代 Web 应用都可以用 HTML 与超媒体范式构建。但正如所有技术选择一样,超媒体同样存在权衡,本文接下来的全部内容就是帮助你在具体项目或功能上判断"超媒体是否合适"。
过渡式应用(Transitional Applications)与超媒体
在讨论"什么时候超媒体是好选择"之前,必须先澄清一个前提:构建 Web 应用时,采纳超媒体并不是一个二选一(either/or)的决定。即便是最"单页"的单页应用(SPA),也必然使用超媒体——至少作为引导(bootstrap)机制来启动应用本身。
Rich Harris 在其演讲Have SPAs Ruined The Web中提出了"过渡式应用(Transitional Applications)"这一术语,指的是同时混合超媒体与非超媒体(SPA)概念的应用。htmx 团队在 《A Response To "Have Single-Page Apps Ruined the Web?"》 中对这场演讲做了详细回应,核心立场是:他们强烈认同"过渡式"这一务实的开发理念——应该为手头的具体工作选择正确的工具。
双方真正的分歧点在于"那条线"划在哪里——即哪些功能适合用超媒体高效实现,哪些功能需要更复杂的客户端方案。htmx 团队认为:有了 htmx,超媒体能覆盖的范围远比当今多数 Web 开发者认为的更大、更远;对许多应用而言,超媒体可以满足其全部或绝大部分 UX 需求。
这一"功能级(feature-by-feature)判断"的思想贯穿全文,也是结论部分的落点:不要笼统地问"htmx 适不适合我的应用",而要问"我的这个功能适不适合用超媒体实现"。
超媒体适合的场景
你的 UI 以文本与图像为主
在 《A Real World React → htmx Port》(被作者称为 "The Mother Of All htmx Demos")中,Contexte 公司的 David Guillot 展示了用 htmx 替换 React 之后,总代码量减少 67%,并伴随大量令人惊艳的指标:JS 依赖减少 96%(255 → 9)、Web 构建时间减少 88%(40 秒 → 5 秒)、首次可交互时间(TTI)缩短 50%~60%、内存占用降低 46%。
作者诚实地指出:并非每个团队从 React 迁移到 htmx 都能获得这些结果,Contexte 之所以如此契合超媒体,是因为它是一个媒体导向的 Web 应用——展示由文本和图像构成的文章供用户阅读。它虽然拥有复杂的过滤机制等交互细节,但应用的核心是"展示与分类文章",这正是超媒体被设计出来要做的事情。
因此,判断的第一条经验法则就是:如果你的应用本质是"读内容"(text & image heavy),超媒体几乎总是最优解。
你的 UI 是 CRUD 型的
超媒体在CRUD(Create, Read, Update, Delete)型 Web 应用上拥有长期的成功记录,典型代表是 Ruby on Rails 风格的应用。如果你的主要应用机制是展示表单、把表单保存进数据库,超媒体可以工作得非常好。
更重要的是,配合 htmx,CRUD 体验可以非常顺滑,不再局限于许多服务端应用采用的"列表页/详情页"简单范式:
- Click to Edit 示例:点击"编辑"按钮,通过
hx-get="/contact/1/edit"无刷新地把详情视图替换为编辑表单,提交时以hx-put="/contact/1"遵循 REST-ful 模式完成更新:
<div hx-target="this" hx-swap="outerHTML"> <div><label>First Name</label>: Joe</div> <div><label>Last Name</label>: Blow</div> <div><label>Email</label>: joe@blow.com</div> <button hx-get="/contact/1/edit" class="btn primary"> Click To Edit </button> </div><form hx-put="/contact/1" hx-target="this" hx-swap="outerHTML"> <div> <label>First Name</label> <input type="text" name="firstName" value="Joe"> </div> <div class="form-group"> <label>Last Name</label> <input type="text" name="lastName" value="Blow"> </div> <div class="form-group"> <label>Email Address</label> <input type="email" name="email" value="joe@blow.com"> </div> <button class="btn" type="submit">Submit</button> <button class="btn" hx-get="/contact/1">Cancel</button> </form>- Edit Row 示例:表格中的每一行都可以就地编辑。其核心技巧是
hx-target="closest tr" hx-swap="outerHTML"——把请求的目标定位到触发元素最近的表格行,整行替换:
<tbody hx-target="closest tr" hx-swap="outerHTML"> ... </tbody>编辑态的行通过hx-include="closest tr"把整行输入框的值纳入请求参数(HTML 规范不允许在<tr>内直接放置<form>,这是表格场景的实用解法),并通过hx-put提交保存:
<tr hx-trigger='cancel' class='editing' hx-get="/contact/${contact.id}"> <td><input autofocus name='name' value='${contact.name}'></td> <td><input name='email' value='${contact.email}'></td> <td> <button class="btn danger" hx-get="/contact/${contact.id}">Cancel</button> <button class="btn danger" hx-put="/contact/${contact.id}" hx-include="closest tr">Save</button> </td> </tr>从 hx-target 属性文档可以看到,hx-target支持this(当前元素自身)、closest <CSS selector>(向上查找最近的匹配元素,如closest tr)以及任意 CSS 选择器,并且该属性可以放在父元素上被继承——这为 CRUD 场景提供了非常灵活的更新定位能力。
你的 UI 是"嵌套"的,更新大多发生在定义明确的块内
超媒体开始"站不稳"的一个场景是:UI 依赖关系跨越了屏幕的结构性区域。一个经常被提起的经典例子是 GitHub 的 "Issues" 标签页上的 issue 计数——很长一段时间里,关闭一个 issue 后标签页上的计数没有正确更新。GitHub 大体上(虽然不是绝对地)采用了超媒体风格的应用。
SPA 爱好者会欢呼:"看,连 GitHub 都做不对这件事!"
这个例子确实暴露了超媒体方法的一个问题:如何干净地更新彼此分离(disjoint)的 UI 部分?htmx 为此提供了多种手段,详见 Updating Other Content 示例,其中给出四种方案:扩大目标(expand the target)、带外交换(hx-swap-oob)、触发自定义事件(配合 HX-Trigger 响应头)、以及 path-deps 扩展。Contexte 的演讲也提到他们用事件方式非常干净地处理了这类问题。
但作者也坦承:这是超媒体方法容易出问题的地方。规避该问题的一个潜在策略是:把某个资源的相互依赖的元素,在屏幕上放到同一个区域/范围内。
举个具体例子:假设一个联系人应用的详情页需要展示与编辑联系人,包含三个区域:
- 基本信息区域(姓名、姓氏等)
- 联系人的邮箱列表及邮箱数量
- 联系人的电话号码列表及电话号码数量
这个 UI 可以按如下方式布局:
在这种布局下,每个子区域都可以拥有自己专属的超媒体端点:
/contacts/<id>/details—— 处理姓名、姓氏等信息/contacts/<id>/emails—— 处理邮箱区域/contacts/<id>/phonenumbers—— 处理电话号码区域
其中的关键技巧在于:邮箱数量和电话号码数量在屏幕上与其集合(collection)共处一地,这样当集合被修改时,可以用 hx-target 只针对那一块区域进行更新。所有数据依赖都被共置(co-located)在一个单一、简单、明确的更新目标内,而且这些区域被替换时彼此互不干扰。每个区域实际上构成了一个独立的"服务端组件",彼此独立,全部嵌套在一个更大的联系人详情 UI 之中。
附注:UI 驱动的超媒体 API
注意,这里的超媒体 API(即我们的端点)是由 UI 驱动的:我们有一个想要达成的 UI 布局,然后让 API 去适配它。如果 UI 变了,我们会毫不犹豫地彻底改造 API 以满足新需求。这是超媒体开发中独特的一面,作者在 《Hypermedia APIs vs. Data APIs》 中做了更深入的讨论——超媒体 API 的消息是自描述的(self-describing),因此 API 可以频繁变动而不破坏客户端;这与必须严格版本化、保持稳定的数据 API(如 JSON API)形成鲜明对比。
当然,有些 UI 需求不允许把相互依赖的元素如此分组;如果上文提到的那些技术(扩大目标、OOB、事件、path-deps)都无法令人满意,那么可能就是时候考虑替代方案了。
你需要"深度链接"与优秀的首次渲染性能
超媒体优于其它方案的最后一个典型场景是:你需要"深度链接"(deep links)——即能直接链接到应用落地页之外的内部页面——或者需要出色的首次渲染性能。
超媒体是 Web 的天然语言,浏览器在给定 URL 后渲染 HTML 的能力极强,因此在"传统" Web 特性(如上两者)上,超媒体方法几乎无可匹敌。SPA 阵营常常引以为傲的"可复制粘贴的 URL",在 htmx 中通过 hx-push-url 等属性即可轻松获得,同时天然保留浏览器后退/前进按钮与书签能力。
超媒体不适合的场景
你的 UI 存在大量动态的相互依赖
正如上文"嵌套 UI"一节所讨论的,当你的 UI 中存在大量散布于各处的依赖关系,且无法承受"整体刷新 UI"的成本时,超媒体就会遇到麻烦。这正是 Roy Fielding 在文首引言中所指出的:Web 是为大粒度超媒体数据传输设计的,而非为大量细碎的小型数据交换设计的。
对超媒体尤其困难的,是当这些依赖是动态的——即依赖那些在服务端渲染时无法确定的信息。最典型的例子是电子表格(spreadsheet):用户可以在任意单元格中输入任意函数,从而在屏幕上动态引入各种依赖关系。
(不过作者也补充:对许多应用而言,Edit Row 示例中的"可编辑行"模式是更通用电子表格行为的可接受替代方案——它把编辑隔离在有限区域内,与超媒体配合得很好。)
你需要离线功能
超媒体分布式架构重度依赖服务端来渲染资源的表示。当服务器宕机或不可达时,架构显然会遇到麻烦。虽然可以借助 Service Worker 处理离线请求(但这是一个复杂的选项),也可以像许多厚客户端应用那样轻易检测到超媒体应用处于离线状态并显示离线提示——但如果你的应用要求在离线环境中具备完整功能,超媒体方法就是不可接受的。
你的 UI 状态更新极其频繁
另一种超媒体不适用的情况是UI 状态频繁更新。典型例子是需要捕捉鼠标移动的在线游戏:在鼠标移动与 UI 更新之间插入一次超媒体网络请求是行不通的。这种情况下,你应该为自己的游戏编写客户端状态管理,并用其它技术与服务器同步。
但注意,作者特别强调:你的游戏可能也有一个设置页,而那个设置页用超媒体做,可能比你为游戏核心选择的方案更好。在"过渡式"风格下混合使用不同方法完全没问题。
作者还补充了一个重要的可操作性观察:通常把 SPA 组件"嵌入"更大的超媒体架构中,比反向操作更容易。孤立的客户端组件可以通过事件与更广泛的超媒体应用通信——正如 Sortable.js 拖拽排序 + htmx 示例所演示的:用htmx.onLoad初始化 Sortable 实例,以hx-trigger="end"监听拖拽结束事件并hx-post="/items"提交新顺序,同时在htmx:afterSwap事件后重新启用排序:
htmx.onLoad(function(content) { var sortables = content.querySelectorAll(".sortable"); for (var i = 0; i < sortables.length; i++) { var sortable = sortables[i]; var sortableInstance = new Sortable(sortable, { animation: 150, ghostClass: 'blue-background-class', filter: ".htmx-indicator", onMove: function (evt) { return evt.related.className.indexOf('htmx-indicator') === -1; }, onEnd: function (evt) { this.option("disabled", true); } }); sortable.addEventListener("htmx:afterSwap", function() { sortableInstance.option("disabled", false); }); } })<form class="sortable" hx-post="/items" hx-trigger="end"> <div class="htmx-indicator">Updating...</div> <div><input type='hidden' name='item' value='1'/>Item 1</div> <div><input type='hidden' name='item' value='2'/>Item 2</div> <div><input type='hidden' name='item' value='3'/>Item 3</div> <div><input type='hidden' name='item' value='4'/>Item 4</div> <div><input type='hidden' name='item' value='5'/>Item 5</div> </form>你想要开箱即用的"复制粘贴"组件
近年来出现了大量"Copy & Paste"友好型组件,例如 ShadCN。这些组件通常为 React 等特定前端框架设计,选择 htmx 意味着你无法使用它们。虽然也存在框架中立的组件库(如 lit),但它们与 htmx 的集成度远不及 ShadCN 与 React 的集成度。如果你的团队高度依赖这类生态组件,这一点需要在选型时纳入考量。
你的团队不买账
最后一个不建议选择超媒体的原因不是技术性的,而是社会性的(sociological):当前超媒体在 Web 开发界并不流行。许多公司已把 React 作为构建 Web 应用的标准库,许多开发者与顾问把自己的职业生涯押注其上,许多招聘经理从未听说过超媒体、更别说 htmx,却出于习惯在每份招聘启事上写上 React——招聘显然容易得多。
作者承认这令人沮丧,但这是一个真实存在的现象,应当以谦逊的态度正视:虽然 Contexte 能够快速高效地用 htmx 重写他们的应用,但并非每个团队都如此小而敏捷、充满热情,也不是每个应用都是该方法的"灌篮(slam dunk)"。务实的建议是:先在边缘地带采用超媒体——也许先从内部工具开始——在证明其价值之后,再考虑更大范围的推广。
结论:把问题从"应用级"细化到"功能级"
作者经常被问到:"那么,什么样的应用不适合htmx?" 他们更倾向于使用"过渡式"应用概念、以**功能为单位(feature-by-feature)**来思考问题,但脑海中保留一些广为人知的应用作为参照,有助于判断超媒体与其他方法各自能覆盖多少。
- 适合超媒体实现的知名应用例子:Twitter与GMail。两者都是文本与图像密集、粗粒度更新的 Web 应用,非常适合超媒体方法。
- 不适合超媒体实现的知名应用例子:Google Sheets与Google Maps。Google Sheets 的许多单元格之间存在大量状态与相互依赖,不可能对每次单元格更新都发起服务器请求;Google Maps 则需要对鼠标移动做出快速响应,同样无法承受每次移动都经历一次服务器往返。这两者都需要远比超媒体所能提供的更复杂的客户端方案。
当然,绝大多数 Web 应用都远未达到这些例子的规模与复杂程度。而且几乎每个 Web 应用——即便是 Google Sheets 或 Google Maps——都有一些部分可能更适合超媒体方法:更简单、更快、更干净。
把超媒体纳入你的工具箱,会提升你作为 Web 开发者解决工程问题的能力——即便它不会成为你最爱的那把锤子。这里有扎实的理论基础,有对许多应用的实践收益,并且它在某种意义上"顺着 Web 的纹理(with the grain of the web)"行事,而这正是其它方法所不具备的。
进一步阅读(仓库内资源)
- 架构总览:《Hypermedia-Driven Applications》 —— HDA 架构的定义与示例片段(如 Active Search 示例)
- 理论基石:《HATEOAS》、《Hypermedia APIs vs. Data APIs》
- 实践案例:《A Real World React → htmx Port》
- 交互示例:Click to Edit、Edit Row、Updating Other Content、Sortable
- 属性文档:hx-target、hx-swap-oob、hx-include
- 核心实现:htmx 源码 与类型声明 位于仓库
src/目录,测试用例位于 test/ 目录,可对照验证上述属性的实际行为
- 前端
【免费下载链接】htmx
htmx - high power tools for HTML
相关推荐
htmx Essays 全览:一份围绕超媒体(Hypermedia)与 REST 思想体系构建的官方文章索引
htmx Essays 全览:一份围绕超媒体(Hypermedia)与 REST 思想体系构建的官方文章索引 导读:htmx 官网的 Essays 栏目(位于仓
前端sealos技术选型:架构决策与技术栈
sealos技术选型:架构决策与技术栈 引言:云原生时代的架构革命 在云原生技术蓬勃发展的今天,传统的云计算架构正面临着前所未有的挑战。复杂的部署流程、高昂的学
云原生后端前端使用 API Blueprint 描述超媒体 API:Polls Hypermedia API 实战范本
使用 API Blueprint 描述超媒体 API:Polls Hypermedia API 实战范本 API Blueprint 是一套建立在 Markdo
文档API设计教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考