第八章:最佳实践与设计模式
---
8.1 渐进增强原则
HTMX 的核心理念是渐进增强——即使 JavaScript 失败,应用仍能正常工作。
#### 基础版本(无 JS 也能工作)
<!-- 传统表单 -->
<form action="/search" method="GET">
<input type="search" name="q" placeholder="搜索...">
<button type="submit">搜索</button>
</form>
#### 增强版本(添加 HTMX)
<!-- 同样的表单,添加 HTMX 属性 -->
<form action="/search"
method="GET"
hx-get="/search"
hx-target="#results"
hx-push-url="true"
003e
<input type="search" name="q" placeholder="搜索...">
<button type="submit">搜索</button>
</form>
<div id="results"></div>
如果 HTMX 加载失败,表单会正常提交,页面正常跳转。
---
8.2 模板组织策略
推荐的项目结构:
templates/
├── base.html # 基础布局
├── index.html # 完整页面
└── partials/ # HTML 片段
├── _header.html
├── _sidebar.html
├── _todo_item.html
├── _todo_list.html
└── _notification.html
#### 模板继承示例
<!-- base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My App{% endblock %}</title>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
{% block extra_head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
<!-- index.html -->
{% extends "base.html" %}
{% block content %}
<h1>任务列表</h1>
<form hx-post="/todos"
hx-target="#todo-list"
hx-swap="afterbegin"
hx-on::after-request="this.reset()"
003e
<input name="title" placeholder="新任务..." required>
<button>添加</button>
</form>
<div id="todo-list">
{% include "partials/_todo_list.html" %}
</div>
{% endblock %}
---
8.3 常见设计模式
#### 模式 1:Active Search(实时搜索)
<input type="search"
name="q"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#search-results"
hx-indicator="#search-spinner"
placeholder="输入搜索..."
autocomplete="off"
003e
<span id="search-spinner" class="htmx-indicator">⏳</span>
<div id="search-results"></div>
#### 模式 2:Inline Edit(行内编辑)
<!-- 显示模式 -->
<div id="item-{{ item.id }}">
<span>{{ item.name }}</span>
<button hx-get="/items/{{ item.id }}/edit"
hx-target="#item-{{ item.id }}"
hx-swap="outerHTML"
003e
编辑
</button>
</div>
<!-- 编辑模式(后端返回)-->
<form id="item-{{ item.id }}"
hx-put="/items/{{ item.id }}"
hx-target="#item-{{ item.id }}"
hx-swap="outerHTML"
003e
<input type="text" name="name" value="{{ item.name }}">
<button type="submit">保存</button>
<button type="button"
hx-get="/items/{{ item.id }}"
hx-target="#item-{{ item.id }}"
hx-swap="outerHTML"
003e
取消
</button>
</form>
#### 模式 3:Click to Load(点击加载更多)
<div id="item-list">
{% for item in items %}
{% include "partials/_item.html" %}
{% endfor %}
</div>
{% if has_more %}
<button hx-get="/items?page={{ next_page }}"
hx-target="#item-list"
hx-swap="beforeend"
hx-select=".item"
hx-indicator=".loading"
hx-on::after-request="this.remove()"
003e
加载更多
<span class="loading htmx-indicator">⏳</span>
</button>
{% endif %}
#### 模式 4:Bulk Actions(批量操作)
<form hx-post="/items/bulk-delete"
hx-confirm="确定删除选中的项目?"
003e
<div class="toolbar">
<button type="submit" class="danger">删除选中</button>
</div>
<table>
{% for item in items %}
<tr>
<td>
<input type="checkbox" name="ids" value="{{ item.id }}">
</td>
<td>{{ item.name }}</td>
</tr>
{% endfor %}
</table>
</form>
#### 模式 5:Lazy Loading(懒加载)
<!-- 图片懒加载 -->
<img hx-get="/image/large/{{ img.id }}"
hx-trigger="revealed"
hx-swap="outerHTML"
src="{{ img.thumbnail_url }}"
alt="{{ img.alt }}"
003e
<!-- 内容懒加载 -->
<div hx-get="/comments/{{ post.id }}"
hx-trigger="revealed"
hx-target="this"
003e
<p>加载评论中...</p>
</div>
#### 模式 6:Tabs(选项卡)
<div class="tabs">
<button hx-get="/tab/content1"
hx-target="#tab-content"
class="active"
003e
选项卡 1
</button>
<button hx-get="/tab/content2"
hx-target="#tab-content"
003e
选项卡 2
</button>
<button hx-get="/tab/content3"
hx-target="#tab-content"
003e
选项卡 3
</button>
</div>
<div id="tab-content">
{% include "partials/_tab_content1.html" %}
</div>
---
8.4 错误处理最佳实践
<!-- 全局错误处理 -->
<script>
document.body.addEventListener('htmx:responseError', function(evt) {
const xhr = evt.detail.xhr;
if (xhr.status === 404) {
alert('请求的资源不存在');
} else if (xhr.status === 500) {
alert('服务器错误,请稍后重试');
} else if (xhr.status === 422) {
// 表单验证错误,显示在页面上
const errorDiv = document.getElementById('form-errors');
errorDiv.innerHTML = xhr.response;
}
});
document.body.addEventListener('htmx:sendError', function(evt) {
alert('网络错误,请检查网络连接');
});
</script>
<!-- 特定元素错误处理 -->
<form hx-post="/submit"
hx-target="#result"
hx-on::response-error="document.getElementById('error-msg').innerText = '提交失败'"
003e
...
<p id="error-msg" style="color: red;"></p>
</form>
---
8.5 性能优化
#### 1. 防抖和节流
<!-- 搜索防抖 -->
<input hx-get="/search"
hx-trigger="keyup changed delay:300ms"
name="q"
003e
<!-- 滚动节流 -->
<div hx-get="/more"
hx-trigger="scroll throttle:100ms"
003e
#### 2. 使用缓存
<!-- 使用 localStorage 缓存(通过扩展)-->
<div hx-get="/static-content"
hx-trigger="load"
hx-ext="local-cache"
003e
#### 3. 预加载
<!-- 鼠标悬停时预加载 -->
<a href="/page/2"
hx-get="/page/2"
hx-trigger="mouseenter once"
hx-swap="none"
hx-push-url="false"
003e
下一页(悬停预加载)
</a>
#### 4. 减小响应大小
# 后端只返回必要的 HTML
def todo_partial(request, id):
todo = get_object_or_404(Todo, id=id)
# 只返回这一行的 HTML,不是整个页面
return render(request, 'partials/todo_item.html', {'todo': todo})
---
8.6 安全考虑
#### CSRF 保护
<!-- Django -->
<form hx-post="/action">
{% csrf_token %}
...
</form>
<!-- Laravel -->
<form hx-post="/action">
@csrf
...</form>
<!-- 或通过 meta 标签全局配置 -->
<meta name="csrf-token" content="{{ csrf_token }}">
<script>
document.body.addEventListener('htmx:configRequest', function(evt) {
evt.detail.headers['X-CSRF-Token'] =
document.querySelector('meta[name="csrf-token"]').content;
});
</script>
#### 确认敏感操作
<!-- 删除确认 -->
<button hx-delete="/items/123"
hx-confirm="确定要删除此项目吗?此操作不可撤销。"
hx-target="closest tr"
hx-swap="delete"
003e
删除
</button>
<!-- 自定义确认对话框(使用 Hyperscript)-->
<button hx-delete="/items/123"
hx-target="closest tr"
hx-swap="delete"
_="on click
call Swal.fire({title: '确认删除?',
text: '此操作不可撤销',
icon: 'warning',
showCancelButton: true})
if result.isConfirmed trigger confirmed"
hx-trigger="confirmed"
003e
删除
</button>
---
8.7 调试技巧
#### 1. 开启日志
<script>
htmx.logAll(); // 开启所有日志
</script>
#### 2. 使用开发者工具
<!-- 显示请求信息 -->
<div hx-get="/test" hx-target="this">测试</div>
在浏览器控制台:
// 查看 HTMX 配置
htmx.config
// 查看元素上的 HTMX 状态
htmx.find('#myElement').htmxData
#### 3. 网络面板调试
查看 Network 面板中的 HTMX 请求:
- 请求头:
HX-Request: true - 响应:HTML 片段
下一章预告:第九章将展示完整实战案例。
---
*第八章完*