教程·阅读约 7 分钟·
React 太重了?用 Python + HTMX 快速构建内部工具的实战教程

React 太重了?用 Python + HTMX 快速构建内部工具的实战教程

本文通过一个完整的实战项目,带你从零开始用 FastAPI + Jinja2 + HTMX 构建一个内部管理面板,无需构建工具、无需 API 层、无需前端状态管理

原文来源:DEV Community: React is Overkill – Why Python + HTMX is Dominating in 2026 — 一个内部仪表盘从 React SPA 迁移到 FastAPI + HTMX 的经验分享

为什么 2026 年还要谈一个 14KB 的前端库?

如果你已经厌倦了每次开新项目都要经历「安装依赖 → 再装依赖的依赖 → 配置工具链 → 看 npm 下载全球 GDP」的过程,HTMX 可能会让你重新感受 Web 开发的原始快乐。

HTMX(GitHub,40k+ stars)是一个只有 ~14KB 的 JavaScript 库,它的核心理念可以用一句话说清楚:给 HTML 元素添加发起 HTTP 请求的能力,然后用服务器返回的 HTML 片段直接更新页面。

没有 Virtual DOM,没有组件生命周期,没有客户端路由,没有状态管理库需要争论。你只需要在 input 上写 hx-get="/search"hx-trigger="keyup changed delay:300ms",搜索结果 div 就会自动更新。

这套理念在 2026 年重新获得了大量关注。不是因为它新,而是因为很多人开始意识到:SPA 时代解决了一些实际问题,但也带来了大量的"附带复杂性"(accidental complexity)——尤其对于那些功能需求并不复杂的内部工具和 CRUD 应用。

本教程将带你从头构建一个完整的内部管理面板,涵盖搜索、分页、编辑、删除等常见功能,全程不需要写一行前端 JavaScript。

—— 广告 ——

第一步:项目初始化

首先,确保你安装了 Python 3.10+ 和 pip:

code
mkdir htmx-admin-demo
cd htmx-admin-demo
python3 -m venv venv
source venv/bin/activate  # Windows 用 venv\Scripts\activate

安装依赖。我们只需要四个包:

code
pip install fastapi uvicorn jinja2 python-multipart httpx
  • FastAPI:高性能 Python Web 框架,支持异步,自动 OpenAPI 文档
  • Uvicorn:ASGI 服务器,运行 FastAPI 用
  • Jinja2:Python 最流行的模板引擎,和 Flask/Django 用的同一个
  • python-multipart:处理表单提交需要
  • httpx:用来在服务端请求外部 API(非必须,但后面会用到)

然后创建一个简单目录结构:

code
mkdir templates static

第二步:FastAPI 应用骨架

创建 main.py

code
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
 
app = FastAPI()
 
# 挂载静态文件
app.mount("/static", StaticFiles(directory="static"), name="static")
 
# 配置模板
templates = Jinja2Templates(directory="templates")
 
 
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
    return templates.TemplateResponse(
        "index.html", {"request": request, "title": "HTMX Admin Demo"}
    )

创建 templates/base.html 作为基础模板,所有页面共用 layout:

code
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }}</title>
    <script src="https://unpkg.com/htmx.org@2.0.4"></script>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <nav class="sidebar">
        <h2>管理面板</h2>
        <ul>
            <li><a href="/">仪表盘</a></li>
            <li><a href="/users">用户管理</a></li>
            <li><a href="/orders">订单管理</a></li>
        </ul>
    </nav>
    <main id="content">
        {% block content %}{% endblock %}
    </main>
</body>
</html>

创建 templates/index.html

code
{% extends "base.html" %}
{% block content %}
<div class="dashboard">
    <h1>仪表盘</h1>
    <div class="stats-grid" hx-get="/api/stats" hx-trigger="load" hx-swap="innerHTML">
        <div class="stat-card">加载中...</div>
    </div>
</div>
{% endblock %}

注意到这里了吗?hx-get="/api/stats" hx-trigger="load"——页面的统计数据是通过 HTMX 在页面加载时异步请求获取的,不需要你写任何 fetch()axios 调用。

第三步:模拟数据层

作为示例,我们使用一个简单的内存数据存储。实际项目中你可以换成 SQLite、PostgreSQL 或其他数据库。

创建 data.py

code
from dataclasses import dataclass, field
from typing import List, Optional
import random
 
@dataclass
class User:
    id: int
    name: str
    email: str
    role: str
    status: str
 
@dataclass
class Order:
    id: int
    user_id: int
    product: str
    amount: float
    status: str
 
# 模拟数据
users_db: List[User] = [
    User(1, "张三", "zhangsan@example.com", "管理员", "活跃"),
    User(2, "李四", "lisi@example.com", "编辑", "活跃"),
    User(3, "王五", "wangwu@example.com", "用户", "非活跃"),
    User(4, "赵六", "zhaoliu@example.com", "编辑", "活跃"),
    User(5, "钱七", "qianqi@example.com", "用户", "活跃"),
]
 
orders_db: List[Order] = [
    Order(1, 1, "企业版许可证", 999.00, "已完成"),
    Order(2, 2, "基础版许可证", 299.00, "处理中"),
    Order(3, 3, "高级支持服务", 599.00, "已完成"),
    Order(4, 1, "额外存储空间", 199.00, "已取消"),
]
 
def get_stats():
    """返回仪表盘统计数据"""
    return {
        "total_users": len(users_db),
        "active_users": sum(1 for u in users_db if u.status == "活跃"),
        "total_orders": len(orders_db),
        "total_revenue": sum(o.amount for o in orders_db if o.status != "已取消"),
    }
 
def search_users(query: str) -> List[User]:
    """搜索用户(模糊匹配)"""
    if not query:
        return users_db
    q = query.lower()
    return [u for u in users_db
            if q in u.name.lower() or q in u.email.lower()]
 
def update_user_status(user_id: int, status: str) -> Optional[User]:
    """更新用户状态"""
    for u in users_db:
        if u.id == user_id:
            u.status = status
            return u
    return None

第四步:API 路由(纯 HTML 片段)

这是 HTMX 模式的核心——所有 API 返回 HTML 片段而非 JSON:

code
from fastapi import Form, Query
from data import get_stats, search_users, update_user_status, users_db
 
@app.get("/api/stats", response_class=HTMLResponse)
async def stats(request: Request):
    stats = get_stats()
    return templates.TemplateResponse(
        "partials/stats.html",
        {"request": request, "stats": stats}
    )
 
@app.get("/api/users", response_class=HTMLResponse)
async def users_list(
    request: Request,
    page: int = Query(1, ge=1),
    search: str = Query("", max_length=100),
):
    results = search_users(search)
    per_page = 5
    total_pages = max(1, (len(results) + per_page - 1) // per_page)
    page = min(page, total_pages)
    start = (page - 1) * per_page
    end = start + per_page
    page_users = results[start:end]
 
    return templates.TemplateResponse(
        "partials/user_table.html",
        {
            "request": request,
            "users": page_users,
            "page": page,
            "total_pages": total_pages,
            "search": search,
        }
    )
 
@app.post("/api/users/{user_id}/toggle-status", response_class=HTMLResponse)
async def toggle_user_status(request: Request, user_id: int):
    user = update_user_status(user_id, "非活跃" if users_db[user_id-1].status == "活跃" else "活跃")
    return templates.TemplateResponse(
        "partials/user_row.html",
        {"request": request, "user": user}
    )

第五步:HTMX 模板片段

创建 templates/partials/stats.html

code
<div class="stat-card">
    <h3>总用户</h3>
    <p class="stat-value">{{ stats.total_users }}</p>
</div>
<div class="stat-card">
    <h3>活跃用户</h3>
    <p class="stat-value">{{ stats.active_users }}</p>
</div>
<div class="stat-card">
    <h3>总订单</h3>
    <p class="stat-value">{{ stats.total_orders }}</p>
</div>
<div class="stat-card">
    <h3>总收入</h3>
    <p class="stat-value">¥{{ "%.2f"|format(stats.total_revenue) }}</p>
</div>

创建 templates/partials/user_table.html

code
<div class="table-controls">
    <input
        type="text"
        name="search"
        placeholder="搜索用户..."
        hx-get="/api/users"
        hx-trigger="keyup changed delay:300ms"
        hx-target="#user-table"
        hx-swap="outerHTML"
        value="{{ search }}"
    />
</div>
 
<table id="user-table">
    <thead>
        <tr>
            <th>ID</th>
            <th>姓名</th>
            <th>邮箱</th>
            <th>角色</th>
            <th>状态</th>
            <th>操作</th>
        </tr>
    </thead>
    <tbody>
        {% for user in users %}
        <tr id="user-{{ user.id }}">
            <td>{{ user.id }}</td>
            <td>{{ user.name }}</td>
            <td>{{ user.email }}</td>
            <td>{{ user.role }}</td>
            <td>{{ user.status }}</td>
            <td>
                <button
                    hx-post="/api/users/{{ user.id }}/toggle-status"
                    hx-target="#user-{{ user.id }}"
                    hx-swap="outerHTML"
                    class="btn-{{ 'danger' if user.status == '活跃' else 'success' }}"
                >
                    {{ '停用' if user.status == '活跃' else '启用' }}
                </button>
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>
 
{% if total_pages > 1 %}
<div class="pagination">
    {% for p in range(1, total_pages + 1) %}
    <button
        class="page-btn {% if p == page %}active{% endif %}"
        hx-get="/api/users?page={{ p }}&search={{ search }}"
        hx-target="#user-table"
        hx-swap="outerHTML"
    >{{ p }}</button>
    {% endfor %}
</div>
{% endif %}

创建 templates/partials/user_row.html

code
<tr id="user-{{ user.id }}">
    <td>{{ user.id }}</td>
    <td>{{ user.name }}</td>
    <td>{{ user.email }}</td>
    <td>{{ user.role }}</td>
    <td>{{ user.status }}</td>
    <td>
        <button
            hx-post="/api/users/{{ user.id }}/toggle-status"
            hx-target="#user-{{ user.id }}"
            hx-swap="outerHTML"
            class="btn-{{ 'danger' if user.status == '活跃' else 'success' }}"
        >
            {{ '停用' if user.status == '活跃' else '启用' }}
        </button>
    </td>
</tr>

第六步:用户管理页面

创建 templates/users.html

code
{% extends "base.html" %}
{% block content %}
<div class="users-page">
    <h1>用户管理</h1>
    <div
        id="user-table"
        hx-get="/api/users"
        hx-trigger="load"
        hx-swap="outerHTML"
    >
        加载中...
    </div>
</div>
{% endblock %}

main.py 中添加路由:

code
@app.get("/users", response_class=HTMLResponse)
async def users_page(request: Request):
    return templates.TemplateResponse(
        "users.html", {"request": request, "title": "用户管理"}
    )

第七步:添加一些基本的 CSS

创建 static/style.css

code
body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    display: flex;
    min-height: 100vh;
}
 
.sidebar {
    width: 240px;
    background: #1a1a2e;
    color: white;
    padding: 20px;
}
 
.sidebar ul { list-style: none; padding: 0; }
.sidebar a {
    color: #a0a0c0;
    text-decoration: none;
    display: block;
    padding: 8px 12px;
    border-radius: 6px;
}
.sidebar a:hover { background: #16213e; color: white; }
 
main { flex: 1; padding: 24px; }
 
.stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
}
.stat-card {
    background: #f8f9fa;
    padding: 20px;
    border-radius: 8px;
    text-align: center;
}
.stat-card h3 { margin: 0; font-size: 14px; color: #666; }
.stat-value { font-size: 28px; font-weight: bold; margin: 8px 0 0; }
 
table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 16px;
}
th, td {
    padding: 10px 12px;
    text-align: left;
    border-bottom: 1px solid #eee;
}
th { background: #f8f9fa; font-weight: 600; }
 
.btn-danger { background: #e74c3c; }
.btn-success { background: #27ae60; }
button {
    color: white;
    border: none;
    padding: 6px 14px;
    border-radius: 4px;
    cursor: pointer;
}
button:hover { opacity: 0.9; }
 
.table-controls { margin-bottom: 12px; }
.table-controls input {
    padding: 8px 12px;
    width: 300px;
    border: 1px solid #ddd;
    border-radius: 6px;
}
.pagination {
    display: flex;
    gap: 6px;
    margin-top: 16px;
}
.page-btn {
    background: #f0f0f0;
    color: #333;
}
.page-btn.active { background: #3498db; color: white; }

第八步:运行你的应用

在项目根目录执行:

code
uvicorn main:app --reload --port 8000

打开浏览器访问 http://localhost:8000,你就应该能看到一个带搜索、分页、状态切换功能的管理面板了。

现在数一数你写了多少行 JavaScript?一行都没有。

HTMX 的核心机制详解

现在我们已经有了一个能跑的应用,让我们深入理解 HTMX 是如何工作的。

hx-get、hx-post、hx-put、hx-delete

这些属性告诉 HTMX 用哪种 HTTP 方法向指定 URL 发起请求:

code
<button hx-get="/api/users">获取用户</button>
<form hx-post="/api/users" hx-target="#result">提交用户</form>

hx-trigger

定义触发请求的事件:

  • load:页面加载时自动触发
  • clickmouseenter:标准 DOM 事件
  • keyup changed delay:300ms:输入后 300ms 无变化才触发(搜索防抖)
  • every 5s:每 5 秒自动轮询

hx-target 和 hx-swap

  • hx-target:指定将返回的 HTML 插入到哪里(CSS 选择器)
  • hx-swap:指定插入方式
    • innerHTML:替换目标元素的内部内容
    • outerHTML:替换目标元素本身
    • beforebegin / afterbegin / beforeend / afterend:在目标元素的四周插入
    • none:仅执行请求,不修改页面

hx-trigger 的高级用法

HTMX 还支持很多高级的触发方式:

code
<!-- 滚动到可见区域时触发 -->
<div hx-get="/api/load-more" hx-trigger="revealed">
    <!-- 无限滚动内容 -->
</div>
 
<!-- 定期轮询 -->
<div hx-get="/api/notifications" hx-trigger="every 10s">
    <!-- 通知栏 -->
</div>
 
<!-- 多事件 -->
<input hx-get="/search" hx-trigger="keyup, search">

什么时候用 HTMX,什么时候用 React

HTMX 并非万能药。它能出色地处理大部分 CRUD 应用:

HTMX 的优势场景:

  • 内部管理工具(仪表盘、CRM、ERP)
  • 内容管理系统
  • 电子商务后台
  • 博客和文档站点
  • 仅需简单交互的落地页
  • 预算有限、快速交付的个人项目

React/SPA 依然必要的场景:

  • 富客户端交互应用(Figma、Notion 这类)
  • 协作编辑(Google Docs 模式)
  • 复杂数据可视化
  • 桌面级应用体验
  • 离线优先的 PWA

关键在于:大多数实际应用都属于第一类。 大部分创业项目在前 18 个月需要的只是一个能用的 CRUD 应用加上一点搜索和筛选。HTMX 可以在几天内交付它,而一个 React SPA 可能需要几周。

性能与部署

HTMX 应用在生产环境中的表现:

  • 初始加载更快:服务端渲染 HTML,首屏无需等待 JS 下载、解析和执行
  • 更小的 bundle:HTMX 本身 ~14KB (gzipped ~6KB),对比 React + ReactDOM ~42KB
  • 更少的网络请求:SPA 通常需要 3-5 次往返加载(HTML → JS → API 数据),HTMX 可以在一次请求中返回完整页面片段
  • 内存占用更低:没有 Virtual DOM 维护开销,适合低端设备

部署方式和传统后端应用一样简单。以 Python 应用为例:

code
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

配合 Nginx 反向代理即可上线。不需要 Node.js 构建服务器、不需要 CDN 部署前端、不需要处理 CORS——只有一个应用。

局限性你要知道

公平起见,HTMX 也有它的短板:

  1. 不适合高度交互的 UI——拖拽、复杂动画、富文本编辑器这些还是需要原生 JS 或专门的库
  2. 服务器开销增加——每次交互都走服务器,对后端性能要求更高
  3. SEO 不需要额外处理——因为是服务端渲染,天然对搜索引擎友好,但这本身不是问题
  4. 前端生态工具不能用——React DevTools、Vue DevTools 这种调试工具派不上用场

但这些局限性对内部工具和小型应用来说几乎不是问题。

总结

2026 年,HTMX 的流行不是对"古老技术"的怀旧,而是对"过度工程化"的理性反思。当你的应用只是一个管理员面板 + 几个 CRUD 页面时,没有必要把 12 个依赖文件和 300KB 的 JS bundle 压在用户身上。

Python + HTMX 的组合让我们重新体验到 Web 开发的快感:写服务端代码 → 渲染 HTML → 前端直接展示。没有构建步骤,没有 node_modules,没有 API 层臃肿,没有状态管理噩梦。

如果你还没有试过,今晚花两小时跑一下这个教程的示例。你可能会惊讶地发现:原来构建一个可用的 Web 应用可以这么简单。

分享到
微博Twitter

© 2026 四月 · CC BY-NC-SA 4.0

原文链接:https://www.aprilzz.com/tutorials/python-htmx-practical-tutorial