12k
All articles

htmx 4.0 正式发布

htmx 4.0更改了继承、错误替换、历史记录和事件,并提供htmx 2应用的迁移建议和回退选项。

OpenReplay Team
OpenReplay Team
htmx 4.0 正式发布

htmx 4.0.0 已于 2026 年 8 月 28 日发布。此版本改变了多项长期沿用的默认行为:属性继承改为显式声明(opt-in)、错误响应会被 swap 进 DOM,history 快照缓存被移除。

如果你在维护一个 htmx 2 应用,最实际的问题是:两年前你提升到容器元素上的那个 hx-confirm,升级后是否还能守住任何操作?答案是不能——除非你加上一个修饰符。本文将梳理哪些行为发生了破坏性变更、如何逐项还原,以及为什么 npm 的发布策略意味着你这周很可能并不需要立刻行动。关于 htmx 是什么以及为何选择 hypermedia,htmx 2.0 详解 覆盖了本文的起点内容。

核心要点

  • htmx 4 通过 :inherited 修饰符将属性继承显式化,而将 htmx.config.implicitInheritance 设为 true 可恢复 htmx 2 的行为,作为迁移过渡方案。
  • 在 htmx 4 中默认只有 204 和 304 响应会跳过 swap,因此服务端渲染的 422 现在会进入目标元素,而不再被丢弃;设置 htmx.config.noSwap = [204, 304, '4xx', '5xx'] 可还原旧行为。
  • 事件名称遵循 htmx:phase:action 模式,且没有对应的配置项:你 JavaScript 中的每个 htmx 监听器都需要重命名,或者安装 htmx-2-compat 扩展。
  • 升级前先把 hx-disable 重命名为 hx-ignore,因为 htmx 4 将 hx-disable 这个名称重新指派给了原先 hx-disabled-elt 承担的职责。
  • htmx 2.x 仍占据 npm 的 latest 标签,而 4.0 位于 next 之下,因此不带版本号的 CDN URL 不会被强制升级;公告中声明 htmx 2 将获得无限期支持。

htmx 4 改变了什么?

htmx 4 将库的请求内部实现从 XMLHttpRequest 迁移到了 fetch(),正是这次重写让本次发布的其余变更成为可能。更换传输层本身已是破坏性变更,因此团队借这个大版本,一并重置了自 htmx 1 以来积累下来的各项默认设定。

编写 htmx 时你并不会直接调用这两个 API,所以传输层的替换在模板层面是不可见的。影响体现在边缘场景:XHR 特有的生命周期事件在 fetch() 中没有对应物,已被移除;同时 htmx 4 将 htmx.config.defaultTimeout 设为 60000,而 htmx 2 允许请求无限期挂起。

关于版本号:htmx 作者 Carson Gross 曾说过永远不会有 htmx 3,所以此次发布直接跳到 4.0,这个承诺在字面上得以保全。他在 2025 年 11 月宣布这次重写的文章中阐述了背后的思路。

属性继承现在是显式的

在 htmx 4 中,继承只在你明确要求时才会发生。容器上的属性仅作用于该容器本身,除非你添加 :inherited 修饰符;该修饰符适用于任意属性:hx-boost:inherited、hx-target:inherited、hx-confirm:inherited。

<!-- htmx 4: the confirm reaches both buttons -->
<div hx-confirm:inherited="Are you sure?">
  <button hx-delete="/account">Delete My Account</button>
  <button hx-put="/account">Update My Account</button>
</div>

默认情况下,子元素上的值会覆盖继承来的值。如果你希望将两者合并,则使用 :append——这正是最容易让人栽跟头的组合场景:

<div hx-vals:inherited="tenant:acme">
  <button hx-post="/save" hx-vals:append="source:save-btn">Save</button>
</div>

若不加 :append,按钮自身的 hx-vals 会取代继承来的值,tenant 就永远不会发送到服务端。而当没有任何祖先元素设置该属性时,append 的值就是唯一发送的值。不同属性的命名略有差异:hx-disable 的参考页面中,对于”向父级值追加内容”这同一职责,文档记录的是 :merge。

hx-inherit 和 hx-disinherit 已被移除,因为显式的 opt-in 机制让二者都不再必要。如果你的模板依赖旧行为,可在迁移期间将 htmx.config.implicitInheritance 设为 true 来恢复它。请把它当作过渡桥梁,而不是最终归宿。

错误响应默认会被 swap

在 htmx 4 中,无论状态码是什么,响应都会进入目标元素,只有 204 和 304 例外。服务端渲染的 422 校验页面现在会落入目标元素,而不再被悄无声息地丢弃——这正是 hypermedia 应用一直以来所期望的。HTTP 错误响应还会触发 htmx:response:error 事件。

新增的 hx-status 属性可将不同状态码路由到各自的 target 和 swap 方式:

<form hx-post="/submit"
      hx-target="#result"
      hx-status:422="target:#validation-errors"
      hx-status:5xx="target:#server-error"
      hx-status:503="swap:none">
  <input name="email">
  <button type="submit">Submit</button>
</form>

htmx 会优先尝试最具体的匹配模式:先是精确状态码,然后是末位被掩码的模式(如 50x),再然后是末两位被掩码的模式(如 5xx)。在属性值内部,你可以设置 swap:、target:、select:、push:、replace: 和 transition:。

如果你的后端返回的错误页面从未打算被 swap,那就把 htmx.config.noSwap 设为 [204, 304, '4xx', '5xx'],即可恢复 htmx 2 的行为。

后退导航现在是一次真实请求

htmx 4 移除了 htmx 2 中支撑 history 的客户端 DOM 快照缓存。点击后退时,htmx 会重新向服务端请求该页面,然后把返回内容 swap 进 <body>,或者当页面存在 [hx-history-elt] 元素时 swap 进该元素。

实际效果是:后退按钮展示的是服务端当前认定的页面内容,而不是你离开页面那一刻冻结的快照。这消除了一整类 bug——第三方脚本修改了 DOM,恢复快照时又把这些修改重放出来,导致状态错乱。当然,这也意味着后退导航需要付出一次请求的代价。

hx-history 属性随缓存一起被移除。如果你确实需要快照,hx-history-cache 核心扩展会以 opt-in 的形式重新引入这一能力。

事件名称遵循 htmx:phase:action 模式

所有 htmx 生命周期事件都被重命名为 htmx:phase:action[:sub-action] 的形态。发布公告给出的示例是 htmx:beforeRequest 变为 htmx:before:request,htmx:beforeSwap 变为 htmx:before:swap;htmx:afterSwap 则变为 htmx:after:swap。

这是唯一一项没有配置逃生舱的变更。每个监听器都需要修改:

// htmx 2
document.body.addEventListener('htmx:afterSwap', (e) => {
  initTooltips(e.detail.target);
});

// htmx 4
document.body.addEventListener('htmx:after:swap', (e) => {
  initTooltips(e.detail.target);
});

大多数错误事件被合并为 htmx:error,而 HTTP 错误响应触发 htmx:response:error。XHR 特有的事件则被直接移除,因为 fetch() 没有对应物。如果手工修改监听器构成了你迁移工作的大头,htmx-2-compat 扩展可以把旧事件名映射到新事件名,同时还能恢复隐式继承和 hx-ext。

htmx 4 中有哪些新特性(而非破坏性变更)?

有三项 htmx 4 的新增功能,单凭其自身就值得升级:morph swap、<hx-partial> 元素,以及重写后的流式扩展。morph swap 已进入核心,因此保留状态的 DOM 更新不再需要扩展。<hx-partial> 元素允许一个响应更新多个目标,每个部分携带各自的 target 和 swap:

<hx-partial hx-target="#messages" hx-swap="beforeend">
  <div>New message</div>
</hx-partial>
<hx-partial hx-target="#count">
  <span>5</span>
</hx-partial>

由于 target 和 swap 方式就写在 partial 自身上,响应本身就声明了每一部分要如何处理,而不是让你从散落在标记各处的 hx-swap-oob 属性中去推断。注意 htmx 4 中带外(out-of-band)内容的处理顺序发生了反转:主内容先 swap。

流式扩展是另一大亮点。SSE 和 WebSocket 两个扩展都为本次发布做了重建,同时还附带了一批全新扩展:hx-multipart、hx-live、hx-targets、hx-ptag、hx-csp、hx-download、hx-prompt 和 hx-history-cache。连接类属性采用了命名空间,因此 SSE 使用 hx-sse:connect 建立连接,WebSocket 则使用 hx-ws:connect。

升级到 htmx 4,以及为何不必着急

升级到 htmx 4 的第一步是运行扫描器:在做任何规划之前先跑一遍。npx htmx.org@4.0.0 upgrade-check -- ./path/to/project/root 会遍历你的项目,并打印出每一处已弃用模式及其文件与行号,足以让你在一个下午内评估出工作量。

npx htmx.org@4.0.0 upgrade-check -- ./templates
npx htmx.org@4.0.0 upgrade-check --ext .vue ./path/to/project/root

开箱即用地,它会检查 .html、.php、.js、.ts、.jinja、.jinja2、.j2、.erb 和 .hbs。单文件组件格式不在此列,因此 .vue、.svelte、.jsx 和 .astro 模板不会被检查,除非你传入 --ext。

在动手做任何其他事情之前,先完成一项重命名:hx-disable 变为 hx-ignore,而 hx-disabled-elt 变为 hx-disable。 旧名称被复用于另一项职责,所以如果你先迁移 hx-disabled-elt,就会覆盖掉那些仍表示 htmx 2 含义的属性。

变更项htmx 4 默认行为如何恢复 htmx 2 行为
属性继承显式声明,通过 :inheritedhtmx.config.implicitInheritance = true
错误响应 swap只有 204/304 跳过 swaphtmx.config.noSwap = [204, 304, '4xx', '5xx']
History后退时向服务端重新请求hx-history-cache 扩展
事件名称htmx:phase:action无配置项;使用 htmx-2-compat 扩展

接下来是决定这一切是否紧迫的关键:在 npm 上,htmx 2.x 持有 latest dist-tag,而 4.0.0 发布在 next 之下。公告明确说明这是刻意为之,以便那些通过不带版本号的 CDN URL 加载 htmx 的站点不会被强制升级到破坏性变更中;2.x 将保持 latest 直到 2027 年初。2.x 将获得无限期支持。

这对应四种情形。不带版本号的 CDN URL 会持续提供 2.x,直到标签切换为止——这是唯一带有未来截止时间的情形。固定版本的 CDN URL 和精确的 npm 版本锁定则永远不会自行改变。像 ^2.0.0 这样的 npm 版本范围无论 dist-tag 如何变动都会停留在 2.x 之内。若想今天就安装 4.0,请锁定版本:npm install htmx.org@4.0.0,或者使用带版本号的 CDN 路径。

新项目直接从 4.0 起步。对于现有的 htmx 2 应用,先运行扫描器,优先完成 hx-disable 的重命名,然后根据报告的长度来决定是现在就迁移,还是等 dist-tag 切换之前再来处理。

常见问题

hx-ext 已被移除,那我在 htmx 4 中该如何加载扩展?

在 htmx 脚本之后引入扩展脚本,其属性即刻生效,无需任何激活属性。将 dist/ext/hx-sse.js 与 htmx.min.js 一并加载,你就可以直接使用 hx-sse:connect。htmax.js 发行版将 htmx 与最流行的一批扩展预打包在单个文件中,这些属性自动可用。扩展作者通过 htmx.registerExtension 注册,传入名称和方法映射表。

我能避免 htmx 4 在后退导航时发出的额外服务端请求吗?

可以。hx-history-cache 核心扩展会从 sessionStorage 恢复历史记录,而不是发起完整的服务端请求,这是最接近 htmx 2 快照机制的方案。另外有两个配置值可以改变该行为:将 htmx.config.history 设为 'reload' 会在历史导航时执行整页刷新,设为 false 则禁用 history 处理。htmx 2 的 localStorage 快照缓存已不复存在。

在 htmx 4 中,hx-vars 和 hx-prompt 由什么替代?

hx-vars 已被移除,计算值改用带 js: 前缀的 hx-vals。hx-prompt 从核心中移除并以扩展形式提供:加载 hx-prompt 扩展即可保持相同语法。其他被移除的属性包括 hx-ext、hx-inherit、hx-disinherit 和 hx-history。hx-disabled-elt 是被重命名而非移除:它变成了 hx-disable,而原来的 hx-disable 变成了 hx-ignore,正如 [htmx 4 新特性](https://four.htmx.org/docs/whats-new-in-htmx-4) 中的重命名对照表所列。upgrade-check 扫描器会将这两项标记为 renamed-attr,将真正被移除的属性标记为 removed-attr,并分别给出文件、行号和建议的替代写法。

hx-swap-oob 在 htmx 4 中还能用吗?什么时候该改用 hx-partial?

hx-swap-oob 仍然可用,但 htmx 4 反转了顺序:主内容先进入,带外元素和 hx-partial 元素随后按文档顺序处理。当你要用同一元素的更新版本替换该元素时,使用 hx-swap-oob;当单个响应需要更新多个位置时,使用 hx-partial,因为每个 partial 自行声明其 hx-target 和 hx-swap,而不必依赖散布在标记各处的属性。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.