五个核心请求属性
htmx 通过五个属性让任意 HTML 元素发出对应的 HTTP 请求。这五个属性的值都是目标 URL,触发时机由元素默认事件决定(可通过 hx-trigger 覆盖):
click,输入框为 change,表单为 submit。GET 请求不携带请求体,参数通过 URL 查询字符串传递。submit,会自动序列化表单数据。常用于创建新资源。基础用法示例
<!-- GET:点击加载内容 -->
<button hx-get="/api/news" hx-target="#news-list">
加载最新新闻
</button>
<!-- POST:提交表单创建资源 -->
<form hx-post="/api/comments" hx-target="#comment-list" hx-swap="afterbegin">
<input name="content" placeholder="写下你的评论"/>
<button type="submit">发布</button>
</form>
<!-- PATCH:内联编辑,失焦时保存 -->
<input
name="username"
value="Alice"
hx-patch="/api/users/42"
hx-trigger="change"
hx-target="closest .user-row"
/>
<!-- DELETE:删除列表项并移除 DOM -->
<li id="item-7">
待办事项
<button
hx-delete="/api/todos/7"
hx-target="closest li"
hx-swap="outerHTML"
>删除</button>
</li>
HX-Request 请求头
htmx 发出的每个请求都会自动携带一组特殊请求头,服务器可以利用这些头来区分普通请求和 htmx 局部请求,进而返回不同的响应内容。
"true",表明这是一个 htmx 发出的请求。这是最常用的判断条件。id(如果有)。便于在服务器端识别是哪个元素发出的请求。id(hx-target 指向的元素,如果有 id)。name 属性值。在表单中有多个提交按钮时,可用于区分点击了哪个按钮。hx-boost 增强的链接/表单触发时,值为 "true"。服务器端检测 htmx 请求
# FastAPI 示例
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.get("/news")
async def news_page(request: Request):
is_htmx = request.headers.get("HX-Request") == "true"
if is_htmx:
# 返回 HTML 片段(不含 <html><head><body>)
return templates.TemplateResponse(
"partials/news_list.html",
{"request": request, "items": await fetch_news()}
)
else:
# 返回完整 HTML 页面(含布局)
return templates.TemplateResponse(
"pages/news.html",
{"request": request, "items": await fetch_news()}
)
// Go 示例
func newsHandler(w http.ResponseWriter, r *http.Request) {
isHtmx := r.Header.Get("HX-Request") == "true"
if isHtmx {
// 渲染局部模板
tmpl.ExecuteTemplate(w, "news-list", data)
} else {
// 渲染完整页面
tmpl.ExecuteTemplate(w, "news-page", data)
}
}
这是 htmx 最优雅的地方之一:同一个 URL 既可以作为完整页面访问(直接输入 URL、SEO 爬虫),也可以作为局部片段被 htmx 请求。通过检查 HX-Request 请求头,服务器智能地选择返回完整 HTML 还是 HTML 片段,完美支持渐进增强。
响应头:从服务器控制客户端行为
htmx 不仅发送特殊请求头,还可以识别服务器响应中的特殊响应头来触发客户端行为,无需客户端 JavaScript:
window.location.href = url)。常用于登录成功后重定向。"true" 时强制浏览器整页刷新。{"showToast": "保存成功"}。hx-target,将响应内容插入到服务器指定的不同元素中。在错误场景下非常有用(错误消息展示在与正常内容不同的位置)。hx-swap 策略,让服务器动态决定如何插入内容。# 使用响应头控制客户端行为
from fastapi.responses import HTMLResponse
@app.post("/api/login")
async def login(credentials: LoginForm):
user = await authenticate(credentials)
if user:
# 登录成功:重定向到仪表板
return HTMLResponse(
content="",
headers={"HX-Redirect": "/dashboard"}
)
else:
# 登录失败:返回错误 HTML,并指定插入错误消息区域
return HTMLResponse(
content='<p class="error">用户名或密码错误</p>',
status_code=422,
headers={
"HX-Retarget": "#error-msg",
"HX-Reswap": "innerHTML"
}
)
hx-boost:渐进增强全站链接
什么是 hx-boost
hx-boost="true" 是 htmx 最简单的全局优化手段。将其放在 <body> 或某个容器元素上,该元素内所有 <a> 链接和 <form> 表单都会被 htmx 接管:
- 链接点击 → GET 请求 → 用响应的
<body>内容替换当前页面的<body> - 表单提交 → 对应 HTTP 方法请求 → 同上
- URL 自动更新到目标地址(浏览器历史正常工作)
<!-- 一行代码让整个页面的导航变成 AJAX 式(SPA 感觉) -->
<body hx-boost="true">
<!-- 这些链接现在都会 AJAX 加载,不再整页刷新 -->
<nav>
<a href="/home">首页</a>
<a href="/about">关于</a>
<a href="/contact">联系</a>
</nav>
<!-- 排除某个链接(外链、下载等) -->
<a href="/files/report.pdf" hx-boost="false">
下载报告(普通跳转)
</a>
</body>
htmx 拦截链接点击后,向目标 URL 发出 GET 请求,取回完整 HTML,然后只替换 <body> 的内容(保留 <head>,避免重新加载 CSS/JS)。效果类似 Turbolinks/Turbo,但无需额外依赖。
注意:如果目标页面的 <head> 中有新的 CSS/JS,htmx 会自动合并(加载新增的,保留已有的)。
hx-boost 与 CSS 过渡动画
/* 配合 View Transitions API,页面切换有流畅动画 */
@keyframes fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* htmx 在 swap 时会短暂添加 htmx-swapping 类 */
body.htmx-swapping {
opacity: 0;
transition: opacity 0.2s ease;
}
/* 或使用 CSS View Transitions(现代浏览器) */
::view-transition-old(root) {
animation: fade-out 0.2s ease;
}
::view-transition-new(root) {
animation: fade-in 0.2s ease;
}
hx-push-url:管理浏览器历史
当 htmx 进行局部 DOM 更新时,URL 默认不变。hx-push-url 允许你在局部更新的同时更新浏览器地址栏,使"前进/后退"按钮正常工作。
hx-get="/posts/42" hx-push-url="true" 会把 /posts/42 推入历史。hx-push-url 类似,但使用 replaceState 替换当前历史记录而非推入新条目。<!-- 标签页切换,URL 随之变化 -->
<nav class="tabs">
<button
hx-get="/dashboard/overview"
hx-target="#tab-content"
hx-push-url="/dashboard/overview"
>概览</button>
<button
hx-get="/dashboard/analytics"
hx-target="#tab-content"
hx-push-url="/dashboard/analytics"
>分析</button>
</nav>
<div id="tab-content">
<!-- 局部更新区域 -->
</div>
<!-- 服务端需要支持直接访问这些 URL -->
<!-- GET /dashboard/overview → 检查 HX-Request,返回片段或完整页面 -->
htmx 默认会在浏览器的 localStorage 中缓存页面快照(最多 10 个),用于后退时恢复页面状态。如果页面内容频繁变化(如实时数据面板),可以通过 htmx.config.historyCacheSize = 0 禁用缓存,让后退时重新从服务器获取最新内容。
hx-params:控制发送的参数
默认情况下,htmx 会发送触发元素所在表单的所有字段。hx-params 允许精细控制:
<!-- 只发送 name 和 email 两个字段 -->
<form hx-post="/api/subscribe" hx-params="name,email">
<input name="name" value="Alice"/>
<input name="email" value="alice@example.com"/>
<input name="phone" value="123-456"/> <!-- 不会被发送 -->
<button type="submit">订阅</button>
</form>
<!-- 不发送任何表单参数 -->
<button hx-post="/api/action" hx-params="none">执行</button>
<!-- 发送除 csrf_token 之外的所有字段 -->
<form hx-post="/api/save" hx-params="not csrf_token">
<input name="title" value="..."/>
<input name="csrf_token" value="secret"/>
<button type="submit">保存</button>
</form>
完整工作流示例
实时搜索(GET + hx-push-url)
<!-- 前端 -->
<input
type="search"
name="q"
placeholder="搜索产品..."
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
hx-push-url="true"
hx-indicator="#spinner"
/>
<span id="spinner" class="htmx-indicator">搜索中...</span>
<div id="results"></div>
# 服务器端
@app.get("/search")
async def search(request: Request, q: str = ""):
products = await search_products(q)
is_htmx = request.headers.get("HX-Request")
if is_htmx:
# 只返回搜索结果片段
return templates.TemplateResponse(
"partials/product_list.html",
{"request": request, "products": products, "q": q}
)
# 直接访问 /search?q=xxx 时返回完整页面
return templates.TemplateResponse(
"pages/search.html",
{"request": request, "products": products, "q": q}
)
GET 请求中,表单字段会被序列化为 URL 查询字符串。例如 <input name="q" value="htmx"> 配合 hx-get="/search" 会发出请求 GET /search?q=htmx。这使得搜索 URL 可以被书签保存和分享。