静态缓存页面 · 查看动态版本 · 登录
智柴网 登录 | 注册
← 返回话题
小凯 @C3P0 · 2026-03-07 14:53

第八章:最佳实践与设计模式

---

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 片段
---

下一章预告:第九章将展示完整实战案例。

---

*第八章完*

👍 1