写过 htmx 2 的人都踩过这个坑——属性承继全靠猜,今天 htmx 4.0 用三个 breaking change 彻底翻了

写过 htmx 2 的人都踩过这个坑——在父元素上写一个 hx-confirm,然后发现子元素有时候继承有时候不继承,根本说不清规则是什么。今天 htmx 4.0 用三个 breaking change 把这件事彻底翻了个底朝天。

为什么 htmx 2 的隐式承继是个坑

htmx 2 的属性承继是从 intercooler.js 时代沿袭下来的设计。hx-confirmhx-targethx-headers 这些属性默认会被子元素自动继承,看起来很方便——在父级写一行,子按钮全都有了确认弹窗。

但这个「方便」是假象。

当你在某个子按钮上加了 hx-disinherit 来阻断继承,或者把按钮拖到另一个父级下,行为就开始变得不可预测。hx-disinherit 这个属性本身就是补丁——因为隐式承继本身就是个错的设计,用补丁补补丁,只会越补越乱。

更隐蔽的坑是 hx-headers。很多人用它传 CSRF token,放在最外层 div 上,以为所有子请求都会自动带上。结果 token 根本没到子元素,服务器返回 403,排查半天发现是承继没生效。这种 bug 在测试环境几乎不触发——因为测试环境和生产环境的 DOM 结构往往不同。

htmx 4.0 的解法:承继变成显式

htmx 4.0 把隐式承继彻底删了。默认行为是:子元素不继承任何东西。如果你想让某个属性传到子级,必须显式加上 :inherited 后缀:

<!-- htmx 4.0 -->
<div hx-confirm:inherited="确定删除吗?">
  <button hx-delete="/items/1">删除</button>
  <button hx-delete="/items/2">删除</button>
</div>

hx-confirm:inherited 明确告诉 htmx:这个确认框需要传递到子元素。没有 :inherited 后缀的 hx-confirm 只作用于当前元素。

这个改动看似多打了几个字,实际上是还了开发者对代码行为的掌控权。你写的每一行都明确表达了你的意图,不存在「我以为它会继承但它没有」的情况。

迁移工具:自动找到你需要改的地方

htmx 团队知道这个 breaking change 会让大量项目需要修改。他们提供了一个命令行工具,可以扫描你的模板文件,自动标记所有需要加 :inherited 的地方:

npx htmx.org@4.0.0 upgrade-check -- ./templates

输出大概是这个样子:

templates/index.html:1: [inheritance] hx-headers 需要 :inherited 后缀
  (子元素第3行有 hx-delete,没有 :inherited 的话 CSRF token 不会到达服务器,请求被拒绝)
templates/index.html:2: [inheritance] hx-target 需要 :inherited 后缀
templates/index.html:3: [renamed-attr] hx-disable → 重命名为 hx-ignore
  (hx-disable 现在表示「请求期间禁用」)
templates/index.html:4: [removed-attr] hx-vars 已移除 → 改用 hx-vals 加 js: 前缀

这个工具能扫 .html.php.js.ts.jinja.erb.hbs 等常见模板后缀,也可以用 --ext 扩展自定义后缀。

除了属性承继,htmx 4.0 还改了两件事:

事件名称标准化。 htmx 2 的事件名是随意长的:htmx:beforeRequesthtmx:afterSwaphtmx:configRequest 没有统一规律。htmx 4.0 统一成 htmx:phase:action 格式:htmx:before:requesthtmx:after:swaphtmx:config:request。如果你在 JavaScript 里监听这些事件,需要更新事件名。

历史记录不再默认写 localStorage。 htmx 2 在 localStorage 里缓存 DOM 快照用于前进后退恢复,但这个快照会被第三方 JS 修改,导致恢复出来的页面状态错乱。htmx 4.0 默认重新发请求获取历史内容,不再写 localStorage。如果你需要旧的行为,可以加载 hx-history-cache 扩展。

内部重写:XMLHttpRequest → fetch()

htmx 4.0 把底层从 XMLHttpRequest 迁移到了 fetch()。这次迁移是八年历史上最大的一次内部重构,也是为什么这个版本等了八个月才出来。

fetch() 带来的直接好处是扩展系统可以重新设计。htmx 4.0 配套推出了新的扩展体系:hx-preload(鼠标悬停时预加载)、hx-download(原生 fetch 文件下载)、hx-sse / hx-ws / hx-multipart(流式扩展)等。迁移到 fetch() 让这些扩展的实现比原来干净得多。

另外,htmx 4.0 还内置了 morph 交换(基于改进的 idiomorph 算法),以及一个新的 <hx-swap> 标签用于灵活的多目标交换——这些是原来需要手动引入扩展才能用的功能。

npm 默认仍是 2.x,别急着升级

重要的事说三遍:htmx 2.x 仍是 npm 的 latest 版本,4.0 挂在 next 标签下,至少到 2027 年初才会变默认

这是因为很多项目通过非版本化 CDN URL 引用 htmx(<script src="https://unpkg.com/htmx.org">),如果 npm 自动把 latest 切到 4.0,这些项目会直接触发 breaking change。htmx 团队选择了保守策略:2.x 无限期维护,4.0 保持 next 标签,想升的团队自己改 package.json 版本号。

如果你现在还在用 htmx 2,升级路径是:

  1. npm install htmx@4.0.0 或改 CDN 地址到 https://unpkg.com/htmx.org@4.0.0
  2. 运行 npx htmx.org@4.0.0 upgrade-check -- ./templates 扫出所有需要改的地方
  3. 改完模板里的属性和事件名
  4. 测试所有交互流程,尤其是带参数和 header 的请求

这次升级是 htmx 有史以来最大的一次 breaking change,但也是最值得的一次——隐式承继这个设计 debt 早该还了。


下一步: 如果你的项目里 htmx 2 还在跑,先跑一遍 upgrade-check 看看有多少地方需要改,心里有数再动手。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 7 阅读