Chapter 02

核心属性:hx-get/post/put/delete/patch

掌握 htmx 的五个核心 HTTP 方法属性,理解请求头机制、hx-boost 全局增强与浏览器历史管理

五个核心请求属性

htmx 通过五个属性让任意 HTML 元素发出对应的 HTTP 请求。这五个属性的值都是目标 URL,触发时机由元素默认事件决定(可通过 hx-trigger 覆盖):

hx-get
发出 HTTP GET 请求。默认触发事件:按钮/链接为 click,输入框为 change,表单为 submit。GET 请求不携带请求体,参数通过 URL 查询字符串传递。
hx-post
发出 HTTP POST 请求。表单元素的默认触发事件是 submit,会自动序列化表单数据。常用于创建新资源。
hx-put
发出 HTTP PUT 请求。REST 语义是完整替换资源。原生 HTML 不支持 PUT,htmx 使 PUT 成为一等公民。
hx-patch
发出 HTTP PATCH 请求。REST 语义是部分更新资源,比 PUT 更常用于"编辑"操作。
hx-delete
发出 HTTP DELETE 请求。用于删除资源。服务器可以返回空响应(204 No Content)或返回替换内容的 HTML 片段。

基础用法示例

<!-- 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 局部请求,进而返回不同的响应内容。

HX-Request
固定值为 "true",表明这是一个 htmx 发出的请求。这是最常用的判断条件。
HX-Trigger
触发请求的元素的 id(如果有)。便于在服务器端识别是哪个元素发出的请求。
HX-Target
目标元素的 idhx-target 指向的元素,如果有 id)。
HX-Current-URL
发出请求时浏览器当前的 URL。便于服务器了解用户所在页面的上下文。
HX-Trigger-Name
触发元素的 name 属性值。在表单中有多个提交按钮时,可用于区分点击了哪个按钮。
HX-Boosted
当请求由 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)
    }
}
同一 URL,两种响应

这是 htmx 最优雅的地方之一:同一个 URL 既可以作为完整页面访问(直接输入 URL、SEO 爬虫),也可以作为局部片段被 htmx 请求。通过检查 HX-Request 请求头,服务器智能地选择返回完整 HTML 还是 HTML 片段,完美支持渐进增强。

响应头:从服务器控制客户端行为

htmx 不仅发送特殊请求头,还可以识别服务器响应中的特殊响应头来触发客户端行为,无需客户端 JavaScript:

HX-Redirect
让浏览器跳转到指定 URL(完整页面跳转,等同于 window.location.href = url)。常用于登录成功后重定向。
HX-Push-Url
将指定 URL 推入浏览器历史记录(不触发页面跳转)。让局部更新也能更新浏览器地址栏。
HX-Refresh
值为 "true" 时强制浏览器整页刷新。
HX-Trigger
触发客户端事件,可以串联其他 htmx 请求或 JavaScript 处理逻辑。值为 JSON 对象,如 {"showToast": "保存成功"}
HX-Retarget
覆盖 hx-target,将响应内容插入到服务器指定的不同元素中。在错误场景下非常有用(错误消息展示在与正常内容不同的位置)。
HX-Reswap
覆盖 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 接管:

<!-- 一行代码让整个页面的导航变成 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>
hx-boost 的工作原理

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-push-url="true"
将请求 URL 推入历史记录。例如 hx-get="/posts/42" hx-push-url="true" 会把 /posts/42 推入历史。
hx-push-url="/custom-url"
将自定义 URL 推入历史记录(而非请求的实际 URL)。适用于 URL 美化场景。
hx-push-url="false"
明确禁止推入历史(覆盖父元素或 hx-boost 的行为)。
hx-replace-url
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 请求的参数传递

GET 请求中,表单字段会被序列化为 URL 查询字符串。例如 <input name="q" value="htmx"> 配合 hx-get="/search" 会发出请求 GET /search?q=htmx。这使得搜索 URL 可以被书签保存和分享。