认识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 里加几个属性就完事:
<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-target、hx-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:beforeRequest→htmx:before:request,htmx:configRequest→htmx: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。
<div hx-get="/dashboard" hx-swap="innerMorph" hx-trigger="every 5s">
<!-- 看板内容,5 秒刷新一次,但你不觉得它在刷新 -->
</div><hx-partial> 标签。 想在一次响应里更新多个元素?2.x 的 OOB(out-of-band)交换能把人绕晕。htmx 4 直接给你一个能自己声明目标和策略的标签:
<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:
<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 年初才切。这个"不催你升级"的态度,在一个天天催你升大版本的前端圈子里,算一股清流。
<script src="https://unpkg.com/htmx.org@4.0.0/dist/htmx.min.js"></script>第二步:配个 Python 后端。 站内已经有 Flask 的文章,这里就用 Flask 搭一个"边打字边搜"的列表,全文法加起来不到 30 行:
# 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 升级的,先跑官方检查工具:
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-guidance、htmx-debugging、htmx-extension-authoring、htmx-upgrade-from-htmx2),给 AI 编程助手喂正确上下文——这大概是前端库里第一个这么干的。
入门别贪多:先挑一个你项目里"没必要上 React"的小角落——搜索框、加载更多、表单提交后刷新一个面板——用几个
hx-属性重写一遍。一下午就能验证合不合适,比开一场架构评审会划算得多。
进阶
更多开源技术干货和学习资料,关注公众号「遇码」,领取专属福利。
