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 行为 |
|---|---|---|
| 属性继承 | 显式声明,通过 :inherited | htmx.config.implicitInheritance = true |
| 错误响应 swap | 只有 204/304 跳过 swap | htmx.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,而不必依赖散布在标记各处的属性。
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