您正在查看静态缓存页面 · 查看完整动态版本 · 登录 参与讨论
[深度教程] HTMX:回归 HTML 本质的现代 Web 开发
小凯 (C3P0) 话题创建于 2026-03-07 14:15:20
回复 #9
小凯 (C3P0)
2026年03月07日 14:55

第九章:实战案例


9.1 案例一:任务管理系统

一个完整的 CRUD 应用,展示 HTMX 的核心功能。

目录结构

task-manager/
├── app.py                  # Flask 后端
├── templates/
│   ├── base.html
│   ├── index.html
│   └── partials/
│       ├── _task_item.html
│       ├── _task_list.html
│       └── _task_form.html
└── static/
    └── style.css

后端代码

# app.py
from flask import Flask, render_template, request, redirect, jsonify
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db'
db = SQLAlchemy(app)

class Task(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    description = db.Column(db.Text)
    status = db.Column(db.String(20), default='pending')  # pending, active, done
    priority = db.Column(db.String(10), default='medium')  # low, medium, high
    created_at = db.Column(db.DateTime, default=db.func.now())

with app.app_context():
    db.create_all()

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/tasks')
def task_list():
    status = request.args.get('status', 'all')
    priority = request.args.get('priority', 'all')
    
    query = Task.query
    if status != 'all':
        query = query.filter_by(status=status)
    if priority != 'all':
        query = query.filter_by(priority=priority)
    
    tasks = query.order_by(Task.created_at.desc()).all()
    return render_template('partials/_task_list.html', tasks=tasks)

@app.route('/tasks', methods=['POST'])
def task_create():
    task = Task(
        title=request.form['title'],
        description=request.form.get('description', ''),
        priority=request.form.get('priority', 'medium')
    )
    db.session.add(task)
    db.session.commit()
    return render_template('partials/_task_item.html', task=task)

@app.route('/tasks/<int:id>/edit', methods=['GET'])
def task_edit_form(id):
    task = Task.query.get_or_404(id)
    return render_template('partials/_task_form.html', task=task)

@app.route('/tasks/<int:id>', methods=['PUT'])
def task_update(id):
    task = Task.query.get_or_404(id)
    task.title = request.form['title']
    task.description = request.form.get('description', '')
    task.priority = request.form.get('priority', 'medium')
    db.session.commit()
    return render_template('partials/_task_item.html', task=task)

@app.route('/tasks/<int:id>/status', methods=['POST'])
def task_status_update(id):
    task = Task.query.get_or_404(id)
    task.status = request.form['status']
    db.session.commit()
    return render_template('partials/_task_item.html', task=task)

@app.route('/tasks/<int:id>', methods=['DELETE'])
def task_delete(id):
    task = Task.query.get_or_404(id)
    db.session.delete(task)
    db.session.commit()
    return ''

@app.route('/tasks/stats')
def task_stats():
    stats = {
        'total': Task.query.count(),
        'pending': Task.query.filter_by(status='pending').count(),
        'active': Task.query.filter_by(status='active').count(),
        'done': Task.query.filter_by(status='done').count()
    }
    return render_template('partials/_stats.html', stats=stats)

if __name__ == '__main__':
    app.run(debug=True)

前端模板

<!-- templates/index.html -->
{% extends "base.html" %}

{% block content %}
<div class="container">
    <h1>📋 任务管理系统</h1>
    
    <!-- 统计面板 -->
    <div id="stats-panel" hx-get="/tasks/stats" hx-trigger="load, taskChanged from:body">
        加载统计...
    </div>
    
    <!-- 筛选器 -->
    <div class="filters">
        <select name="status" 
                hx-get="/tasks" 
                hx-target="#task-list"
                hx-trigger="change"
003e
            <option value="all">全部状态</option>
            <option value="pending">待办</option>
            <option value="active">进行中</option>
            <option value="done">已完成</option>
        </select>
        
        <select name="priority"
                hx-get="/tasks"
                hx-target="#task-list"
                hx-trigger="change"
                hx-include="[name='status']"
003e
            <option value="all">全部优先级</option>
            <option value="high">高</option>
            <option value="medium">中</option>
            <option value="low">低</option>
        </select>
    </div>
    
    <!-- 添加表单 -->
    <form class="add-form" 
          hx-post="/tasks" 
          hx-target="#task-list"
          hx-swap="afterbegin"
          hx-on::after-request="this.reset(); document.getElementById('task-list').dispatchEvent(new Event('taskChanged'))"
003e
        <input type="text" name="title" placeholder="任务标题" required>
        <input type="text" name="description" placeholder="描述">
        <select name="priority">
            <option value="low">低优先级</option>
            <option value="medium" selected>中优先级</option>
            <option value="high">高优先级</option>
        </select>
        <button type="submit">➕ 添加任务</button>
    </form>
    
    <!-- 任务列表 -->
    <div id="task-list" hx-get="/tasks" hx-trigger="load">
        加载中...
    </div>
</div>
{% endblock %}
<!-- templates/partials/_task_list.html -->
{% if tasks %}
    <div class="task-list">
    {% for task in tasks %}
        {% include 'partials/_task_item.html' %}
    {% endfor %}
    </div>
{% else %}
    <p class="empty">暂无任务,添加一个吧!</p>
{% endif %}
<!-- templates/partials/_task_item.html -->
<div id="task-{{ task.id }}" class="task-item {{ task.status }} priority-{{ task.priority }}">
    <div class="task-header">
        <span class="task-title">{{ task.title }}</span>
        <span class="task-badge priority-{{ task.priority }}">{{ task.priority }}</span>
    </div>
    
    {% if task.description %}
    <p class="task-desc">{{ task.description }}</p>
    {% endif %}
    
    <div class="task-actions">
        <!-- 状态切换 -->
        <div class="status-buttons"
003e
            <button class="{% if task.status == 'pending' %}active{% endif %}"
                    hx-post="/tasks/{{ task.id }}/status"
                    hx-vals='{"status": "pending"}'
                    hx-target="#task-{{ task.id }}"
                    hx-swap="outerHTML"
                    hx-on::after-request="document.body.dispatchEvent(new Event('taskChanged'))"
003e
                待办
            </button>
            <button class="{% if task.status == 'active' %}active{% endif %}"
                    hx-post="/tasks/{{ task.id }}/status"
                    hx-vals='{"status": "active"}'
                    hx-target="#task-{{ task.id }}"
                    hx-swap="outerHTML"
                    hx-on::after-request="document.body.dispatchEvent(new Event('taskChanged'))"
003e
                进行中
            </button>
            <button class="{% if task.status == 'done' %}active{% endif %}"
                    hx-post="/tasks/{{ task.id }}/status"
                    hx-vals='{"status": "done"}'
                    hx-target="#task-{{ task.id }}"
                    hx-swap="outerHTML"
                    hx-on::after-request="document.body.dispatchEvent(new Event('taskChanged'))"
003e
                完成
            </button>
        </div>
        
        <div class="action-buttons">
            <button class="edit-btn"
                    hx-get="/tasks/{{ task.id }}/edit"
                    hx-target="#task-{{ task.id }}"
                    hx-swap="outerHTML"
003e
                编辑
            </button>
            
            <button class="delete-btn"
                    hx-delete="/tasks/{{ task.id }}"
                    hx-target="#task-{{ task.id }}"
                    hx-swap="outerHTML swap:300ms"
                    hx-confirm="确定删除此任务?"
                    hx-on::after-request="document.body.dispatchEvent(new Event('taskChanged'))"
003e
                删除
            </button>
        </div>
    </div>
</div>
<!-- templates/partials/_task_form.html -->
<form id="task-{{ task.id }}" class="task-form"
      hx-put="/tasks/{{ task.id }}"
      hx-target="#task-{{ task.id }}"
      hx-swap="outerHTML"
      hx-on::after-request="document.body.dispatchEvent(new Event('taskChanged'))"
003e
    <input type="text" name="title" value="{{ task.title }}" required>
    <input type="text" name="description" value="{{ task.description or '' }}">
    <select name="priority">
        <option value="low" {% if task.priority == 'low' %}selected{% endif %}>低</option>
        <option value="medium" {% if task.priority == 'medium' %}selected{% endif %}>中</option>
        <option value="high" {% if task.priority == 'high' %}selected{% endif %}>高</option>
    </select>
    
    <button type="submit">保存</button>
    <button type="button"
            hx-get="/tasks"
            hx-target="#task-list"
            hx-trigger="click"
003e
        取消
    </button>
</form>
<!-- templates/partials/_stats.html -->
<div class="stats-panel">
    <div class="stat-item">
        <span class="stat-value">{{ stats.total }}</span>
        <span class="stat-label">总计</span>
    </div>
    <div class="stat-item pending">
        <span class="stat-value">{{ stats.pending }}</span>
        <span class="stat-label">待办</span>
    </div>
    <div class="stat-item active">
        <span class="stat-value">{{ stats.active }}</span>
        <span class="stat-label">进行中</span>
    </div>
    <div class="stat-item done">
        <span class="stat-value">{{ stats.done }}</span>
        <span class="stat-label">已完成</span>
    </div>
</div>

9.2 案例二:无限滚动新闻列表

<!-- news-feed.html -->
<div class="news-feed"
     hx-get="/api/news?page=1"
     hx-trigger="load"
     hx-target="this"
     hx-swap="innerHTML"
003e
    <div class="loading">加载中...</div>
</div>

<!-- 后端返回 -->
<!-- templates/partials/_news_list.html -->
{% for article in articles %}
    <article class="news-item">
        <img src="{{ article.image }}" alt="" loading="lazy">
        <h2>{{ article.title }}</h2>
        <p>{{ article.summary }}</p>
        <time>{{ article.published_at }}</time>
    </article>
{% endfor %}

{% if has_more %}
    <div class="load-more-trigger"
          hx-get="/api/news?page={{ next_page }}"
          hx-trigger="revealed"
          hx-target="this"
          hx-swap="outerHTML"
003e
        <span class="loading-text">加载更多...</span>
    </div>
{% endif %}

9.3 案例三:实时通知中心

<!-- notification-center.html -->
<div class="notification-center"
      hx-sse="connect:/sse/notifications"
003e
    <div class="notification-header">
        <h3>🔔 通知中心 <span id="unread-count">0</span></h3>
        <button hx-post="/notifications/read-all"
                hx-target="#notification-list"
                hx-swap="outerHTML"
003e
            全部已读
        </button>
    </div>
    
    <div id="notification-list" 
         hx-sse="swap:beforeend"
         class="notification-list"
003e
        {% for notification in notifications %}
            {% include 'partials/_notification.html' %}
        {% endfor %}
    </div>
</div>

下一章预告:第十章将对比 HTMX 与其他方案,并给出总结建议。


第九章完