Skip to content

认识htmx

你有没有算过,为了做一个"点击加载更多"的按钮,你的项目里塞了多少东西?

npm install 敲下去,node_modules 300MB,package-lock.json 一万行。你写了个 <button>,然后配了 React、配了路由、配了状态管理、配了请求库、配了构建工具。等终于跑起来,首屏 2 秒可交互——你还得安慰自己"现代前端就该这样"。

然后有个法国公司做了件很不给面子的事:他们把做了两年的 React UI 换成 Django 模板 + htmx,两个月搞定,代码从 21500 行砍到 7200 行,JS 依赖从 255 个砍到 9 个,构建时间从 40 秒降到 5 秒。用户体验?没变差。

这家公司叫 Contexte,这个分享后来被 htmx 官方称为"所有 htmx Demo 之母"。

前端这十年最大的幻觉是:交互一定要在浏览器里做。而大部分后台系统,其实只是"表单 + 表格 + 看板"。

2026 年 8 月 28 日——就在昨天——htmx 4.0.0 正式发布,8 个月打磨,登上 Hacker News 首页。它干的第一件事就挺反常识:拒不把自己标成 npm 的 latest

什么是 htmx

htmx 是一个只有十几 KB(官方主页口径约 16KB min.gz)的 JavaScript 库,零依赖,无构建步骤。它的全部主张可以用一句话说完:

让任意 HTML 元素都能发 HTTP 请求,让服务器直接返回 HTML 片段,浏览器负责把它换进去。

不用写 JSON API,不用在客户端维护第二份状态,不用虚拟 DOM。你在 HTML 里加几个属性就完事:

html
<button hx-post="/clicked" hx-swap="outerHTML">
  点我
</button>

这行代码的意思是:点这个按钮 → 向 /clicked 发一个 POST → 用服务端返回的 HTML 把按钮自己替换掉。逻辑、状态、渲染全在服务端,浏览器只做"换"这一个动作。

凭什么只有 <a><form> 能发请求?凭什么只有 click 和 submit 能触发?凭什么每次都得整页刷新?——htmx 的四个"凭什么"

htmx 出自 Carson Gross 之手,是 intercooler.js 的继任者,BSD 2-Clause 协议开源,目前在 GitHub 上接近 4.9 万 Star。它的理论根基是 Roy Fielding 原始意义上的 REST 与 HATEOAS——服务端不只返回数据,还把"下一步能干什么"一起写在 HTML 里

配套的还有个好玩的说法叫 HOWL 技术栈(Hypermedia On Whatever you'd Like):因为服务端只需要吐 HTML,你用 Django、Flask、FastAPI、Rails、Laravel、Go、Express 都行,htmx 根本不挑。

htmx 4.0 到底更新了什么

先说结论:从使用者视角看,htmx 4 和 htmx 2 几乎一样。官方明确说了,两者行为差异很小,而且htmx 2.x 会无限期维护,你完全不用急着升

底层:XMLHttpRequest 换成了 fetch()

这是整个版本的地基。XHR 是 IE 时代的老古董,有个硬伤:必须把整个响应缓冲完,htmx 才能往 DOM 里塞。换成 fetch() 之后有了 ReadableStream,HTML 就能边到边渲染——真正的流式 UI,而且只靠一个十几 KB 的库。

代价是:你在 2.x 里写过 XHR 拦截器、自定义传输层的话,会挂。htmx:xhr:* 系列事件也全部移除。

三大破坏性变化

  • 属性继承变成显式的(这是最大的升级负担)。htmx 2 里父元素的 hx-targethx-confirm 会自动被子元素继承,灵感来自 CSS——然后也和 CSS 一样,强大但难懂。htmx 4 里必须显式加 :inherited

    html
    <!-- htmx 2 -->
    <div hx-confirm="确定删除?">
        <button hx-delete="/item/1">删除</button>
    </div>
    
    <!-- htmx 4 -->
    <div hx-confirm:inherited="确定删除?">
        <button hx-delete="/item/1">删除</button>
    </div>

    坑在哪?它不报错。忘了加,按钮就静悄悄地不再弹确认框、不再指向正确的目标。所以官方专门给了命令行检查工具(见下文)。

  • 事件名标准化htmx:phase:action[:sub-action]htmx:beforeRequesthtmx:before:requesthtmx:configRequesthtmx:config:request。大部分错误事件合并进 htmx:error,HTTP 错误统一走 htmx:response:error

  • history 不再用 localStorage 缓存快照。2.x 把整页 DOM 快照存 localStorage,结果第三方 JS 改过的 DOM 会被一起存进去,恢复时 DOM 回来了、JS 逻辑没了——一堆玄学 bug。htmx 4 改成后退时重新拉取页面,配合 HTTP 缓存又快又正确。

两个真正的新玩具

Morph 交换进核心了。 idiomorph(Carson 自己写的 DOM 合并算法)原本是 2.x 的扩展,现在内置。新的 swap 样式是 innerMorph / outerMorph:不再粗暴地整块替换,而是把新内容合并进现有 DOM。输入框焦点不跳、视频不中断、CSS 过渡不断。这不就是 React 那套 diff,但没有 React。

html
<div hx-get="/dashboard" hx-swap="innerMorph" hx-trigger="every 5s">
  <!-- 看板内容,5 秒刷新一次,但你不觉得它在刷新 -->
</div>

<hx-partial> 标签。 想在一次响应里更新多个元素?2.x 的 OOB(out-of-band)交换能把人绕晕。htmx 4 直接给你一个能自己声明目标和策略的标签:

html
<hx-partial hx-target="#messages" hx-swap="beforeend">
    <div>新消息</div>
</hx-partial>
<hx-partial hx-target="#count">
    <span>5</span>
</hx-partial>

意图写在 HTML 结构里,一眼能看懂,比满屏属性汤舒服太多。

扩展生态:fetch() 带来的连锁反应

换成 fetch() 之后,扩展的可能性一下打开了,这次一口气放出一批:

  • hx-preload:鼠标悬停就预加载,点击时请求已经在路上了
  • hx-download:基于 fetch 的原生文件下载,不用再玩 iframe 黑魔法
  • hx-sse / hx-ws / hx-multipart:三种流式传输(SSE、WebSocket、multipart/mixed)
  • hx-alpine-compat / hx-history-cache:和 Alpine.js 的配合与历史缓存
  • hx-live:htmx 团队自己下场做的前端脚本方案,受 Alpine、jQuery、hyperscript 启发,主打"DOM 驱动的、HATEOAS 友好的响应式"
  • htmax.js:懒得挑?一个文件打包 htmx + 常用扩展,直接用

另外还有个值得注意的默认行为变化:htmx 4 默认会交换 4xx/5xx 响应(2.x 会直接忽略)。服务端返回 422 + 一段校验错误的 HTML,它会老老实实换进目标里——用户终于能看到反馈了。想精确控制就用新的 hx-status

html
<form hx-post="/save"
      hx-status:422="swap:innerHTML target:#errors select:#validation-errors"
      hx-status:5xx="swap:none push:false">

还有个容易踩的:默认超时从"永不超时"改成了 60 秒

谁在用:真实案例,带硬数据

说架构都是虚的,上真数据。

Contexte(法国政策与政治媒体 SaaS)——这是最完整、最经得起查的一个案例,2022 年 DjangoCon Europe 上由 David Guillot 公开分享,被 htmx 官方收录:

  • 迁移耗时 2 个月(原代码库 21500 行,绝大部分是 JS)
  • 代码总量 减少 67%(21500 → 7200 行)
  • Python 代码增加 140%(500 → 1200 行)——在官方看来这是好事,能用 Python 就别用 JS
  • JS 依赖 减少 96%(255 个 → 9 个)
  • 构建时间 减少 88%(40 秒 → 5 秒)
  • 首屏可交互时间 降低 50~60%(2~6 秒 → 1~2 秒)
  • 内存占用 降低 46%(75MB → 45MB)
  • 数据集容量反而变大了——原来 React 根本扛不住那个数据量

还有一个容易被忽略的收益:团队结构变了。用 React 时,2 个纯后端、1 个纯前端、1 个"全栈";换成 htmx 后,4 个人全成了全栈。每个人都能独立交付一个完整功能,不用跨端协调。

一个后台页面的成本,从来不只是代码行数,还有"改一个字段要三个人排期"这件事。

除此之外,社区侧的体感数据也值得一提:awesome-htmx 资源库 2200+ Star 还在涨,npm 上月下载量在六十万级。当然,实话实说,htmx 目前的主力战场仍然是内部工具、管理后台、CRUD、内容型应用——你要做在线表格、实时地图、离线优先的应用,老老实实用 React,别硬来。

初体验:五分钟跑起来

第一步:引一行脚本。 注意版本号必须写死 4.0.0——因为 npm 上 latest 还指向 2.x,官方要等到 2027 年初才切。这个"不催你升级"的态度,在一个天天催你升大版本的前端圈子里,算一股清流。

html
<script src="https://unpkg.com/htmx.org@4.0.0/dist/htmx.min.js"></script>

第二步:配个 Python 后端。 站内已经有 Flask 的文章,这里就用 Flask 搭一个"边打字边搜"的列表,全文法加起来不到 30 行:

python
# pip install flask
from flask import Flask, render_template_string, request

app = Flask(__name__)

DATA = ["Doris", "DuckDB", "SeaTunnel", "StarRocks", "Paimon", "TiDB", "ClickHouse"]

PAGE = """
<script src="https://unpkg.com/htmx.org@4.0.0/dist/htmx.min.js"></script>
<input type="search" name="q"
       hx-get="/search"
       hx-trigger="keyup changed delay:300ms"
       hx-target="#result"
       placeholder="搜一个开源项目">
<div id="result">{% for r in rows %}<div>{{ r }}</div>{% endfor %}</div>
"""

ROWS = "{% for r in rows %}<div>{{ r }}</div>{% endfor %}"

@app.get("/")
def index():
    return render_template_string(PAGE, rows=DATA)

@app.get("/search")
def search():
    q = request.args.get("q", "").lower()
    return render_template_string(ROWS, rows=[d for d in DATA if q in d.lower()])

app.run(debug=True)

跑起来打字试试。没有 fetch(),没有 JSON,没有 setState,没有状态同步 bug——delay:300ms 还是 htmx 自带的防抖。

第三步:如果你是从 htmx 2 升级的,先跑官方检查工具:

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

它会扫 .html/.php/.js/.ts/.jinja/.jinja2/.j2/.erb/.hbs,把继承缺失、属性改名/移除、旧事件名、废弃 API 全给你点出来,连"这个 CSRF header 忘了加 :inherited,服务端会拒绝请求"这种都提示到了。官方甚至还放了四个 LLM skills 文件htmx-guidancehtmx-debugginghtmx-extension-authoringhtmx-upgrade-from-htmx2),给 AI 编程助手喂正确上下文——这大概是前端库里第一个这么干的。

入门别贪多:先挑一个你项目里"没必要上 React"的小角落——搜索框、加载更多、表单提交后刷新一个面板——用几个 hx- 属性重写一遍。一下午就能验证合不合适,比开一场架构评审会划算得多。

进阶

更多开源技术干货和学习资料,关注公众号「遇码」,领取专属福利。

遇码MeetCoding 开源技术社区