htmx 2 换一次 DOM 焦点必丢,htmx 4 内置 morph swap 把焦点、选区、滚动位置全保住了
用 htmx 写过表单的人都见过这个瞬间:提交后服务器返回的新 HTML 是到了,页面也更新了,可输入框焦点丢了、滚动条跳回顶部、刚敲到一半的表单像是被人清空。这不是你的代码写得差——htmx 2 的交换机制本来就是整棵子树替换,focus、scroll、selection 全部跟着 DOM 节点一起销毁。2026 年 8 月 28 日发布的 htmx 4.0.0(直接跳过 3.0)把这个问题在内核里解决了:内置基于 idiomorph 的 morphing swap,默认就保留 DOM 状态,一行扩展都不用装。
从「整棵换掉」到「只换变了的」
htmx 2 的 hx-swap="innerHTML" 本质是 element.innerHTML = newHTML:旧节点整棵销毁,新节点整棵插入。浏览器没有任何机会保留焦点或滚动位置,因为承载这些状态的节点对象已经不存在了。
htmx 4 的 morph swap 走的是另一条路——对比新旧两棵 DOM 树,只对真正变化的节点做增删改,未变化的节点原地保留。这和 React 的虚拟 DOM diff 思路类似,但直接作用在真实 DOM 上,没有虚拟层开销。库体积没有变重:htmx 4 全量仍然约 14KB。
<!-- htmx 2:交换后焦点必丢 -->
<form hx-post="/profile" hx-swap="innerHTML">
<input name="email" />
<button type="submit">保存</button>
</form>
<!-- htmx 4:morph 合并新旧 DOM,焦点、滚动位置、文本选区原样保留 -->
<form hx-post="/profile" hx-swap="morph:inner">
<input name="email" />
<button type="submit">保存</button>
</form>
morph:inner 只合并内容,morph:outer 连外层标签一起换但目标状态仍保留。htmx 4 把 morph 设为默认交换策略,旧代码不改也能受益。
顺手解决的第二个问题:一次响应更新多处
以前一个响应想同时更新页头计数和正文列表,得用 out-of-band swap(hx-swap-oob),把片段散落在返回 HTML 各处,靠属性标记目标——能用,但乱。htmx 4 新增 <hx-partial> 标签:
<!-- 服务器一次返回三块,htmx 4 自动分发到对应目标 -->
<hx-partial name="header" />
<hx-partial name="content" />
<hx-partial name="footer" />
目标和交换方式写在 partial 自身上,响应自己说明每一块要做什么,不用再回头翻标记里的 hx-swap-oob。
升级前必须知道的坑:属性继承从隐式改成了显式
这是 htmx 4 破坏性变更里最容易在生产出事的一条。htmx 2 中写在父元素上的 hx-headers、hx-confirm、hx-target 会自动传给子元素;htmx 4 要求显式加 :inherited 后缀:
<!-- htmx 2:CSRF token 自动传给子按钮,一切正常 -->
<div hx-headers="X-CSRF-Token: abc">
<button hx-post="/submit">提交</button>
</div>
<!-- htmx 4:不加 :inherited,token 到不了子按钮 -->
<div hx-headers:inherited="X-CSRF-Token: abc">
<button hx-post="/submit">提交</button>
</div>
不加后缀的后果是静默失败:请求照发,服务器返回 403,浏览器控制台没有任何 htmx 报错,页面看着完全正常。DEV Community 的迁移文章专门警告了这一点——CSRF 场景是最高发的中招点。事件命名也同步改了:htmx:afterRequest 变成 htmx:after:request,hx-disable 改名 hx-ignore(旧名字被挪给了「请求期间禁用」这个新用途)。
官方给了扫描器,升级前先跑一遍:
npx htmx.org@4.0.0 upgrade-check -- ./templates
它会逐行标出需要加 :inherited 的属性、改名的属性和旧事件名,默认覆盖 .html/.php/.js/.ts/.jinja/.erb/.hbs,.vue/.svelte/.jsx 要加 --ext 才扫。
htmx 2 和 htmx 4 对照
| htmx 2.x | htmx 4.0 | |
|---|---|---|
| 请求底层 | XMLHttpRequest | fetch()(支持原生流式) |
| 默认交换 | innerHTML,状态全丢 | morph,焦点/滚动保留 |
| 属性继承 | 隐式传递 | 需 :inherited 后缀 |
| 历史回退 | localStorage DOM 快照 | 重新获取页面 |
| 4xx/5xx 响应 | 静默忽略 | 默认换进 DOM |
| 默认超时 | 无 | 60 秒 |
| npm 标签 | latest(至少到 2027 初) |
next |
版本节奏也值得留意:2.x 是 npm 的 latest,npm install htmx.org 拿到的仍是 2.x,未固定版本的 CDN 链接不会被强制升级;2.x 官方承诺无限期维护。迁移没有时间压力,但 morph swap 带来的体验提升是立竿见影的。
下一步
- 今天就跑扫描器:
npx htmx.org@4.0.0 upgrade-check -- ./templates,先看报告长度再决定迁移节奏;CSRF 相关的hx-headers优先处理 - 新项目直接上 4.0:
npm install htmx.org@4.0.0或固定版本 CDN 路径,morph 默认生效 - 存量项目挂 htmx-2-compat 扩展渐进迁移:它把旧事件名和隐式继承先恢复回来,等扫描报告清零再摘掉
评论区
登录后可评论。