第七章:与后端框架集成
---
7.1 Django + HTMX
Django 与 HTMX 配合非常自然,使用模板引擎返回 HTML 片段。
#### 基础配置
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('todo/', views.todo_list, name='todo_list'),
path('todo/add/', views.todo_add, name='todo_add'),
path('todo/<int:pk>/toggle/', views.todo_toggle, name='todo_toggle'),
path('todo/<int:pk>/delete/', views.todo_delete, name='todo_delete'),
]
#### 视图函数
# views.py
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from .models import Todo
def index(request):
return render(request, 'index.html')
def todo_list(request):
todos = Todo.objects.all()
return render(request, 'todos/list.html', {'todos': todos})
def todo_add(request):
if request.method == 'POST':
title = request.POST.get('title', '').strip()
if title:
todo = Todo.objects.create(title=title)
# 返回单个 todo 项
return render(request, 'todos/item.html', {'todo': todo})
return HttpResponse('')
def todo_toggle(request, pk):
todo = get_object_or_404(Todo, pk=pk)
todo.completed = not todo.completed
todo.save()
return render(request, 'todos/item.html', {'todo': todo})
def todo_delete(request, pk):
todo = get_object_or_404(Todo, pk=pk)
if request.method == 'DELETE':
todo.delete()
return HttpResponse('') # 返回空,前端移除元素
return HttpResponse('Method not allowed', status=405)
#### 模板
<!-- templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body>
<h1>Django + HTMX Todo</h1>
<!-- 添加表单 -->
<form hx-post="{% url 'todo_add' %}"
hx-target="#todo-list"
hx-swap="afterbegin"
hx-on::after-request="this.reset()"
003e
{% csrf_token %}
<input type="text" name="title" placeholder="新任务..." required>
<button type="submit">添加</button>
</form>
<!-- 列表 -->
<div id="todo-list" hx-get="{% url 'todo_list' %}" hx-trigger="load">
加载中...
</div>
</body>
</html>
<!-- templates/todos/list.html -->
{% for todo in todos %}
{% include 'todos/item.html' with todo=todo %}
{% empty %}
<p>暂无任务</p>
{% endfor %}
<!-- templates/todos/item.html -->
<div id="todo-{{ todo.id }}" class="todo-item"
003e
<input type="checkbox"
{% if todo.completed %}checked{% endif %}
hx-post="{% url 'todo_toggle' todo.id %}"
hx-target="#todo-{{ todo.id }}"
hx-swap="outerHTML"
>
<span class="{% if todo.completed %}completed{% endif %}">
{{ todo.title }}
</span>
<button hx-delete="{% url 'todo_delete' todo.id %}"
hx-target="#todo-{{ todo.id }}"
hx-swap="outerHTML swap:300ms"
hx-confirm="删除此任务?"
003e
删除
</button>
</div>
---
7.2 Laravel + HTMX
Laravel 生态系统对 HTMX 支持很好,特别是 Blade 模板引擎。
#### 基础配置
// routes/web.php
Route::get('/contacts', [ContactController::class, 'index']);
Route::get('/contacts/create', [ContactController::class, 'create']);
Route::post('/contacts', [ContactController::class, 'store']);
Route::get('/contacts/{contact}/edit', [ContactController::class, 'edit']);
Route::put('/contacts/{contact}', [ContactController::class, 'update']);
Route::delete('/contacts/{contact}', [ContactController::class, 'destroy']);
#### 控制器
// app/Http/Controllers/ContactController.php
class ContactController extends Controller
{
public function index()
{
$contacts = Contact::all();
// 检测 HTMX 请求
if (request()->header('HX-Request')) {
return view('contacts._list', compact('contacts'));
}
return view('contacts.index', compact('contacts'));
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required',
'email' => 'required|email',
]);
$contact = Contact::create($validated);
return view('contacts._row', compact('contact'));
}
public function update(Request $request, Contact $contact)
{
$contact->update($request->all());
return view('contacts._row', compact('contact'));
}
public function destroy(Contact $contact)
{
$contact->delete();
return response('')->header('HX-Trigger', 'contactDeleted');
}
}
#### Blade 模板
<!-- resources/views/contacts/index.blade.php -->
@extends('layouts.app')
@section('content')
<h1>联系人管理</h1>
<form hx-post="{{ route('contacts.store') }}"
hx-target="#contact-list"
hx-swap="afterbegin"
class="mb-4"
003e
@csrf
<input type="text" name="name" placeholder="姓名" required>
<input type="email" name="email" placeholder="邮箱" required>
<button type="submit">添加</button>
</form>
<div id="contact-list">
@include('contacts._list')
</div>
@endsection
<!-- resources/views/contacts/_row.blade.php -->
<tr id="contact-{{ $contact->id }}">
<td>{{ $contact->name }}</td>
<td>{{ $contact->email }}</td>
<td>
<button hx-get="{{ route('contacts.edit', $contact) }}"
hx-target="closest tr"
hx-swap="outerHTML"
003e
编辑
</button>
<button hx-delete="{{ route('contacts.destroy', $contact) }}"
hx-target="closest tr"
hx-swap="delete"
hx-confirm="确定删除?"
003e
删除
</button>
</td>
</tr>
---
7.3 Flask + HTMX
Flask 的简洁性与 HTMX 完美匹配。
from flask import Flask, render_template, request, redirect
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todos.db'
db = SQLAlchemy(app)
class Todo(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
done = db.Column(db.Boolean, default=False)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/todos')
def todo_list():
todos = Todo.query.all()
return render_template('todos.html', todos=todos)
@app.route('/todos', methods=['POST'])
def todo_add():
title = request.form.get('title', '').strip()
if title:
todo = Todo(title=title)
db.session.add(todo)
db.session.commit()
return render_template('todo_item.html', todo=todo)
return '', 400
@app.route('/todos/<int:id>/toggle', methods=['POST'])
def todo_toggle(id):
todo = Todo.query.get_or_404(id)
todo.done = not todo.done
db.session.commit()
return render_template('todo_item.html', todo=todo)
@app.route('/todos/<int:id>', methods=['DELETE'])
def todo_delete(id):
todo = Todo.query.get_or_404(id)
db.session.delete(todo)
db.session.commit()
return ''
---
7.4 Node.js/Express + HTMX
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.set('view engine', 'ejs');
let todos = [
{ id: 1, title: '学习 HTMX', done: false },
{ id: 2, title: '构建应用', done: false },
];
// 检测 HTMX 请求的辅助函数
const isHtmx = (req) => req.headers['hx-request'] === 'true';
app.get('/', (req, res) => {
res.render('index');
});
app.get('/todos', (req, res) => {
if (isHtmx(req)) {
// 只返回列表片段
res.render('partials/todo-list', { todos });
} else {
res.render('full-page', { todos });
}
});
app.post('/todos', (req, res) => {
const todo = {
id: Date.now(),
title: req.body.title,
done: false
};
todos.push(todo);
res.render('partials/todo-item', { todo });
});
app.post('/todos/:id/toggle', (req, res) => {
const todo = todos.find(t => t.id == req.params.id);
if (todo) {
todo.done = !todo.done;
res.render('partials/todo-item', { todo });
} else {
res.status(404).send('Not found');
}
});
app.delete('/todos/:id', (req, res) => {
todos = todos.filter(t => t.id != req.params.id);
res.send(''); // HTMX 将移除目标元素
});
app.listen(3000);
---
7.5 检测 HTMX 请求
各框架检测 HTMX 请求的方法:
| 框架 | 检测方法 |
|---|
| Django | request.headers.get('HX-Request') |
| Laravel | $request->header('HX-Request') |
| Flask | request.headers.get('HX-Request') |
| Express | req.headers['hx-request'] |
| Rails | request.headers['HX-Request'] |
| Go | r.Header.Get("HX-Request") |
---
7.6 常用 HTMX 响应头
后端可以设置这些响应头来控制 HTMX 行为:
# 触发客户端事件
response['HX-Trigger'] = 'itemCreated'
# 带数据的触发
response['HX-Trigger'] = json.dumps({
'itemCreated': {'id': item.id, 'name': item.name}
})
# 重定向
response['HX-Redirect'] = '/success-page'
# 刷新页面
response['HX-Refresh'] = 'true'
# 替换 URL(不跳转)
response['HX-Push-Url'] = '/new-url'
# 替换标题
response['HX-Retarget'] = '#other-element'
response['HX-Reswap'] = 'innerHTML'
---
7.7 小结
HTMX 与任何后端框架都能很好地配合:
- 后端只需要返回 HTML 片段
- 使用
HX-Request 头检测 HTMX 请求
- 使用
HX-Trigger 等头与前端通信
下一章预告:第八章将讲解最佳实践和设计模式。
---
*第七章完*