hx-target:指定更新目标
默认情况下,htmx 将服务器响应的 HTML 内容更新到发出请求的元素自身。hx-target 让你指定任意其他元素作为更新目标。
标准 CSS 选择器
<!-- 通过 ID 定位(最常用) -->
<button hx-get="/api/users" hx-target="#user-list">加载用户</button>
<div id="user-list"></div>
<!-- 通过 class 定位 -->
<button hx-get="/api/stats" hx-target=".dashboard-stats">刷新统计</button>
<!-- CSS 选择器完整支持 -->
<button hx-get="/api/data" hx-target="main > .content">更新内容</button>
<!-- 指向自身 -->
<div hx-get="/api/widget" hx-target="this" hx-trigger="load">
加载中...
</div>
htmx 特殊选择器
除了标准 CSS 选择器,htmx 提供了一组相对定位选择器,特别适合列表、表格、树形结构等重复元素中的操作:
this
指向触发请求的元素本身。等效于省略
hx-target 时的默认行为,但明确写出更清晰。closest selector
从触发元素向上遍历 DOM 树,找到第一个匹配 CSS 选择器的祖先元素。等同于 JS 中的
element.closest(selector)。find selector
在触发元素的后代中,找到第一个匹配 CSS 选择器的元素。等同于 JS 中的
element.querySelector(selector)。next
触发元素的下一个兄弟元素。也可以写成
next selector,在后续兄弟元素中找第一个匹配的。previous
触发元素的上一个兄弟元素。也可以写成
previous selector。document
指向整个文档。配合特定 hx-swap 策略可以更新
<title> 等文档级元素。window
指向 window 对象,常配合事件触发使用。
<!-- closest:在列表项中操作,定位到父 <li> -->
<ul>
<li class="todo-item">
<span>学习 htmx</span>
<button
hx-delete="/api/todos/1"
hx-target="closest li"
hx-swap="outerHTML"
>删除</button>
</li>
<li class="todo-item">
<span>部署应用</span>
<button
hx-delete="/api/todos/2"
hx-target="closest li"
hx-swap="outerHTML"
>删除</button>
</li>
</ul>
<!-- find:触发元素内部的特定子元素 -->
<div class="card">
<button hx-get="/api/details/1" hx-target="find .details">展开</button>
<div class="details"></div>
</div>
<!-- next:更新紧随其后的元素 -->
<input
name="username"
hx-get="/api/check-username"
hx-trigger="blur"
hx-target="next .hint"
/>
<span class="hint">输入用户名以检查可用性</span>
hx-swap:控制内容替换策略
hx-swap 定义如何将服务器响应的 HTML 插入到目标元素中。htmx 提供了 8 种策略:
innerHTML(默认)
替换目标元素的内部内容(子节点),保留目标元素本身。最常用的策略。
outerHTML
替换目标元素本身(含元素标签)。用于完全替换一个组件,如删除列表项后服务器返回空字符串。
beforebegin
在目标元素的开始标签之前插入内容(成为目标元素的前一个兄弟)。
afterbegin
在目标元素内容的最前面插入(成为第一个子节点)。用于在列表顶部插入新项。
beforeend
在目标元素内容的最后面插入(成为最后一个子节点)。用于在列表底部追加新项(无限滚动)。
afterend
在目标元素的结束标签之后插入内容(成为目标元素的后一个兄弟)。
delete
删除目标元素,忽略服务器响应内容。用于"成功删除"场景,服务器返回 200 空响应,元素从 DOM 中移除。
none
不更新 DOM(但仍然执行其他副作用,如
HX-Trigger 响应头触发的事件)。适用于只需触发服务端动作但不更新页面的场景。DOM 插入位置示意:
<div id="target"> ← beforebegin 插在这里
<div id="target">
← afterbegin(第一个子节点前)
[现有内容]
← beforeend(最后一个子节点后)
</div>
</div> ← afterend 插在这里
<!-- 场景1:新评论插入到列表顶部 -->
<form
hx-post="/api/comments"
hx-target="#comments"
hx-swap="afterbegin"
>
<textarea name="content"></textarea>
<button>发布评论</button>
</form>
<div id="comments">
<!-- 新评论将插在这里最前面 -->
</div>
<!-- 场景2:无限滚动追加内容 -->
<div
hx-get="/api/posts?page=2"
hx-trigger="revealed"
hx-target="#post-list"
hx-swap="beforeend"
>
加载更多...
</div>
<!-- 场景3:删除并移除元素 -->
<button
hx-delete="/api/todos/7"
hx-target="closest .todo"
hx-swap="outerHTML"
hx-confirm="确认删除?"
>删除</button>
<!-- 场景4:只触发服务端动作,不更新 DOM -->
<button
hx-post="/api/mark-all-read"
hx-swap="none"
>全部标为已读</button>
hx-swap 修饰符
hx-swap 的值可以追加修饰符,用空格分隔,精细控制动画时序和滚动行为:
<!-- swap 之前等待 200ms(让旧内容的淡出动画完成) -->
<div
hx-get="/api/data"
hx-swap="innerHTML swap:200ms"
></div>
<!-- settle(新内容添加到 DOM 后等 100ms 再移除 htmx-settling 类) -->
<div
hx-get="/api/data"
hx-swap="innerHTML settle:100ms"
></div>
<!-- 滚动:更新后将目标元素滚动到视口顶部 -->
<button
hx-get="/api/page/2"
hx-target="#content"
hx-swap="innerHTML scroll:#content:top"
>下一页</button>
<!-- 聚焦:更新后将新内容中的第一个输入框聚焦 -->
<button
hx-get="/api/edit-form"
hx-target="#modal"
hx-swap="innerHTML focus-scroll:true"
>编辑</button>
hx-select:只取响应的一部分
当服务器返回完整 HTML 页面(例如,后端模板统一渲染完整页面),但只需要其中某个片段时,hx-select 允许你从响应中提取特定元素:
<!-- 服务器返回完整 HTML 页面,但只取 #main-content 的内容 -->
<a
href="/products"
hx-get="/products"
hx-target="#page-content"
hx-select="#main-content"
hx-push-url="true"
>产品列表</a>
<!-- 服务器返回的完整 HTML:
<html>
<head>...</head>
<body>
<nav>...</nav>
<div id="main-content"> ← 只取这部分
<h1>产品列表</h1>
...
</div>
<footer>...</footer>
</body>
</html>
-->
hx-select 与 hx-boost 的配合
在整个页面都启用了 hx-boost 的情况下,htmx 默认取响应的 <body> 内容。配合 hx-select 可以进一步细化,只提取 body 中的主要内容区域,跳过导航栏等重复部分——实现类似 SPA 的局部刷新。
hx-select-oob:越界更新
这是 htmx 最强大的特性之一。Out-of-Band(OOB,越界)更新允许服务器在一次响应中同时更新多个不相关的 DOM 区域,无需多次请求。
工作原理
服务器在 HTML 响应中包含带有 hx-swap-oob="true" 属性的元素。htmx 会:
- 将主要响应内容插入
hx-target指定的区域(正常流程) - 扫描响应中所有带
hx-swap-oob属性的元素 - 根据这些 OOB 元素的
id找到页面中对应的元素并替换
<!-- 前端:添加 Todo -->
<form
hx-post="/api/todos"
hx-target="#todo-list"
hx-swap="beforeend"
>
<input name="title" placeholder="新待办事项"/>
<button type="submit">添加</button>
</form>
<ul id="todo-list">...</ul>
<!-- 计数器(需要同时更新) -->
<span id="todo-count">3 个待办</span>
# 服务器返回两部分内容
@app.post("/api/todos")
async def create_todo(title: str):
todo = await db.create_todo(title)
count = await db.count_todos()
# 主要内容:新的 todo 项(插入列表末尾)
# OOB 内容:更新计数器
html = f"""
<li class="todo-item">
{todo.title}
<button hx-delete="/api/todos/{todo.id}"
hx-target="closest li"
hx-swap="outerHTML">删除</button>
</li>
<span id="todo-count" hx-swap-oob="true">{count} 个待办</span>
"""
return HTMLResponse(html)
hx-swap-oob 的值
hx-swap-oob="true" 默认使用 outerHTML 策略替换整个目标元素。也可以指定策略:hx-swap-oob="innerHTML"、hx-swap-oob="beforeend:#some-id"(向指定 id 的元素追加内容)。OOB 元素必须有 id 属性以便 htmx 找到页面中的对应元素。
越界更新的典型场景
# 购物车:添加商品时同时更新购物车数量和总价
@app.post("/api/cart/add")
async def add_to_cart(product_id: int):
cart = await add_item(product_id)
return HTMLResponse(f"""
<!-- 主要内容:成功提示(替换按钮) -->
<button class="btn-success" disabled>已加入购物车 ✓</button>
<!-- OOB:更新导航栏的购物车数量 -->
<span id="cart-count" hx-swap-oob="true">{cart.item_count}</span>
<!-- OOB:更新购物车侧边栏总价 -->
<span id="cart-total" hx-swap-oob="true">¥{cart.total:.2f}</span>
""")