Skip to content

HTMX 深度解析:超媒体 API 的现代 Web 回归 ​

当全栈框架用 React/Vue 重构一切的时候,HTMX 告诉你:其实 HTML 才是最好的 UI 描述语言。HTMX 用"超媒体 API"的理念,让服务器直接返回 HTML 片段,而不用写一行 JavaScript。Stripe、LinkedIn、Cloudflare 都在用它。2024 年它成为最受关注的 Web 技术之一。本文从理念到实战,带你搞懂 HTMX 为什么让这么多人"真香"。

本文由小虾子 撰写

为什么 HTMX 让人"真香"? ​

传统 Web 开发的困境 ​

现代 Web 开发:
前端:React/Vue + Redux/Zustand + React Query + 10个npm包 → 100KB+ JS
后端:REST API + JWT + 鉴权层 → 复杂
状态:前端管数据、后端管业务 → 数据不一致
⏱  开发周期:数周起步
HTMX 的思路:
服务器返回 HTML 片段 → 直接替换页面局部
浏览器零 JavaScript → 最终产物 HTML
前端、后端合一 → 一个后端搞定全栈
⏱  开发周期:几天

HTMX 不是"不要 JavaScript",而是"用最少的 JavaScript 做最多的事情"。


HTMX 核心理念:超媒体 API ​

超媒体(HATEOAS) ​

REST 架构之父 Roy Fielding 的理念:客户端通过服务器返回的链接来发现 API 能做什么,而不需要提前约定。

传统 API(需要提前知道接口):
GET /api/users/123
→ { id: 1, name: "小虾子", email: "x@example.com" }
客户端必须知道字段名才能用

HTMX 超媒体:
GET /users/123
→ <div>小虾子 <a href="/users/123/edit">编辑</a></div>
链接告诉客户端下一步能做什么

HTMX 把这个理念带到浏览器:页面上的每个元素都包含下一步操作的链接,浏览器只需要跟随链接,不需要提前知道任何 API。


快速上手 ​

安装(CDN 一行引入) ​

html
<!-- 最简方式 -->
<script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js"></script>

<!-- 或 npm -->
npm install htmx.org

最简示例 ​

html
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js"></script>
</head>
<body>
  <!-- 点击按钮 → 发起 GET /hello → 把返回的 HTML 插入到 #content -->
  <button hx-get="/hello" hx-target="#content">
    点我加载内容
  </button>

  <div id="content"></div>
</body>
</html>
python
# Python Flask 后端
from flask import Flask, render_template_string
app = Flask(__name__)

@app.get("/hello")
def hello():
    return "<span>Hello from HTMX! </span>"

@app.get("/")
def index():
    return render_template_string("""
    <button hx-get="/hello" hx-target="#content">
        点我加载内容
    </button>
    <div id="content"></div>
    """)

核心属性 ​

1. hx-get / hx-post / hx-put / hx-delete —— HTTP 方法 ​

html
<!-- GET 请求(默认) -->
<button hx-get="/api/users">加载用户</button>

<!-- POST 表单提交 -->
<form hx-post="/api/users">
  <input name="name" />
  <button type="submit">提交</button>
</form>

<!-- 删除操作 -->
<button hx-delete="/api/users/123">删除</button>

2. hx-target —— 操作目标元素 ​

html
<!-- 把返回内容放到指定元素 -->
<button hx-get="/sidebar" hx-target="#sidebar">
  加载侧边栏
</button>

<div id="sidebar"></div>

<!-- hx-target 可用选择器 -->
<button hx-get="/panel" hx-target="this">替换自己</button>
<button hx-get="/panel" hx-target="closest .card">最近的父级 card</button>
<button hx-get="/panel" hx-target="next .modal">下一个 modal 兄弟元素</button>

3. hx-swap —— 插入方式 ​

html
<!-- innerHTML(默认):替换内部内容 -->
<div hx-get="/data" hx-target="this" hx-swap="innerHTML">

<!-- outerHTML:替换整个元素 -->
<button hx-get="/button" hx-target="this" hx-swap="outerHTML">

<!-- afterbegin:插入到元素内部开头 -->
<ul hx-get="/items" hx-target="this" hx-swap="afterbegin">

<!-- beforeend:插入到元素内部末尾 -->
<ul hx-get="/items" hx-target="this" hx-swap="beforeend">

<!-- after:插入到元素之后 -->
<div hx-get="/more" hx-target="this" hx-swap="after">

<!-- before:插入到元素之前 -->
<div hx-get="/more" hx-target="this" hx-swap="before">

<!-- delete:删除目标元素 -->
<button hx-delete="/item/1" hx-target="closest li" hx-swap="delete">

4. hx-trigger —— 触发事件 ​

html
<!-- 点击触发(默认) -->
<button hx-get="/data">点我</button>

<!-- 鼠标悬停触发 -->
<button hx-get="/preview" hx-trigger="mouseenter">
  悬停查看预览
</button>

<!-- 表单提交后触发 -->
<form hx-post="/submit" hx-trigger="submit">
  <input name="email" />
  <button type="submit">提交</button>
</form>

<!-- 页面加载时触发 -->
<div hx-get="/widgets" hx-trigger="load">
  加载中...
</div>

<!-- 定时刷新 -->
<div hx-get="/stock-price" hx-trigger="every 3s">
  加载中...
</div>

<!-- 元素变化时触发(Intersection Observer) -->
<div hx-get="/analytics" hx-trigger="revealed">
  显示时加载
</div>

<!-- 组合触发 -->
<div hx-get="/search" hx-trigger="change delay:500ms">
  <!-- 变化后延迟 500ms 触发 -->
</div>

表单与验证 ​

表单提交 + 服务器端验证 ​

html
<!-- 表单 -->
<form hx-post="/contact" hx-target="#form-container" hx-swap="outerHTML">
  <div>
    <label>邮箱</label>
    <input name="email" type="email" required />
  </div>
  <div>
    <label>消息</label>
    <textarea name="message"></textarea>
  </div>
  <button type="submit">发送</button>
</form>

<div id="form-container">
  <!-- 提交后替换整个表单 -->
</div>
python
# Flask 后端
from flask import Flask, render_template_string, request, redirect
app = Flask(__name__)

contact_template = """
<form hx-post="/contact" hx-target="closest form" hx-swap="outerHTML">
  <div>
    <label>邮箱</label>
    <input name="email" type="email" required />
  </div>
  <div>
    <label>消息</label>
    <textarea name="message"></textarea>
  </div>
  <button type="submit">发送</button>
</form>
"""

success_template = """
<div class="success">
  <h3>正确 发送成功!</h3>
  <p>感谢您的留言</p>
</div>
"""

@app.post("/contact")
@app.post("/contact")
def contact():
    email = request.form.get("email")
    message = request.form.get("message")

    # 服务器端验证
    errors = {}
    if not email or "@" not in email:
        errors["email"] = "请输入有效邮箱"
    if not message or len(message) < 10:
        errors["message"] = "消息至少10个字符"

    if errors:
        # 返回带错误提示的表单
        return render_template_string(contact_template, errors=errors)

    # 处理成功
    return success_template

实时搜索 ​

html
<!-- 搜索输入 -->
<div>
  <input
    type="text"
    name="q"
    placeholder="搜索..."
    hx-get="/search"
    hx-trigger="change"
    hx-target="#results"
    hx-indicator=".spinner"
  />
  <span class="htmx-indicator spinner"> 搜索中...</span>
</div>

<div id="results"></div>
python
# 搜索后端
@app.get("/search")
def search():
    q = request.args.get("q", "")
    if not q:
        return "<p>输入关键词开始搜索</p>"

    results = db.search(q)  # 你的搜索逻辑
    return render_template_string("""
    <ul>
    {% for item in results %}
      <li>{{ item.title }}</li>
    {% endfor %}
    </ul>
    """, results=results)

级联操作(boost) ​

全站 HTMX(无需写链接) ​

html
<html>
<head>
  <script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js"></script>
  <!-- 启用 boost:把普通 <a> 和 <form> 自动变成 HTMX -->
  <script>
    htmx.config.defaultSwapStyle = 'innerHTML';
  </script>
</head>
<body hx-boost="true">
  <nav>
    <a href="/">首页</a>
    <a href="/about">关于</a>
    <a href="/blog">博客</a>
  </nav>

  <!-- 这两个链接会被自动 HTMX 化:
       点击 → GET / → 返回 <main> 部分 → 替换 #main-content
       不需要写任何 hx-get! -->
  <main id="main-content">
    {{ content }}
  </main>
</body>
</html>
python
# Flask:返回带导航的完整页面,或只返回 main 部分
from functools import wraps
import htmlex

@app.get("/")
def index():
    content = render_template("index_content.html")
    return render_template("layout.html", content=content)

@app.get("/_fragment/about")
def about_fragment():
    # HTMX 请求返回片段(不带 layout)
    content = render_template("about_content.html")
    return content

# 中间件检测 HTMX 请求头
def htmx_request():
    return "HX-Request" in request.headers

def is_htmx():
    return request.headers.get("HX-Request") == "true"

进阶:自定义事件 ​

HTMX 事件体系 ​

html
<!-- HTMX 提供丰富的事件 -->
<div
  hx-get="/data"
  hx-trigger="every 5s"
  hx-swap="innerHTML"

  hx-on::before-request="showLoading()"
  hx-on::after-request="hideLoading()"
  hx-on::after-settle="initializeWidgets()"
>
  数据加载中...
</div>

常用事件 ​

javascript
// 请求开始
document.body.addEventListener('htmx:beforeRequest', (e) => {
  console.log('请求开始:', e.detail);
  showLoadingIndicator();
});

// 请求完成(无论成功失败)
document.body.addEventListener('htmx:afterRequest', (e) => {
  console.log('请求完成:', e.detail);
  hideLoadingIndicator();
});

// 请求成功
document.body.addEventListener('htmx:afterSettle', (e) => {
  console.log('DOM 更新完成');
  initializeNewContent(); // 初始化新加载的内容
});

// 请求失败
document.body.addEventListener('htmx:send-error', (e) => {
  console.error('网络错误:', e.detail);
  showError('网络请求失败,请重试');
});

// 超时
document.body.addEventListener('htmx:timeout', (e) => {
  showError('请求超时');
});

// 请求发送前(可取消)
document.body.addEventListener('htmx:before-ajax-request', (e) => {
  if (!confirm('确定要提交吗?')) {
    e.preventDefault();
  }
});

实战:ToDo 列表 ​

前端(HTML + HTMX) ​

html
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js"></script>
  <style>
    .todo-item { padding: 8px; border-bottom: 1px solid #eee; }
    .todo-item.completed { text-decoration: line-through; color: gray; }
    .done-btn { color: red; background: none; border: none; cursor: pointer; }
  </style>
</head>
<body>
  <h1> HTMX ToDo 列表</h1>

  <!-- 添加新任务 -->
  <form hx-post="/todos" hx-target="#todo-list" hx-swap="afterbegin">
    <input name="text" placeholder="新任务..." required />
    <button type="submit">添加</button>
  </form>

  <!-- 任务列表(服务器返回已完成的列表 HTML) -->
  <div id="todo-list">
    {% include 'todo_list.html' %}
  </div>
</body>
</html>

后端(Flask) ​

python
from flask import Flask, render_template, request, redirect
from flask import render_template_string

app = Flask(__name__)

todos = [
    {"id": 1, "text": "学习 HTMX", "done": False},
    {"id": 2, "text": "写博客文章", "done": True},
]

# 单条待办项模板
todo_item_template = """
<div class="todo-item {{ 'completed' if todo.done }}">
  <span>{{ todo.text }}</span>
  <button
    hx-delete="/todos/{{ todo.id }}"
    hx-target="closest div"
    hx-swap="outerHTML"
    class="done-btn">
    {{ '↩ 撤销' if todo.done else '是 完成' }}
  </button>
</div>
"""

@app.get("/")
def index():
    return render_template("index.html", todos=todos)

@app.post("/todos")
def create_todo():
    text = request.form.get("text")
    todo = {
        "id": max([t["id"] for t in todos], default=0) + 1,
        "text": text,
        "done": False,
    }
    todos.insert(0, todo)
    # 返回单个待办项的 HTML 片段
    return render_template_string(todo_item_template, todo=todo)

@app.delete("/todos/<int:todo_id>")
def delete_todo(todo_id):
    global todos
    todos = [t for t in todos if t["id"] != todo_id]
    return ""  # HTMX 会删除目标元素

# 或切换完成状态
@app.put("/todos/<int:todo_id>/toggle")
def toggle_todo(todo_id):
    global todos
    for t in todos:
        if t["id"] == todo_id:
            t["done"] = not t["done"]
    todo = next(t for t in todos if t["id"] == todo_id)
    return render_template_string(todo_item_template, todo=todo)

HTMX + 主流后端框架 ​

Python FastAPI / Flask ​

python
# FastAPI
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get("/", response_class=HTMLResponse)
async def home():
    return open("templates/index.html").read()

@app.post("/todos", response_class=HTMLResponse)
async def create_todo(request: Request):
    data = await request.form()
    todo_html = f'<li>{data["text"]}</li>'
    return todo_html

Node.js Express / Hono ​

javascript
// Hono
import { Hono } from 'hono';
import { html } from 'hono/html';

const app = new Hono();

app.get('/', (c) => c.html(<html>
  <head><script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js" /></head>
  <body>
    <button hx-get="/api/hello" hx-target="#result">
      点我
    </button>
    <div id="result"></div>
  </body>
</html>));

app.get('/api/hello', (c) => c.html('<span>Hello HTMX! </span>'));

Go net/http ​

go
package main

import (
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("HX-Request") != "" {
            // HTMX 请求,返回片段
            w.Write([]byte(`<span>Hello from Go!</span>`))
            return
        }
        // 普通请求,返回完整页面
        http.ServeFile(w, r, "templates/index.html")
    })

    http.ListenAndServe(":8080", nil)
}

HTMX vs 传统前端框架 ​

维度HTMXReact/VueAlpine.js
JS Bundle1.9KB40-150KB15KB
学习曲线⭐ 极低⭐⭐⭐⭐⭐⭐⭐
后端复杂度低(返回 HTML)高(REST API + JWT)低
状态管理服务器端客户端客户端
SEO正确 天然友好注意 需要 SSR注意 需要 SSR
复杂交互错误 不适合正确 适合⭐⭐⭐
实时更新注意 需 WebSocket正确 WebSocket/SSE注意 需扩展
移动端注意正确⭐⭐⭐

HTMX 适用场景 ​

正确 强项:
─────────────────
- 后台管理系统(CRUD 为主)
- 博客、文档站点
- 表单驱动的应用
- 渐进增强(HTML 优先)
- 快速 MVP(几天完成全栈)
- 不想写 JavaScript 的后端开发者

错误 弱项:
─────────────────
- 复杂客户端交互(地图编辑器、设计工具)
- 实时多人协作
- 离线优先 App
- 大量状态在前端计算
- 移动端原生体验

常见问题 ​

Q: 表单验证怎么做? ​

python
# 服务器端验证,返回带错误信息的表单
@app.post("/submit")
def submit():
    if errors:
        return render_template("form.html", errors=errors)
    return "成功"

Q: 如何处理加载状态? ​

html
<!-- hx-indicator 显示/隐藏加载指示器 -->
<button hx-get="/data" hx-indicator="#spinner">
  加载
</button>
<img id="spinner" class="htmx-indicator" src="/spinner.gif" />

Q: 如何取消请求? ​

javascript
const btn = document.querySelector('#my-button');
htmx.on('htmx:beforeRequest', (e) => {
  if (shouldCancel()) e.preventDefault();
});

Q: 如何调试? ​

javascript
// 开启 HTMX 调试模式
htmx.logAll();

总结 ​

HTMX 的核心价值:把复杂的前端框架换成 HTML 的简单回归。

HTMX 核心理念:
─────────────────
超媒体 API:服务器告诉浏览器能做什么(通过链接)
零 JavaScript:不需要写 JS 交互逻辑
渐进增强:HTML 优先,逐步增强
HTML 片段:服务器返回 HTML,不是 JSON

适合的场景:
─────────────────
后台系统、表单应用、博客、内容站点
不想写 JavaScript 的后端开发者
快速 MVP、渐进增强项目

不适合的场景:
─────────────────
复杂交互应用、设计工具、实时协作
移动端原生体验、高度状态化的 UI

HTMX 不是"反 JavaScript",而是"按需 JavaScript" —— 如果 HTML 能解决,就用 HTML。如果真的需要复杂交互,再引入 JavaScript 库。

HTML 是 Web 的根基,HTMX 让 HTML 重新成为 UI 的主角

本文由小虾子 撰写

Last updated: