htmx 4 的核心变化,不是增加一套更复杂的前端运行时,而是重新整理了几个影响实际开发体验的底层能力:从 XMLHttpRequest 转向 fetch()、增强流式响应、内置 morphing swap、引入 hx-partial,并把属性继承改为显式行为。对于偏好服务端渲染和少量 JavaScript 的团队,这些变化会直接影响请求、更新 DOM 以及维护局部状态的方式。
从 XMLHttpRequest 转向 Fetch
fetch() 已经成为现代浏览器处理 HTTP 请求的标准 API。htmx 4 将请求基础迁移到 Fetch,意味着库可以更自然地利用流式响应、标准化的请求配置和更现代的浏览器能力。
对应用开发者来说,最重要的变化不是手动调用 fetch(),而是 htmx 的请求模型更适合逐步接收服务端输出。长列表、日志窗口、服务器推送的局部结果,都可以围绕流式传输设计,而不必等整个响应生成完毕后再替换页面。
可以先用原生 Fetch 验证服务端是否支持流式响应。下面的示例会持续读取响应体,并把每个文本片段追加到日志区域。它不依赖任何构建工具,保存为 HTML 后即可改造:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Fetch stream demo</title>
</head>
<body>
<button id="start">Start stream</button>
<pre id="output"></pre>
<script>
const button = document.querySelector('#start');
const output = document.querySelector('#output');
button.addEventListener('click', async () => {
button.disabled = true;
output.textContent = '';
try {
const response = await fetch('/api/progress');
if (!response.ok || !response.body) {
throw new Error(`HTTP ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
output.textContent += decoder.decode(value, { stream: true });
}
} catch (error) {
output.textContent += `\nRequest failed: ${error.message}`;
} finally {
button.disabled = false;
}
});
</script>
</body>
</html>
运行这个页面前,需要让 /api/progress 返回分块响应。例如,服务端可以按行写入内容,而不是一次性构造完整响应。实际项目中还要确认代理服务器没有缓冲响应,并设置合适的超时时间。
Morphing Swap 让 DOM 更新更细腻
传统的 HTML 局部更新通常会把目标元素的 innerHTML 或整个节点替换掉。这种方式简单,但可能丢失输入框焦点、光标位置、展开状态以及其他由浏览器或客户端代码维护的 DOM 状态。
Morphing swap 的思路是比较旧 DOM 和新 HTML,再尽量复用已有节点。这样,服务端仍然可以返回完整、清晰的 HTML,浏览器端则有机会保留用户正在进行的交互状态。它特别适合以下场景:
- 用户正在编辑的表单局部刷新。
- 可展开的列表或树形菜单重新加载。
- 带有焦点、选中项或滚动位置的结果区域更新。
- 服务端重新渲染后仍希望保留节点身份的组件。
可以这样设计一个 htmx 4 风格的局部更新区域。示例中的 hx-swap="morph" 表达使用 morphing swap;具体属性名称和兼容细节应以实际版本文档为准:
<section id="profile-panel">
<form
hx-put="/profile"
hx-target="#profile-panel"
hx-swap="morph"
>
<label>
Display name
<input name="display_name" value="Ada" autocomplete="name">
</label>
<label>
Time zone
<select name="timezone">
<option value="UTC">UTC</option>
<option value="Asia/Shanghai" selected>Asia/Shanghai</option>
</select>
</label>
<button type="submit">Save</button>
</form>
</section>
这里的关键不在于把所有更新都改成 morphing,而是明确哪些区域需要保留 DOM 状态。纯展示内容、不会被用户交互的区域,普通替换通常更容易理解,也更容易排查问题。Morphing 算法并不能自动解决所有状态冲突,第三方组件、重复 ID 和强依赖节点身份的代码仍然需要测试。
hx-partial 与更清晰的响应边界
hx-partial tag 旨在让服务端返回的局部内容边界更明确。过去,开发者经常依赖目标选择器约定响应必须包含哪些片段;当页面变大、模板变多时,响应的职责容易变得模糊。
实践中,可以把一个请求的响应限制在明确的局部模板内:列表请求只返回列表片段,编辑请求只返回编辑区域。这样做有两个好处:服务端模板更容易复用,前端也更容易判断一次请求会影响什么内容。
一个可改造的结构如下:
<div id="orders">
<button
hx-get="/orders?page=2"
hx-target="#orders"
hx-swap="morph"
>
Load next page
</button>
</div>
<!-- The server response is deliberately limited to this partial region. -->
<hx-partial id="orders">
<ol>
<li>Order #1042 - shipped</li>
<li>Order #1041 - processing</li>
</ol>
</hx-partial>
上面的响应结构是实践示意。接入项目时,应根据 htmx 4 的实际解析规则调整标签用法,并确保服务端模板不会在同一个响应中混入无关页面结构。
显式属性继承,减少隐式影响
属性继承很方便:在父元素上设置请求地址、目标区域或触发行为,子元素可以自动使用这些配置。但隐式继承也会制造维护成本,尤其是组件被复制到另一个页面后,开发者很难从局部代码判断它为什么会发起某个请求。
htmx 4 将属性继承改为显式行为后,团队需要更认真地定义组件边界。建议把共享配置放在真正稳定的容器上,并在跨组件复用时明确声明继承关系。不要依赖“页面上某个更高层的元素碰巧设置了属性”。
同时,事件名称标准化会降低不同模块之间的沟通成本。项目可以把事件命名当作接口管理:统一大小写和命名风格,记录哪些事件由服务端响应触发,哪些事件由用户操作触发。
升级时的检查清单
升级到 htmx 4 前,可以按以下顺序检查:
- 搜索项目中依赖
XMLHttpRequest行为的扩展、拦截器和测试桩。 - 为流式响应增加代理缓冲、断连和超时测试。
- 只对需要保留焦点或节点状态的区域启用 morphing swap。
- 检查模板是否清楚区分完整页面和局部响应。
- 显式梳理父子元素之间的属性继承关系。
- 统一事件名称,并更新自动化测试中的事件断言。
- 在真实表单、键盘操作和浏览器后退场景中验证 DOM 状态是否保留。
htmx 4 的方向仍然很克制:把更多能力放进 HTML 请求和响应流程,同时减少应用层 JavaScript 的数量。它并不意味着所有页面都应该放弃组件框架,而是为服务端渲染、渐进增强和局部交互提供了一条更完整的路径。采用时应从请求边界和 DOM 状态入手,逐块迁移,而不是一次性重写整个前端。