首页 / 文章 / 升级到 htmx 4:Fetch、显式继承以及会出问题的地方

升级到 htmx 4:Fetch、显式继承以及会出问题的地方

了解 htmx 4 的架构变化,从基于 Fetch 的核心机制与显式属性继承,到错误替换、形态变换及历史记录功能,并制定安全的迁移方案。

2717 词

HTMX 基于一个简单且刻意选择过时方式的设想:服务器返回 HTML,浏览器将该 HTML 插入页面中,这样就能生成一个功能完备的应用程序,而无需将整个界面都作为客户端状态进行复制。第 4 版在保留这一设想的同时,利用现代浏览器的原生功能重新构建了其底层架构。本指南将解释发生了哪些变化以及原因,哪些变化可能会在悄无声息中破坏现有应用,以及如何以结构化的审计方式而非简单的版本升级来执行此次更新。相关细节基于撰写本文时的 HTMX 4 版文档,迁移前请务必对照当前版本说明确认具体内容。

HTMX 4 所保留的契约

回想一下 htmx 替代了什么会有所帮助。典型的客户端渲染应用会向 API 请求 JSON 数据,将这些数据存储在 JavaScript 中,据此渲染组件,并不断同步本地状态与服务器状态。而 htmx 则省去了大部分中间环节——服务器直接返回用户实际需要的格式,也就是 HTML。

以一个带有 hx-post 等属性、目标为 #task-list 且采用追加方式的按钮为例。当有人点击该按钮时,htmx 会收集请求相关上下文并发送到服务器,解析返回的标记内容,然后将这些标记追加到任务列表中。验证、授权、数据持久化以及界面展示等功能仍由服务器处理,而交互、导航、焦点控制以及文档本身则由浏览器负责。

这远不止是对 fetch() 的简略称呼。这些属性描述了一种超媒体控制机制:可执行的操作是什么、该操作被发送到何处,以及生成的响应内容应如何融入当前页面。返回的片段本身也可以包含链接和表单,用来提示下一步可执行的操作,就像完整页面一样。换言之,HTML依然是应用协议;htmx仅负责协调数据的双向传输过程。

第4版几乎保持了这一公共接口的稳定,没有太多变化。它重新整理的是其背后的生命周期机制。其结果并非新的前端框架,而是为HTML、HTTP、DOM以及拥有状态管理的服务器之间的协作制定了更严格的规则。

基于Fetch API重建的核心

早期版本依赖 XMLHttpRequest。在htmx需要兼容旧版浏览器时,这种选择是合理的,而且XHR提供了某些应用所依赖的上传进度事件。然而随着时间推移,这一兼容性决策逐渐演变成了架构上的负担。htmx团队在名为Fixi的较小项目以及流式HTML技术的实验基础上,重新设计了基于Promise的Fetch API的请求流程。

可预测的事件命名规则

更清晰的流程体现在事件模型中。现在的事件名称遵循统一的格式 htmx:phase:action,必要时可再添加子操作(htmx:phase:action:sub-action)。由于阶段名称位于最前,因此可以一目了然地判断监听器是在其对应的步骤之前还是之后触发。

每个与请求相关的事件都会收到相同的上下文对象。扩展模块和监听器无需再拼凑各种与特定事件相关的细节来查找源元素、请求配置、响应以及待处理的交换数据;所有信息都在一处。此外,请求还会包含一个finally阶段,无论结果是成功、失败还是取消,该阶段都会被触发。这正是用于执行清理操作(如隐藏加载指示器或重新启用按钮)的理想时机。

如果你的代码库是按名称来监听htmx事件的,那么每个监听器都需要重新检查,因为旧的名称已不再与新方案匹配。

已被平台取代的封装函数

Htmx 4还去掉了那些重复了浏览器现已稳定提供的API的辅助函数:

  • htmx.addClass()已被element.classList.add()取代
  • htmx.closest() 已被 element.closest() 取代
  • htmx.remove() 已被 element.remove() 取代
  • 这是一种合理的演进。一旦平台已经具备了这些功能,小型库就不应再长期保留这些便捷的 API,而应使用任何开发者都已熟悉的标准 DOM 方法来替代。

    属性继承现在为可选功能

    此次迁移中最具影响力的变化与 Fetch 无关。Htmx 4 不再默认从父元素继承大多数属性。

    此前,放置在容器上的属性——例如目标元素、确认提示或一组请求头——会自动应用于所有基于 htmx 的后代元素。在版本 4 中,必须通过在父元素的属性名后添加 :inherited 后缀来明确声明这种继承关系。如果后代元素希望扩展而非覆盖继承来的值或选择器,则可以使用 :append 后缀。

    该后缀并非装饰性元素,它向读取模板的人表明父元素的属性确实是其后代元素行为的一部分。这进一步推动了 htmx 向行为局部性的发展:声明越靠近其所控制的元素,读者就需要重构的隐含上下文就越少。共享行为仍然是可行的;其作用范围只需在声明处明确说明即可。

    为何这是升级中最具风险的部分

    这一变更还会引发一种隐蔽的故障模式。假设布局容器中始终存在 CSRF 标头,升级后页面显示与之前完全一致,但子元素发送的请求不再包含该标头,进而被服务器拒绝。在有人提交表单之前,一切看似正常。

    Htmx 提供了官方的升级检查工具,可扫描模板和脚本中的隐式继承、过时的事件名称、已被移除的属性以及废弃的 API。请将检查报告作为参考清单而非绝对保证,随后实际测试各种请求路径,尤其是那些需要通过标头、确认信息或共享目标才能访问的路径。

    错误响应变成了可被替换的片段

    在 Htmx 2 中,默认情况下状态码为 4xx5xx 的响应不会被替换。Htmx 4 反转了这一规则:它会替换除 204 No Content304 Not Modified 之外的所有 HTTP 响应。

    当不同状态码类别需要被发送到不同位置或适用不同的替换规则时,新的 hx-status 属性允许你针对每个状态码类别进行配置,例如将验证错误发送到内联消息区域,而服务器错误则显示在页面级别的横幅上。

    实际影响体现在服务器端。现在,每一个错误响应都必须是能够被接收它的元素所识别的有效片段。完整的堆栈跟踪信息或原始的 JSON 错误内容会原样插入 DOM 中。状态码对缓存、日志和客户端而言仍保留其 HTTP 含义,而 HTML 正文则负责呈现内容,并尽可能包含用户可采取的下一步操作,比如修正后的表单。

    通过一次响应更新多个区域

    通常,单次服务器端操作需要修改的不仅仅是用户点击过的元素。发送消息时,可能需要将其添加到时间轴中、更新未读计数器,并替换分页控件。Htmx 长期提供针对此场景的离带交换功能:响应中被标记为离带的元素会替换文档中其他位置的对应元素。

    Htmx 4 引入了一个更为明确的工具,即 <hx-partial> 元素。每个部分都会指定自己的目标及替换策略,因此响应内容会以一系列明确标识的更新项形式呈现。

    同时也定义了排序规则:首先替换主响应内容,随后按文档顺序处理各个部分及非核心元素。这样的设计有助于让每次更新都具有独立意义,而不必依赖同一响应中先前 DOM 变更带来的副作用。

    这实际上实现了一种轻量级的响应协调机制。服务器可以在一个响应中描述某次操作带来的所有可见结果,无需返回 JSON 并让客户端代码负责在各个组件间分配字段。

    保持浏览器状态不变的动态替换

    替换 innerHTML 虽然容易理解,但会丢弃浏览器所维护的状态。文本字段可能会失去选中状态,焦点可能跳走,视频可能会重新开始播放,而自定义元素即便大部分标记未变也可能会被拆解并重新创建。

    Htmx 4 提供了基于改进后的 Idiomorph 算法实现的 innerMorphouterMorph 交换模式。这种模式不会丢弃目标子树,而是通过比较旧节点与新节点,应用最小且合理的变更集。匹配的节点会保留其身份,进而保持焦点、选中状态、播放位置以及内部状态。

    变形功能并不总是更好的选择。对于简单的片段,整体替换通常更安全且更易于调试。只有当目标内容包含动态表单控件、自定义元素、媒体文件或身份至关重要的第三方小部件时,变形功能才有价值。HTMX还提供了选择器,可在变形过程中跳过整个节点或其子节点,这是保护嵌入式地图或富文本编辑器等具有状态的特殊内容的有效方法。

    这一通用原则在HTMX之外也同样适用:服务器决定HTML的内容,而浏览器则保留那些需要在转换过程中保持不变的DOM对象。

    流式处理功能存在于扩展模块中,而非核心代码中

    Fetch为htmx提供了更完善的流式响应基础,但核心并未强制所有人使用同一种流式传输协议。相反,htmx 4为服务器推送事件、WebSockets以及多部分响应提供了独立的专用扩展。

    其中用于处理多部分响应的hx-multipart扩展能够理解multipart/mixedmultipart/parallel格式的请求体。每个部分都可以携带HTML内容以及自身的HX-*动作标识头。这样服务器就可以先发送一个临时占位内容,等耗时操作完成后再逐个流式传输后续部分,而无需自行构建客户端消息总线。

    选择哪种扩展取决于数据流的特性:

    • SSE适用于从服务器端按顺序推送的单向更新。
    • WebSockets则适合真正的双向通信。
  • Multipart适用于在单次HTTP操作中分阶段返回多个数据格式的场景。
  • 这三种方式都通过相同的交换机制实现,因此其余的标记内容无需关心是哪种传输方式带来了相应数据片段。

    更简单的扩展模型

    将流式处理独立于核心部分,有助于保持核心代码的简洁,同时扩展机制也获得了更强的功能。htmx 4中的扩展可以直接注册,并能接入请求、响应和数据交换阶段。只需引入相应的脚本即可启用该扩展;hx-ext属性已不再存在。如果需要明确的边界限制,可通过配置指定站点允许使用的扩展名称。

    HCON:属性选项的简洁表示法

    随着属性不断增加选项,htmx需要一种比嵌入在属性值中的JSON更简洁的语法。答案就是HCON,即htmx配置对象表示法。它支持用空格分隔的键值对、作为布尔值的纯标志符、数字、带引号的字符串以及用于嵌套的点号键。

    JSON仍然被支持,当服务器已生成配置时这很方便。HCON则针对手动编写的标记语:简洁到易于浏览,同时又具有足够的结构化,使得htmx无需为每个属性单独创建解析器。这种表示法被广泛应用于触发器、交换修饰符、请求配置、头部信息、值以及HX-Location响应头中,因此只需学习一次便可在各处使用。

    用于处理剩余客户端状态的hx-live

    超媒体并未消除所有本地交互功能。在任何请求发出之前都会先弹出下拉菜单,每次按键时字符计数器也会随之更新。标签页、展开式组件以及临时选择通常由浏览器自行处理。

    新的 hx-live 扩展通过一个以 DOM 为中心的轻量级脚本层解决了这些问题。它提供了查询辅助工具、用于查找附近元素的定向选择器、DOM 工具、异步辅助函数、对属性和数据值的类型化访问方式,以及 :text:class:hidden 等响应式绑定功能。

    它的核心原则具有哲学意义:DOM 就是状态存储库。响应式表达式会从附近元素读取状态并更新相应的界面显示。所有持久性数据仍由服务器掌控,并以 HTML 的形式像以前一样呈现在页面上。

    可以将 hx-live 视为处理小型 UI 任务的泄压阀,而非在浏览器中开发第二个应用程序的工具。仅在需要为临时性 UI 编写重复的事件监听器代码时才使用它。当状态需要在页面导航后保留、在用户之间共享、需要权限控制或参与事务处理时,这类功能应放在服务器端。

    历史记录导航会重新获取数据而非恢复快照

    Htmx 2 将历史记录快照存储在 localStorage 中。恢复这些快照虽然能找回由无关脚本所做的 DOM 变更,但却无法恢复生成这些变更的 JavaScript 运行时状态。页面看似具有交互性,但实际上只是被冻结的 DOM:组件虽然已渲染,但其背后并无任何逻辑连接。

    Htmx 4 会清除默认缓存。在页面前后导航时,它会重新获取页面内容,并将结果放入 <body> 中或指定的历史记录元素中。合理的 HTTP 缓存头可以让此类请求几乎无需成本,同时为脚本提供干净的文档以进行初始化。

    那些确实需要本地快照的应用程序可以加载 hx-history-cache 扩展,该扩展利用 sessionStorage 并明确实现了相关功能。在整个版本中都遵循这一模式:默认情况下使用最新版本的内容,而本地重建则是一种可选功能,并有专门的名称。

    将迁移视为行为审计

    最安全的升级方式并非盲目更换代码包,而是仔细检查所有行为可能超出标记边界的地方。大多数页面的标记结构无需更改;工作重点应放在隐式继承、事件监听器、响应处理、历史记录以及扩展功能上。

    首先确定具体的 htmx 4 版本,然后运行迁移文档中描述的官方升级检查工具。按照能够避免属性重命名冲突的顺序处理其检测结果:

    1. 将原本表示“忽略此子树”的旧属性 hx-disable 重命名为 hx-ignore
    2. 之后再将 hx-disabled-elt 重命名为新的 hx-disable。如果颠倒这两个步骤的顺序,可能会意外地将一个属性变成另一个。
  • 在父元素需要持续影响子元素的任何地方添加 :inherited,尤其要关注页眉、确认信息、目标元素以及包含的内容。
  • 更新事件监听器的名称,并用浏览器原生 API 替换已被移除的辅助函数。
  • 测试 4xx5xx 响应,因为它们现在默认会互换,同时要确保每种响应都能返回合理的片段内容。
  • 测试每一个 hx-delete 操作,除非使用 hx-include 明确要求,否则它不会再发送所在表单的数据。
  • 测试前后导航、异步排序、超时设置、请求队列以及你使用的所有扩展功能,同时要记住 hx-ext 已经被移除。
  • 何时选择 htmx 4 作为合适工具

    标题虽改为 Fetch,但其核心理念仍是明确性。只有当属性声明明确指定时,它才会传递给后代元素。默认情况下,失败请求的 HTML 会直接显示给用户,具体异常情况则由开发者自行配置。涉及多个区域的响应会明确说明每部分内容的位置及替换方式。除非刻意启用带名称的快照缓存,否则前后导航都会加载新页面。扩展功能通过统一的生命周期进行集成,而那些原本由 htmx 自带辅助功能的场景则由标准 DOM 方法来处理。

    综上所述,这些设计在避免隐藏行为的同时,不会将应用状态强加到客户端框架中。这正是 htmx 所追求的平衡点:丰富的交互体验、服务器端的控制力,以及仍能清晰描述页面功能的 HTML 结构。

    这并不会让所有界面都变得更简单。图形编辑器、以离线优先为特点的工作空间,或是基于高度协作的本地模型构建的应用程序,可能需要更复杂的客户端架构。然而,许多商业应用主要涉及导航、表单、表格、验证以及服务器已具备的工作流功能。对于这类应用,直接返回处理后的结果往往比保持两个状态机同步更为简单。

    关键要点

    • htmx模型保持不变:元素仍是超媒体控件,响应为HTML格式,状态由服务器管理。
    • 隐式属性继承功能已被取消;缺少:inherited后缀很可能是导致问题 silently 发生的原因,尤其是对于CSRF请求头而言。
    • 错误响应现在默认会进行替换,因此所有的4xx5xx错误响应内容都必须是其目标格式的有效片段。
  • <hx-partial>与形态转换功能使得多区域更新及状态保留型转换更加明确且可预测。
  • 流式处理、客户端交互以及历史记录缓存已被移至可选扩展中,从而保持核心代码的简洁。
  • 先运行升级检查工具,再测试实际请求路径;检查工具可找出潜在问题,唯有通过测试才能确认应用仍能正常运行。
  • 相关阅读