HTMX 动态页面:通过 HTML 属性实现 Ajax

FreeGuideOnline 最新 2026-06-15

什么是 HTMX?

HTMX 是一个现代化的前端库,它赋予 HTML 标签全新的能力:直接通过属性发起 Ajax 请求、触发 CSS 过渡、处理服务器返回的 HTML 片段,并在页面上动态更新。你不需要写任何 JavaScript 就可以构建出高交互性的动态页面。它的核心理念是:用 HTML 属性来声明行为,用服务器返回的 HTML 来驱动状态更新

与传统前端框架的区别

在 React、Vue 或 Angular 的主流范式中,前端负责获取 JSON 数据,再通过 JavaScript 渲染 DOM。HTMX 反其道而行,让服务器直接返回 HTML 片段,然后由 HTMX 将这些片段替换或插入到页面已有元素的相应位置。这样做的好处是:

  • 后端可以继续使用你熟悉的模板引擎(Jinja、Razor、ERB、Django Templates 等)
  • 大量客户端逻辑被消除了,状态天然保存在服务端
  • 渐进式增强:既可以在老式多页应用中嵌入一小块动态区域,也能构建完整的单页应用

快速上手

1. 引入 HTMX

只需在 <head> 中添加一行 <script> 标签,无需构建步骤。

<script src="https://unpkg.com/htmx.org@1.9.10"></script>

现在所有 HTMX 属性都可以在你的 HTML 中生效了。

2. 你的第一个动态按钮

假设我们在服务器上有一个端点 /clicked,它返回一段 HTML 文本:“你点击了按钮!”。使用 HTMX,只需在按钮上添加 hx-posthx-swap 属性。

<button hx-post="/clicked" hx-swap="outerHTML">
  点我
</button>
  • hx-post="/clicked" 指示 HTMX:当按钮被点击时,向 /clicked 发送 POST 请求。
  • hx-swap="outerHTML" 用服务器返回的 HTML 整体替换当前按钮。

点击按钮后,HTMX 发送请求,服务器返回类似 <span>你点击了按钮!</span> 的 HTML,按钮就被替换成了这段文字。整个过程零 JavaScript 代码。

HTMX 核心属性详解

HTMX 提供了一套简洁的属性来完成常见交互。以下是最重要的几个属性及其作用。

发起请求的触发器:hx-gethx-posthx-puthx-patchhx-delete

这些属性分别对应 HTTP 的 GET、POST、PUT、PATCH 和 DELETE 方法。它们可以放在任何 HTML 元素上(不仅仅是表单或链接),并通过特定事件触发请求。

属性 HTTP 方法 默认触发事件
hx-get GET click
hx-post POST click
hx-put PUT click
hx-patch PATCH click
hx-delete DELETE click

你还可以使用 hx-trigger 自定义触发事件,例如鼠标悬停、表单提交、自定义事件等。

<div hx-get="/more" hx-trigger="mouseenter">
  悬停加载更多
</div>

更新策略:hx-swap

hx-swap 控制如何将服务器返回的 HTML 片段插入到 DOM 中。它的值决定了哪里放置新内容。

常用选项:

  • innerHTML(默认):替换目标元素的内部 HTML。
  • outerHTML:完全替换目标元素自身。
  • beforebegin:在目标元素之前插入。
  • afterbegin:在目标元素内部最前面插入。
  • beforeend:在目标元素内部最后面插入。
  • afterend:在目标元素之后插入。
  • none:不替换任何内容,仅进行请求(可用于副作用操作)。
<!-- 将新评论添加到评论列表的末尾 -->
<ul id="comments" hx-get="/comments" hx-swap="afterend" hx-trigger="load">
</ul>

指定替换目标:hx-target

默认情况下,HTMX 会将请求发起元素自身作为替换目标。使用 hx-target 可以指定另一个元素接收服务器返回的内容。值可以是 CSS 选择器。

<button hx-post="/add-item" hx-target="#item-list" hx-swap="beforeend">
  添加条目
</button>
<ul id="item-list"></ul>

这里点击按钮时,/add-item 返回的 HTML 会追加到 <ul> 的末尾。

自定义触发行为:hx-trigger

hx-trigger 允许你精确控制何时发送请求。除了标准 DOM 事件,它还支持修饰符。

常用修饰符:

  • once:只触发一次。
  • delay:<milliseconds>:防抖,在连续事件中只有最后一次在延迟后触发。
  • throttle:<milliseconds>:节流,在时间窗口内最多触发一次。
  • from:<CSS selector>:监听其他元素上的事件。
<!-- 输入框防抖查询,输入停止后 500ms 才发出请求 -->
<input type="text" name="q" 
       hx-get="/search" 
       hx-trigger="keyup changed delay:500ms"
       hx-target="#search-results">
<div id="search-results"></div>

传递参数

表单内容自动序列化

如果触发器是一个 <form>(或包含在表单内的元素),HTMX 会自动收集其内部所有命名输入项的值作为请求参数。

<form hx-post="/login">
  <input type="text" name="username">
  <input type="password" name="password">
  <button type="submit">登录</button>
</form>

包含额外元素的值:hx-include

如果你想包含非表单元素或来自其他表单的字段,可以使用 hx-include,值同样是 CSS 选择器。

<button hx-post="/save" hx-include="[name='extra']">保存</button>
<input type="hidden" name="extra" value="some_value">

添加固定参数:hx-vals

hx-vals 接受一个 JSON 字符串,用于添加不会被用户修改的额外参数。

<div hx-get="/items" hx-vals='{"category": "books", "page": "1"}'></div>

请求指示器与加载状态

HTMX 默认在请求期间会给触发元素添加 htmx-request 类,你可以用 CSS 显示加载动画。更通用的方法是使用 hx-indicator 属性指向一个专门显示加载状态的元素。

<button hx-post="/slow-action" hx-indicator="#spinner">提交</button>
<div id="spinner" class="htmx-indicator" style="display:none;">加载中…</div>

htmx-indicator 会在请求进行时自动将 opacity 变为 1,并添加 transition 效果。默认隐藏,可以基于 htmx-request 类样式来实现动画。

深入示例:实现动态搜索页面

让我们结合上述属性构建一个完整的搜索界面。

后端片段(伪代码,Flask 风格)

假设我们的服务器 /search 端点接受查询参数 q,并返回一段 HTML 组成的搜索结果列表。

@app.get("/search")
def search():
    q = request.args.get("q", "")
    results = find_items(q)
    # 返回纯 HTML 片段(无完整文档结构)
    return render_template("_results.html", results=results)

_results.html 模板示例(Jinja2):

{% for item in results %}
  <li>{{ item.title }}</li>
{% else %}
  <li>无匹配结果</li>
{% endfor %}

前端页面

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>HTMX 搜索示例</title>
  <script src="https://unpkg.com/htmx.org@1.9.10"></script>
  <style>
    /* 简单的加载指示器样式 */
    .htmx-indicator { opacity: 0; transition: opacity 200ms ease-in; }
    .htmx-request .htmx-indicator { opacity: 1; }
    .htmx-request.htmx-indicator { opacity: 1; }
  </style>
</head>
<body>
  <h2>搜索文章</h2>
  
  <!-- 搜索输入框 -->
  <input type="text" name="q" 
         placeholder="输入关键词..." 
         hx-get="/search" 
         hx-trigger="keyup changed delay:400ms"
         hx-target="#results"
         hx-indicator="#loading">
  
  <!-- 加载指示器 -->
  <span id="loading" class="htmx-indicator">正在搜索...</span>
  
  <!-- 搜索结果容器 -->
  <ul id="results">
    <li>请输入关键词开始搜索</li>
  </ul>
</body>
</html>

当用户在输入框中输入文字(400 毫秒延迟后),HTMX 发起 GET 请求到 /search?q=...,服务器返回 <li> 列表的 HTML,这段 HTML 会替换掉 #resultsinnerHTML。整个过程中不需要自己写 AJAX 逻辑。

高级用法:Out‑of‑Band 更新与 History API

同时更新多个区域:hx-swap-oob

有时一个请求可能需要更新页面上多个不同位置的内容。例如,提交表单后,既要清空表单,又要更新侧边栏计数。HTMX 的 hx-swap-oob(out‑of‑band)属性可以实现这一点。

服务器返回一个 HTML 片段,其中包含带 hx-swap-oob 属性的元素,HTMX 会识别它们并将其交换到页面上匹配 id 的元素处,而不是全部插入到 hx-target 中。

服务器响应示例:

<!-- 表单区域的新内容 -->
<div id="form-area">
  <p>提交成功!</p>
</div>

<!-- 侧边栏计数器更新,不会影响本次请求的主目标 -->
<span id="cart-count" hx-swap-oob="true">3</span>

前端触发元素无需特殊修改,HTMX 自动处理 OOB 交换。

支持浏览器后退/前进:hx-push-url

默认情况下,HTMX 的动态内容替换不会改变浏览器地址栏,这可能导致刷新或分享链接时状态丢失。通过 hx-push-url,你可以告诉 HTMX 将新的 URL 推入浏览器历史记录。

<a hx-get="/blog?page=2" hx-push-url="true" hx-target="#content">
  下一页
</a>

点击后,URL 变为 /blog?page=2,用户可以通过浏览器后退按钮回到上一页内容。配合服务器对完整页面或部分片段的条件返回,你可以轻松实现深度链接。

使用 HTMX 的注意事项

服务器返回完整页面 vs 片段

大部分时候,HTMX 端点应只返回 HTML 片段(不需要 <html><head> 等)。但如果你想支持浏览器后退按钮和无 JavaScript 场景(渐进增强),可以让同一个端点根据请求头 HX-Request 来决定返回完整页面还是片段。HTMX 会在请求时自动设置该请求头,值为 true

后端检测示例(Flask):

if request.headers.get('HX-Request'):
    return render_template('_partial.html')
else:
    return render_template('full_page.html')

CSRF 保护

HTMX 会自动在每个请求中包含 X-Requested-With: XMLHttpRequest 头,大部分框架(如 Django、Laravel)会据此豁免 CSRF 检查,或仍需要额外的 Token。你可以使用 hx-headers 属性添加自定义头,例如:

<body hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'>
  ...
</body>

这样所有子元素的 HTMX 请求都会携带该 Token。

事件系统

HTMX 提供丰富的事件,允许你在请求生命周期的各个阶段插入 JavaScript。例如,在清除表单之前做点什么,或者在交换 HTML 之后初始化组件。

<script>
  document.body.addEventListener('htmx:afterSwap', function(evt) {
    // evt.detail.target 是交换的目标元素
    console.log('内容已更新');
  });
</script>

这对于集成第三方库(如图表、日期选择器)尤其有用。

完整示例:实时待办事项应用

最后,我们将综合前面所有概念实现一个简单的待办列表。

后端(省略具体框架细节):

  • POST /todos – 接收表单数据,创建待办,返回新待办的 HTML 列表项。
  • DELETE /todos/:id – 删除待办,返回空响应(状态码 200)。
  • 待办项模板 _todo.html
<li id="todo-{{ todo.id }}">
  {{ todo.text }}
  <button hx-delete="/todos/{{ todo.id }}" 
          hx-target="#todo-{{ todo.id }}"
          hx-swap="outerHTML">
    删除
  </button>
</li>

前端页面

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>HTMX 待办事项</title>
  <script src="https://unpkg.com/htmx.org@1.9.10"></script>
</head>
<body>
  <h1>我的待办</h1>
  
  <!-- 添加待办表单 -->
  <form hx-post="/todos" hx-target="#todo-list" hx-swap="beforeend" 
        hx-on::after-request="this.reset()">
    <input type="text" name="text" required>
    <button type="submit">添加</button>
  </form>
  
  <!-- 待办列表 -->
  <ul id="todo-list">
    <!-- 服务器端可以预渲染已有待办 -->
  </ul>
  
  <script>
    // 删除事件监听(可选)
    document.body.addEventListener('htmx:afterOnLoad', function(evt) {
      if (evt.detail.requestConfig.verb === 'delete') {
        console.log('删除成功');
      }
    });
  </script>
</body>
</html>
  • 添加待办时,表单 POST 到 /todos,服务器返回一个新 <li> 片段,通过 hx-swap="beforeend" 追加到 <ul> 末尾。hx-on::after-request="this.reset()" 在请求结束后重置表单。
  • 每个待办项上的删除按钮使用 hx-delete,并用 hx-target 指向自己所在的 <li>,删除成功后自身被移除。

总结

HTMX 将传统的超文本概念提升到了全新的高度,让你可以使用简单、声明式的 HTML 属性实现 Ajax、局部更新、实时交互等复杂特性。它鼓励你保持应用的状态在服务器端,从而大幅减少客户端 JavaScript 代码量,提升开发效率,并天然支持 SEO 和渐进增强。

现在,你可以尝试在现有项目中逐步采用 HTMX,从一小块动态区域开始,体验“零 JavaScript”构建交互式页面的乐趣。