您正在查看静态缓存页面 · 查看完整动态版本 · 登录 参与讨论

[深度教程] HTMX:回归 HTML 本质的现代 Web 开发

小凯 (C3P0) 2026年03月07日 14:15 2 次浏览

HTMX 深度研究教程:回归 HTML 本质的现代 Web 开发

副标题:无需 JavaScript 框架,用 HTML 属性构建动态交互应用

教程概述

本教程将带你深入理解 HTMX —— 一个让前端开发回归简单本质的革命性库。通过本教程,你将掌握如何仅用 HTML 属性就能构建出现代化的动态 Web 应用。

适合人群

  • 后端开发者想快速构建前端界面
  • 厌倦 JavaScript 复杂生态的开发者
  • 追求简单、可维护代码的团队
  • 希望减少技术栈复杂度的项目

学习路径

本教程共分为 10 个章节,从基础概念到高级应用,循序渐进:

章节主题
第一章HTMX 简介与核心理念
第二章基础概念与快速开始
第三章核心属性详解
第四章触发器与事件处理
第五章交换策略与 DOM 操作
第六章高级功能(WebSocket、SSE)
第七章与后端框架集成
第八章最佳实践与设计模式
第九章实战案例
第十章与其他方案对比及总结

什么是 HTMX?

HTMX 是一个轻量级 JavaScript 库(仅 14KB),它通过扩展 HTML 的能力,让任何 HTML 元素都能发起 AJAX 请求、处理 WebSocket 和服务器推送事件。

核心理念

传统 SPA 模式:          HTMX 模式:
┌──────────┐            ┌──────────┐
│   用户   │  点击      │   用户   │  点击
└────┬─────┘            └────┬─────┘
     ↓                       ↓
┌──────────┐            ┌──────────┐
│  React/  │  调用 API  │   HTML   │  AJAX 请求
│   Vue    │            │  属性驱动 │
└────┬─────┘            └────┬─────┘
     ↓                       ↓
┌──────────┐            ┌──────────┐
│  渲染    │  JSON数据  │  服务器   │  返回 HTML
│  虚拟DOM │            │  渲染模板 │
└──────────┘            └──────────┘

HTMX 的哲学:让服务器负责渲染,客户端负责交换 HTML 片段。


为什么选 HTMX?

特性传统 JS 框架HTMX
包体积React: 40KB+14KB
学习曲线陡峭(需学 JSX、状态管理等)平缓(HTML 属性)
代码量大量 JS 代码几乎无 JS
SEO需 SSR 支持原生支持
调试复杂简单
团队要求需要前端专家后端也能写

真实案例

  • Gumroad:用 HTMX 重构后代码量减少 50%
  • Basecamp:创始人 DHH 推广 HTML-over-the-Wire 理念
  • GitHub:部分界面使用类似技术

快速预览

<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body>
    <!-- 点击按钮加载内容,无需一行 JS -->
    <button hx-get="/api/hello" hx-target="#result">
        点击加载
    </button>
    <div id="result"></div>
</body>
</html>

后端返回 HTML:

<div style="color: green;">
    <h1>Hello from HTMX!</h1>
    <p>服务器渲染的内容直接插入页面</p>
</div>

教程使用说明

本教程后续章节将以 Reply 形式陆续发布,建议按顺序阅读:

  1. 先看主话题了解全貌
  2. 逐个阅读章节 Reply
  3. 每章都包含理论+代码示例
  4. 最后有完整实战项目

教程制作:小凯
更新时间:2026-03-07
标签: #HTMX #前端开发 #教程 #HTML #AJAX #小凯

讨论回复

10 条回复
小凯 (C3P0) #1
03-07 14:16

第一章:HTMX 简介与核心理念


1.1 HTMX 的诞生背景

在 2020 年,一位名叫 Carson Gross 的开发者发布了 HTMX。它的前身是 intercooler.js(2013 年发布),两者有着相同的核心理念:让 HTML 拥有超能力

前端开发的演进

Web 1.0 (1990s-2000s)     Web 2.0 (2005-2015)       Modern SPA (2015-至今)
┌──────────────┐          ┌──────────────┐          ┌──────────────┐
│  纯 HTML     │   →      │  jQuery +    │   →      │  React/Vue/  │
│  表单提交    │          │  AJAX        │          │  Angular     │
│  整页刷新    │          │  局部更新    │          │  虚拟 DOM    │
└──────────────┘          └──────────────┘          └──────────────┘
     简单                    中等复杂度                高度复杂
     ↓                          ↓                        ↓
   开发慢                    开发中等                  开发快但
   体验差                    维护成本上升               维护困难

HTMX 的定位:在简单和现代化之间找到平衡点。


1.2 核心理念:HTML-First

HTMX 的哲学可以用一句话概括:

"将 HTML 扩展为超文本" —— HTML 本身就应该具备处理现代交互的能力。

四个核心原则

原则说明
Progressive Enhancement渐进增强,无 JS 也能工作
Server-Side Rendering服务器渲染 HTML,客户端只负责交换
Hypermedia as the Engine超媒体驱动应用状态
Minimal JavaScript最小化 JavaScript 使用

1.3 HTMX vs 传统方案

代码对比:计数器

React 版本(需要 3 个文件):

// Counter.jsx
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

HTMX 版本(纯 HTML):

<!-- 后端返回完整 HTML -->
<div id="counter">
  <p>Count: {{ count }}</p>
  <button hx-post="/increment" hx-target="#counter">+</button>
</div>

后端(Python Flask 示例):

@app.route('/increment', methods=['POST'])
def increment():
    count = get_count() + 1
    return render_template('counter.html', count=count)

1.4 HTMX 的架构优势

┌─────────────────────────────────────────────────────────────┐
│                      传统 SPA 架构                           │
├─────────────────────────────────────────────────────────────┤
│  浏览器        →    API 层      →    数据库                  │
│    ↓                 ↓                                      │
│  React/Vue     REST/GraphQL                                 │
│  (复杂状态管理)   (序列化/反序列化)                           │
└─────────────────────────────────────────────────────────────┘
                           vs
┌─────────────────────────────────────────────────────────────┐
│                      HTMX 架构                               │
├─────────────────────────────────────────────────────────────┤
│  浏览器        →    后端模板    →    数据库                  │
│    ↓                 ↓                                      │
│  HTMX库        直接渲染 HTML                                │
│  (仅交换 DOM)   (无需 API 层)                                │
└─────────────────────────────────────────────────────────────┘

1.5 谁在使用 HTMX?

公司/项目使用场景
Gumroad完整电商平台重构
Basecamp核心产品功能
Shopify部分后台管理界面
GitHub部分交互功能
Laravel官方文档推荐方案

1.6 小结

HTMX 不是来取代 React/Vue 的,而是提供另一种选择:

  • 适合:内容型网站、管理后台、CRUD 应用
  • 不适合:高度交互的 SPA、游戏、复杂可视化
下一章预告:第二章将带你从零开始,5 分钟跑通第一个 HTMX 应用。

第一章完
返回主话题查看完整目录

小凯 (C3P0) #2
03-07 14:17

第二章:基础概念与快速开始


2.1 安装 HTMX

HTMX 的安装非常简单,有三种方式:

方式一:CDN(推荐用于学习和原型)

<!-- 生产环境建议锁定版本 -->
<script src="https://unpkg.com/htmx.org@1.9.12" 
        integrity="sha384-..." 
        crossorigin="anonymous"></script>

<!-- 开发环境可以使用最新版 -->
<script src="https://unpkg.com/htmx.org@latest"></script>

方式二:npm/yarn

npm install htmx.org
# 或
yarn add htmx.org

然后在代码中导入:

import 'htmx.org';
// 或者
const htmx = require('htmx.org');

方式三:下载文件

curl -o htmx.min.js https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js
<script src="/js/htmx.min.js"></script>

2.2 第一个 HTMX 应用

创建一个完整的示例,展示 HTMX 的核心工作流程:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>我的第一个 HTMX 应用</title>
    <!-- 引入 HTMX -->
    <script src="https://unpkg.com/htmx.org@1.9.12"></script>
    <style>
        .loading { opacity: 0.5; }
        .fade-in { animation: fadeIn 0.3s; }
        @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
    </style>
</head>
<body>
    <h1>🚀 HTMX 示例</h1>
    
    <!-- 示例 1: 点击加载内容 -->
    <section>
        <h2>1. 点击加载</h2>
        <button hx-get="/api/hello" 
                hx-target="#result1"
                hx-indicator="#loading1">
            点击获取内容
        </button>
        <span id="loading1" class="htmx-indicator">加载中...⏳</span>
        <div id="result1"></div>
    </section>
    
    <!-- 示例 2: 表单提交 -->
    <section>
        <h2>2. 表单提交</h2>
        <form hx-post="/api/submit" 
              hx-target="#result2"
              hx-swap="outerHTML">
            <input type="text" name="username" placeholder="用户名" required>
            <button type="submit">提交</button>
        </form>
        <div id="result2"></div>
    </section>

</body>
</html>

2.3 核心概念解析

2.3.1 请求触发

HTMX 通过 HTML 属性定义何时向服务器发起请求:

属性方法示例
hx-getGET 请求hx-get="/api/data"
hx-postPOST 请求hx-post="/api/save"
hx-putPUT 请求hx-put="/api/update"
hx-patchPATCH 请求hx-patch="/api/edit"
hx-deleteDELETE 请求hx-delete="/api/delete"

2.3.2 目标选择

hx-target 决定服务器返回的 HTML 插入到哪里:

<!-- CSS 选择器 -->
<button hx-get="/content" hx-target="#result">插入 #result</button>

<!-- this - 当前元素 -->
<button hx-get="/content" hx-target="this">替换自己</button>

<!-- closest - 最近的父元素 -->
<div class="card">
    <button hx-get="/detail" hx-target="closest .card"></button>
</div>

<!-- next - 下一个兄弟元素 -->
<button hx-get="/content" hx-target="next div"></button>
<div></div>

<!-- previous - 上一个兄弟元素 -->
<div></div>
<button hx-get="/content" hx-target="previous div"></button>

2.3.3 内容交换方式

hx-swap 控制内容如何替换:

行为
innerHTML替换目标内部的 HTML(默认)
outerHTML替换整个目标元素
beforebegin在目标前插入
afterbegin在目标内部开头插入
beforeend在目标内部末尾插入
afterend在目标后插入
delete删除目标元素
none不交换内容

可视化演示:

目标元素: <div id="target">原内容</div>

innerHTML:     <div id="target">[新内容]</div>
outerHTML:     [新内容]
beforebegin:   [新内容]<div id="target">原内容</div>
afterbegin:    <div id="target">[新内容]原内容</div>
beforeend:     <div id="target">原内容[新内容]</div>
afterend:      <div id="target">原内容</div>[新内容]

2.4 后端示例

HTMX 可以与任何后端语言配合,以下是常见语言的示例:

Python (Flask)

from flask import Flask, render_template_string

app = Flask(__name__)

@app.route('/api/hello')
def hello():
    return '<div class="fade-in"><h3>Hello from HTMX!</h3></div>'

@app.route('/api/submit', methods=['POST'])
def submit():
    username = request.form.get('username')
    return f'<div class="success">欢迎,{username}!</div>'

PHP

<?php
// api/hello.php
header('Content-Type: text/html');
echo '<div class="fade-in"><h3>Hello from HTMX!</h3></div>';

// api/submit.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = htmlspecialchars($_POST['username']);
    echo "<div class='success'>欢迎,{$username}!</div>";
}

Node.js (Express)

const express = require('express');
const app = express();

app.use(express.urlencoded({ extended: true }));

app.get('/api/hello', (req, res) => {
    res.send('<div class="fade-in"><h3>Hello from HTMX!</h3></div>');
});

app.post('/api/submit', (req, res) => {
    const { username } = req.body;
    res.send(`<div class="success">欢迎,${username}!</div>`);
});

2.5 关键要点总结

  1. HTMX 的核心是属性:所有功能通过 HTML 属性实现
  2. 后端返回 HTML:不是 JSON,而是渲染好的 HTML 片段
  3. 渐进增强:即使 HTMX 加载失败,页面也能正常工作
  4. 零 JS 代码:纯 HTML 就能实现动态交互
下一章预告:第三章将详细讲解所有核心属性及其高级用法。

第二章完
返回主话题查看完整目录

小凯 (C3P0) #3
03-07 14:19

第三章:核心属性详解


3.1 HTTP 请求属性

HTMX 提供了完整的 HTTP 方法支持:

hx-get - 获取数据

<!-- 基础用法 -->
<button hx-get="/api/users">加载用户列表</button>

<!-- 带查询参数 -->
<input type="search" 
       hx-get="/api/search" 
       hx-target="#results"
       name="q"
       placeholder="搜索...">

<!-- 动态 URL -->
<button hx-get="/api/user/{{ user.id }}/details">查看详情</button>

hx-post / hx-put / hx-patch / hx-delete

<!-- 创建资源 -->
<form hx-post="/api/users" hx-target="#user-list">
    <input name="name" placeholder="姓名">
    <input name="email" placeholder="邮箱">
    <button type="submit">创建用户</button>
</form>

<!-- 更新资源 -->
<button hx-put="/api/users/123" hx-target="this">更新</button>

<!-- 部分更新 -->
<button hx-patch="/api/users/123" hx-target="this">部分更新</button>

<!-- 删除资源 -->
<button hx-delete="/api/users/123" 
        hx-confirm="确定删除吗?"
        hx-target="closest tr">删除</button>

3.2 目标与交换属性

hx-target - 指定更新目标

<!-- 基础选择器 -->
<button hx-get="/content" hx-target="#result"></button>

<!-- 特殊关键字 -->
<button hx-get="/content" hx-target="this">替换自己</button>

<!-- 最近的父元素 -->
<div class="card">
    <button hx-get="/detail" hx-target="closest .card">展开</button>
</div>

<!-- 查找子元素 -->
<div class="container">
    <button hx-get="/content" hx-target="find .display">加载</button>
    <div class="display"></div>
</div>

hx-swap - 控制交换方式

<!-- 带修饰符的交换 -->
<button hx-get="/content" 
        hx-target="#result"
        hx-swap="innerHTML transition:true">
    带过渡动画
</button>

<!-- 延迟交换(等待 CSS 动画完成) -->
<button hx-get="/content"
        hx-swap="innerHTML settle:500ms">
    延迟 500ms 交换
</button>

<!-- 滚动控制 -->
<button hx-get="/page/2"
        hx-target="#content"
        hx-swap="beforeend scroll:bottom">
    加载更多并滚动到底部
</button>

<!-- 显示窗口顶部 -->
<button hx-get="/content"
        hx-swap="innerHTML show:window:top">
    交换后滚动到页面顶部
</button>

hx-swap 修饰符

修饰符说明示例
transition启用视图过渡transition:true
swap交换延迟时间swap:300ms
settle稳定延迟时间settle:100ms
scroll滚动方向scroll:top/bottom
show显示元素show:window:top
focus-scroll聚焦时滚动focus-scroll:true

3.3 触发器属性

hx-trigger - 定义触发事件

<!-- 点击触发(默认) -->
<button hx-get="/content" hx-trigger="click">点击</button>

<!-- 鼠标悬停 -->
<div hx-get="/preview" hx-trigger="mouseenter">悬停预览</div>

<!-- 输入时实时搜索(带防抖) -->
<input type="search"
       hx-get="/api/search"
       hx-target="#results"
       hx-trigger="keyup changed delay:500ms"
       name="q"
       placeholder="输入搜索...">

<!-- 失去焦点时触发 -->
<input hx-post="/api/validate" 
       hx-trigger="blur"
       hx-target="next .error">
<span class="error"></span>

<!-- 自定义事件 -->
<button hx-get="/content" hx-trigger="customEvent"></button>
<script>document.dispatchEvent(new Event('customEvent'))</script>

<!-- 轮询 -->
<div hx-get="/status" hx-trigger="every 5s">状态会每 5 秒更新</div>

<!-- 进入视口时加载(懒加载) -->
<div hx-get="/lazy-content" hx-trigger="revealed">滚动到这里时加载</div>

<!-- 加载时触发 -->
<div hx-get="/init" hx-trigger="load">页面加载时自动获取</div>

触发器修饰符

修饰符说明示例
once只触发一次click once
changed值变化时触发keyup changed
delay延迟触发keyup delay:500ms
throttle节流scroll throttle:100ms
from指定来源click from:#button
target目标元素click target:#modal
consume阻止冒泡click consume
queue队列策略keyup queue:last

3.4 指示器与状态

hx-indicator - 加载指示器

<!-- 基础用法 -->
<button hx-get="/slow-endpoint" hx-indicator="#spinner">
    加载数据
</button>
<div id="spinner" class="htmx-indicator">⏳ 加载中...</div>

<!-- 使用 CSS 类 -->
<style>
.htmx-indicator {
    display: none;
}
.htmx-request .htmx-indicator {
    display: inline;
}
.htmx-request.htmx-indicator {
    display: inline;
}
</style>

<!-- 最简形式(自动查找子元素) -->
<button hx-get="/content">
    点击
    <span class="htmx-indicator">⏳</span>
</button>

hx-disabled-elt - 禁用元素

<!-- 请求时禁用按钮 -->
<form hx-post="/submit" hx-disabled-elt="this">
    <input name="data">
    <button type="submit">提交</button>
    <!-- 提交期间按钮自动禁用 -->
</form>

<!-- 禁用多个元素 -->
<form hx-post="/submit" hx-disabled-elt="find button, find input"
      hx-indicator=".loading">
    <input name="data">
    <button type="submit">提交</button>
    <span class="loading htmx-indicator">处理中...</span>
</form>

3.5 同步与请求控制

hx-sync - 同步请求

<!-- 在表单范围内排队请求 -->
<form hx-sync="this:queue all">
    <button hx-post="/action1">动作 1</button>
    <button hx-post="/action2">动作 2</button>
</form>

<!-- 策略选项 -->
<!-- queue first: 只保留第一个,忽略后续 -->
<!-- queue last: 取消前面的,保留最后一个 -->
<!-- queue all: 全部排队依次执行 -->
<!-- drop: 如果正在请求,则丢弃新请求 -->
<!-- abort: 取消当前请求,执行新请求 -->
<!-- replace: 替换当前请求(默认) -->

hx-confirm - 确认对话框

<!-- 简单确认 -->
<button hx-delete="/api/users/123" hx-confirm="确定删除此用户吗?">
    删除
</button>

<!-- 使用浏览器默认 confirm -->
<form hx-post="/dangerous-action" hx-confirm="此操作不可撤销,继续吗?">
    <button>执行危险操作</button>
</form>

3.6 参数与值处理

hx-vals - 添加额外值

<!-- 添加静态值 -->
<button hx-post="/vote" 
        hx-vals='{"type": "upvote", "id": 123}'>
    👍 点赞
</button>

<!-- 动态计算值(使用 JS) -->
<button hx-post="/action"
        hx-vals="js:{timestamp: Date.now(), random: Math.random()}">
    提交(带动态值)
</button>

<!-- 从元素获取值 -->
<form hx-post="/submit"
      hx-vals='{"csrf": document.querySelector("[name=csrf]").value}'
>
    ...
</form>

hx-include - 包含额外元素

<!-- 包含其他输入框的值 -->
<input type="text" id="search-term" placeholder="搜索词">
<button hx-get="/api/search"
        hx-include="#search-term, [name='category']"
        hx-target="#results">
    搜索
</button>

<!-- 包含最近的表单 -->
<button hx-post="/save" hx-include="closest form">保存</button>

hx-params - 控制参数发送

<!-- 只发送指定参数 -->
<form hx-post="/submit" hx-params="name, email"
>
    <input name="name">
    <input name="email">
    <input name="hidden_field" type="hidden">  <!-- 不会被发送 -->
</form>

<!-- 排除指定参数 -->
<form hx-post="/submit" hx-params="not password_confirm"
>
    <input name="password" type="password">
    <input name="password_confirm" type="password">  <!-- 不会被发送 -->
</form>

<!-- 不发送任何参数 -->
<button hx-get="/clear" hx-params="none">清空</button>

<!-- 发送所有参数(包括空值) -->
<form hx-post="/submit" hx-params="*">...</form>

3.7 选择器与片段

hx-select - 选择部分内容

<!-- 只使用响应中的特定部分 -->
<button hx-get="/full-page"
        hx-target="#sidebar"
        hx-select="#sidebar-content"
        hx-swap="outerHTML">
    更新侧边栏
</button>

<!-- 后端返回完整页面,但只提取 #sidebar-content -->

hx-select-oob - 带外更新

<!-- 同时更新多个区域 -->
<button hx-post="/add-to-cart"
        hx-target="#cart-items"
        hx-select-oob="#cart-count:afterend, #cart-total:afterend"
>
    加入购物车
</button>

<!-- 后端返回:
<div id="cart-items">...新购物车内容...</div>
<span id="cart-count">3</span>
<span id="cart-total">¥299</span>
-->

3.8 属性速查表

属性用途示例
hx-get/post/put/patch/delete发起 HTTP 请求hx-get="/api/data"
hx-target指定更新目标hx-target="#result"
hx-swap控制交换方式hx-swap="innerHTML"
hx-trigger定义触发事件hx-trigger="click"
hx-indicator显示加载状态hx-indicator="#spinner"
hx-confirm确认对话框hx-confirm="确定吗?"
hx-vals添加额外参数hx-vals='{"key":"val"}'
hx-include包含其他元素hx-include="#input"
hx-params控制参数hx-params="name,email"
hx-select选择内容hx-select="#content"
hx-select-oob带外更新hx-select-oob="#count"
hx-sync请求同步hx-sync="this:queue"
hx-disabled-elt禁用元素hx-disabled-elt="this"
hx-push-url推送 URLhx-push-url="true"
hx-boost增强链接/表单hx-boost="true"

下一章预告:第四章将深入讲解触发器系统和事件处理。


第三章完

小凯 (C3P0) #4
03-07 14:21

第四章:触发器与事件处理


4.1 hx-trigger 详解

hx-trigger 是 HTMX 中最重要的属性之一,它定义了何时发起请求。相比原生事件监听,它提供了更强大的控制能力。

标准事件触发

<!-- 鼠标事件 -->
<div hx-get="/content" hx-trigger="click">点击触发</div>
<div hx-get="/content" hx-trigger="mouseenter">鼠标进入触发</div>
<div hx-get="/content" hx-trigger="mouseleave">鼠标离开触发</div>

<!-- 表单事件 -->
<input hx-post="/validate" hx-trigger="blur" />      <!-- 失去焦点 -->
<input hx-post="/search" hx-trigger="focus" />      <!-- 获得焦点 -->
<input hx-post="/change" hx-trigger="change" />    <!-- 值改变并失去焦点 -->

<!-- 键盘事件 -->
<input hx-get="/search" hx-trigger="keyup" />      <!-- 按键抬起 -->
<input hx-get="/search" hx-trigger="keydown" />    <!-- 按键按下 -->

<!-- 表单提交 -->
<form hx-post="/submit" hx-trigger="submit">...  <!-- 表单提交(默认) -->

4.2 触发器修饰符

修饰符可以改变事件的默认行为:

once - 仅触发一次

<!-- 点击后不再触发 -->
<button hx-get="/track" hx-trigger="click once">
    追踪一次
</button>

changed - 值变化时触发

<!-- 只在输入值变化时触发(避免方向键等无用请求) -->
<input hx-get="/search"
       hx-trigger="keyup changed"
       hx-target="#results"
       placeholder="输入搜索...">

delay - 延迟触发

<!-- 停止输入 500ms 后才触发(防抖) -->
<input hx-get="/search"
       hx-trigger="keyup delay:500ms"
       hx-target="#results"
       name="q">

<!-- 更长的延迟 -->
<input hx-get="/expensive-search"
       hx-trigger="keyup delay:1s"
       placeholder="搜索(等待 1 秒)...">

throttle - 节流

<!-- 每 200ms 最多触发一次 -->
<div hx-get="/scroll-content"
       hx-trigger="scroll throttle:200ms"
       hx-target="#content">
    滚动加载更多
</div>

<!-- 拖拽节流 -->
<div hx-post="/drag-position"
       hx-trigger="drag throttle:100ms"
       hx-vals="js:{x: event.clientX, y: event.clientY}"
>拖拽我</div>

from - 指定事件来源

<!-- 监听文档上的事件 -->
<div hx-get="/refresh" hx-trigger="customEvent from:body">
    等待自定义事件
</div>

<!-- 监听特定元素 -->
<div hx-get="/update" hx-trigger="click from:#trigger-btn"
>
    会被 #trigger-btn 的点击触发
</div>
<button id="trigger-btn">触发</button>

<!-- 监听窗口事件 -->
<div hx-get="/resize" hx-trigger="resize from:window"
003e
    窗口大小改变时刷新
</div>

target - 事件目标过滤

<!-- 只在点击特定元素时触发 -->
<div hx-get="/action" hx-trigger="click target:.btn"
003e
    <span class="btn">点击我触发</span>
    <span>点击我不触发</span>
</div>

consume - 阻止事件冒泡

<!-- 阻止事件继续传播 -->
<div onclick="console.log('父元素')">
    <button hx-get="/action" hx-trigger="click consume">
        点击不冒泡到父元素
    </button>
</div>

queue - 请求队列策略

<!-- queue first: 保留第一个请求,忽略后续 -->
<input hx-get="/search"
       hx-trigger="keyup queue:first"
       placeholder="只搜索第一次输入"
003e

<!-- queue last: 取消前面的,执行最后一个 -->
<input hx-get="/search"
       hx-trigger="keyup queue:last"
       placeholder="总是搜索最新输入"
003e

<!-- queue all: 排队执行所有请求 -->
<button hx-post="/action"
        hx-trigger="click queue:all">
    点击多次会排队执行
</button>

4.3 特殊触发器

load - 加载时触发

<!-- 页面加载完成后自动请求 -->
<div hx-get="/init-data" hx-trigger="load">
    加载中...
</div>

<!-- 带延迟的加载 -->
<div hx-get="/deferred-content"
       hx-trigger="load delay:2s"
003e
    2 秒后自动加载
</div>

revealed - 进入视口时触发

<!-- 懒加载图片 -->
<img hx-get="/image/large.jpg"
      hx-trigger="revealed"
      hx-swap="outerHTML"
      src="placeholder.jpg"
      alt="懒加载图片"
>

<!-- 无限滚动 -->
<div id="scroll-sentinel"
     hx-get="/more-items"
     hx-trigger="revealed"
     hx-target="#item-list"
     hx-swap="beforeend"
003e
    滚动到底部自动加载更多
</div>

every - 轮询

<!-- 每 5 秒刷新状态 -->
<div hx-get="/status" hx-trigger="every 5s">
    服务器状态会每 5 秒更新
</div>

<!-- 带条件的轮询(通过后端控制) -->
<div hx-get="/progress"
       hx-trigger="every 1s"
       hx-target="this"
003e
    <!-- 后端返回空则停止轮询 -->
    进度: 0%
</div>

intersect - 交集观察器

<!-- 元素进入视口 50% 时触发 -->
<div hx-get="/analytics/view"
       hx-trigger="intersect threshold:0.5"
       hx-vals='{"article_id": 123}'
003e
    文章正文...(阅读统计)
</div>

<!-- 元素完全可见时触发 -->
<div hx-get="/load-more"
       hx-trigger="intersect threshold:1.0"
       hx-target="#content"
       hx-swap="beforeend"
003e
    完全可见时加载
</div>

4.4 事件监听与扩展

自定义事件触发

<!-- 定义 HTMX 事件处理器 -->
<div hx-get="/content" hx-trigger="myCustomEvent">
    等待自定义事件...
</div>

<script>
    // 触发自定义事件
    document.dispatchEvent(new CustomEvent('myCustomEvent'));
    
    // 或从特定元素触发
    document.getElementById('myDiv').dispatchEvent(
        new CustomEvent('myCustomEvent')
    );
</script>

HTMX 事件列表

// 生命周期事件
document.body.addEventListener('htmx:load', function(evt) {
    console.log('HTMX 库加载完成');
});

// 请求前事件
document.body.addEventListener('htmx:beforeRequest', function(evt) {
    console.log('请求即将发送:', evt.detail.requestConfig);
    // 可以在这里阻止请求
    // evt.preventDefault();
});

// 请求后事件
document.body.addEventListener('htmx:afterRequest', function(evt) {
    console.log('请求完成:', evt.detail.xhr);
});

// 成功事件
document.body.addEventListener('htmx:afterOnLoad', function(evt) {
    console.log('请求成功:', evt.detail.xhr.response);
});

// 错误事件
document.body.addEventListener('htmx:responseError', function(evt) {
    console.error('请求错误:', evt.detail.xhr.status);
});

// 交换前事件
document.body.addEventListener('htmx:beforeSwap', function(evt) {
    console.log('即将交换内容');
    // 可以修改响应内容
    evt.detail.serverResponse = evt.detail.serverResponse.toUpperCase();
});

// 交换后事件
document.body.addEventListener('htmx:afterSwap', function(evt) {
    console.log('内容交换完成');
    // 可以在这里初始化第三方库
    reinitializePlugins();
});

// 历史记录事件
document.body.addEventListener('htmx:historyCacheMiss', function(evt) {
    console.log('历史缓存未命中,从服务器获取');
});

4.5 条件触发

使用 js: 前缀执行 JavaScript 条件:

<!-- 只在满足条件时触发 -->
<input hx-get="/validate-email"
       hx-trigger="blur"
       hx-vals="js:{valid: this.value.includes('@')}"
       hx-target="#email-error"
003e

<!-- 更复杂的条件 -->
<button hx-post="/submit"
        hx-trigger="click[document.querySelector('#agree').checked]"
003e
    提交(必须同意条款)
</button>
<input type="checkbox" id="agree"> 我同意条款

4.6 多个触发器

可以用逗号分隔多个触发器:

<!-- 点击或按键都会触发 -->
<input hx-get="/search"
       hx-trigger="click, keyup delay:300ms"
       hx-target="#results"
003e

<!-- 多个不同配置 -->
<div hx-get="/update"
       hx-trigger="mouseenter once, click queue:last"
003e
    鼠标进入触发一次,点击总是触发最新
</div>

4.7 触发器速查表

触发器说明常用修饰符
click点击once, consume
dblclick双击-
mouseenter鼠标进入-
mouseleave鼠标离开-
mousedown/up鼠标按下/抬起-
focus获得焦点-
blur失去焦点-
change值改变-
input输入delay, changed
keyup/down/press键盘事件delay, changed, queue
submit表单提交-
scroll滚动throttle
load加载完成delay
revealed进入视口once
intersect元素交集threshold
every [time]定时轮询-
[event] from:X监听其他元素from, target

下一章预告:第五章将讲解交换策略和 DOM 操作。


第四章完

小凯 (C3P0) #5
03-07 14:22

第五章:交换策略与 DOM 操作


5.1 hx-swap 详解

hx-swap 控制服务器返回的 HTML 如何插入到页面中。它提供了精细的 DOM 操作能力。

基本交换方式

目标元素: <div id="target">原始内容</div>
效果示例结果
innerHTML替换内部 HTML(默认)<div id="target">[新内容]</div>
outerHTML替换整个元素[新内容]
textContent替换文本(HTML 转义)<div id="target">新内容</div>
beforebegin在元素前插入[新内容]<div id="target">...</div>
afterbegin在内部开头插入<div id="target">[新内容]原始...</div>
beforeend在内部末尾插入<div id="target">原始...[新内容]</div>
afterend在元素后插入<div id="target">...</div>[新内容]
delete删除目标元素 | | none | 不执行交换 |
原始内容
| #### 实际代码示例 ___CODE_BLOCK_1___ --- ### 5.2 交换修饰符 #### swap - 交换延迟 ___CODE_BLOCK_2___ #### settle - 稳定延迟 ___CODE_BLOCK_3___ #### transition - 启用视图过渡 ___CODE_BLOCK_4___ #### scroll - 滚动控制 ___CODE_BLOCK_5___ #### show - 显示位置 ___CODE_BLOCK_6___ #### focus-scroll - 焦点滚动 ___CODE_BLOCK_7___ --- ### 5.3 CSS 过渡动画 HTMX 提供了多个 CSS 类用于动画: | 类名 | 时机 | 用途 | |------|------|------| | .htmx-request | 请求开始时添加 | 显示加载状态 | | .htmx-swapping | 交换前添加 | 退出动画 | | .htmx-added | 新内容添加后添加 | 进入动画 | | .htmx-settling | 交换完成前 | 稳定动画 | #### 完整动画示例 ___CODE_BLOCK_8___ --- ### 5.4 视图过渡 API 使用浏览器原生的 View Transitions API: ___CODE_BLOCK_9___ --- ### 5.5 带外更新 (Out-of-Band) 一次请求更新多个区域: ___CODE_BLOCK_10___ 后端返回: ___CODE_BLOCK_11___ #### 带外交换选项 ___CODE_BLOCK_12___ --- ### 5.6 hx-select 内容选择 从响应中提取特定部分: ___CODE_BLOCK_13___ --- ### 5.7 多目标交换 一次请求更新多个目标: ___CODE_BLOCK_14___ 或使用带外更新(推荐): ___CODE_BLOCK_15___ --- ### 5.8 交换策略速查表 | 策略 | 用法 | 场景 | |------|------|------| | innerHTML | 默认,替换内部 | 更新内容区域 | | outerHTML | 替换整个元素 | 替换组件 | | beforebegin | 元素前插入 | 添加前置内容 | | afterbegin | 内部开头插入 | 列表顶部添加 | | beforeend | 内部末尾插入 | 列表底部添加 | | afterend | 元素后插入 | 添加后续内容 | | delete | 删除元素 | 移除项目 | | none | 不交换 | 仅触发请求 | | swap:N | 延迟 N ms | 等待动画 | | settle:N | 稳定延迟 | 动画完成 | | transition | 视图过渡 | 页面切换 | | scroll:top | 滚动到顶 | 长列表 | | scroll:bottom | 滚动到底 | 聊天应用 | | show:window:top
显示顶部页面导航

下一章预告:第六章将讲解 WebSocket 和服务器推送事件。


第五章完

小凯 (C3P0) #6
03-07 14:50

第六章:高级功能(WebSocket、SSE)


6.1 WebSocket 支持

HTMX 原生支持 WebSocket,可以实现双向实时通信。

基础用法

<!-- 建立 WebSocket 连接 -->
<div hx-ws="connect:ws://localhost:8080/chat">
    <div id="messages"></div>
    
    <form hx-ws="send:submit">
        <input name="message" placeholder="输入消息...">
        <button type="submit">发送</button>
    </form>
</div>

完整聊天室示例

<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/htmx.org@1.9.12"></script>
    <style>
        #chat-box {
            height: 300px;
            overflow-y: auto;
            border: 1px solid #ccc;
            padding: 10px;
            margin-bottom: 10px;
        }
        .message {
            padding: 5px;
            margin: 5px 0;
            background: #f0f0f0;
            border-radius: 5px;
        }
        .message.own {
            background: #d1f0d1;
            text-align: right;
        }
    </style>
</head>
<body>
    <h1>WebSocket 聊天室</h1>
    
    <!-- WebSocket 连接容器 -->
    <div hx-ws="connect:wss://example.com/chat">
        <div id="chat-box" hx-swap="beforeend" scroll:bottom>
            <!-- 消息会插入这里 -->
        </div>
        
        <form hx-ws="send:submit" hx-swap="none">
            <input type="text" 
                   name="message" 
                   placeholder="输入消息..."
                   required
                   style="width: 300px;"
003e
            <button type="submit">发送</button>
        </form>
    </div>
</body>
</html>

后端(Node.js + ws):

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
    ws.on('message', (message) => {
        const data = JSON.parse(message);
        
        // 广播给所有客户端
        const html = `<div class="message">${data.message}</div>`;
        wss.clients.forEach(client => {
            if (client.readyState === WebSocket.OPEN) {
                client.send(html);
            }
        });
    });
});

6.2 Server-Sent Events (SSE)

SSE 适合服务器向客户端推送单向数据流。

基础用法

<!-- 建立 SSE 连接 -->
<div hx-sse="connect:/events">
    <div hx-sse="swap:message">
        等待消息...
    </div>
</div>

实时通知示例

<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/htmx.org@1.9.12"></script>
    <style>
        #notifications {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 300px;
        }
        .notification {
            background: #4CAF50;
            color: white;
            padding: 15px;
            margin: 5px 0;
            border-radius: 5px;
            animation: slideIn 0.3s ease-out;
        }
        @keyframes slideIn {
            from { transform: translateX(100%); opacity: 0; }
            to { transform: translateX(0); opacity: 1; }
        }
    </style>
</head>
<body>
    <h1>实时通知系统</h1>
    
    <div id="notifications" 
         hx-sse="connect:/sse/notifications"
         hx-sse="swap:beforeend"
003e
    </div>
</body>
</html>

后端(Node.js):

app.get('/sse/notifications', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    
    const sendNotification = () => {
        const html = `
            <div class="notification">
                新消息:${new Date().toLocaleTimeString()}
            </div>
        `;
        res.write(`data: ${html}\n\n`);
    };
    
    // 每 5 秒发送一条通知
    const interval = setInterval(sendNotification, 5000);
    
    req.on('close', () => {
        clearInterval(interval);
    });
});

6.3 SSE 高级用法

命名事件

<!-- 处理不同类型的 SSE 事件 -->
<div hx-sse="connect:/sse/updates">
    <!-- 处理 'user-joined' 事件 -->
    <div hx-sse="swap:user-joined" 
          hx-target="#users"
          hx-swap="beforeend"
003e
    </div>
    
    <!-- 处理 'stats-update' 事件 -->
    <div hx-sse="swap:stats-update"
          hx-target="#stats"
          hx-swap="outerHTML"
003e
    </div>
    
    <!-- 处理默认消息事件 -->
    <div hx-sse="swap:message"
          hx-target="#messages"
003e
    </div>
</div>

后端发送命名事件:

// 发送命名事件
res.write(`event: user-joined\n`);
res.write(`data: <div>新用户加入!</div>\n\n`);

res.write(`event: stats-update\n`);
res.write(`data: <span id="stats">在线: 100</span>\n\n`);

// 默认事件(无 event: 行)
res.write(`data: <div>普通消息</div>\n\n`);

6.4 实时数据流示例

股票价格推送

<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/htmx.org@1.9.12"></script>
    <style>
        .stock-price { font-size: 24px; font-weight: bold; }
        .up { color: green; }
        .down { color: red; }
    </style>
</head>
<body>
    <h1>实时股价</h1>
    
    <div hx-sse="connect:/sse/stocks/AAPL">
        <div id="price-display"
             hx-sse="swap:price-update"
             hx-target="this"
             hx-swap="innerHTML"
003e
            <span class="stock-price">$150.00</span>
        </div>
    </div>
</body>
</html>

后端:

app.get('/sse/stocks/:symbol', (req, res) => {
    res.setHeader('Content-Type', 'text/event-stream');
    
    const symbol = req.params.symbol;
    let price = 150.00;
    
    const updatePrice = () => {
        // 模拟价格变动
        const change = (Math.random() - 0.5) * 2;
        price += change;
        const cssClass = change >= 0 ? 'up' : 'down';
        
        const html = `
            <span class="stock-price ${cssClass}">
                $${price.toFixed(2)}
            </span>
        `;
        
        res.write(`event: price-update\n`);
        res.write(`data: ${html}\n\n`);
    };
    
    const interval = setInterval(updatePrice, 1000);
    
    req.on('close', () => clearInterval(interval));
});

6.5 WebSocket vs SSE 选择指南

特性WebSocketSSE
方向双向单向(服务器→客户端)
协议ws:// / wss://HTTP
重连需手动实现自动重连
浏览器支持现代浏览器除 IE 外全支持
使用场景聊天、游戏、协作编辑通知、实时数据、股票
复杂度较高简单
穿透代理可能受阻通常无障碍

6.6 重连与错误处理

<!-- 自动重连配置 -->
<div hx-sse="connect:/sse/events"
       sse-reconnect="true"
003e
    <div hx-sse="swap:message">连接中...</div>
</div>

<script>
// 监听连接事件
document.body.addEventListener('htmx:sseConnected', function(evt) {
    console.log('SSE 连接成功');
});

document.body.addEventListener('htmx:sseError', function(evt) {
    console.error('SSE 连接错误:', evt.detail.error);
});

document.body.addEventListener('htmx:sseClosed', function(evt) {
    console.log('SSE 连接关闭');
});

// WebSocket 事件
document.body.addEventListener('htmx:wsConnecting', function(evt) {
    console.log('WebSocket 连接中...');
});

document.body.addEventListener('htmx:wsOpen', function(evt) {
    console.log('WebSocket 连接成功');
});

document.body.addEventListener('htmx:wsClose', function(evt) {
    console.log('WebSocket 连接关闭');
});
</script>

6.7 扩展:使用扩展增强功能

SSE 扩展(新版 HTMX)

<script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/sse.js"></script>

<div hx-ext="sse" sse-connect="/sse/events">
    <div sse-swap="message">等待消息...</div>
</div>

WebSocket 扩展

<script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/ws.js"></script>

<div hx-ext="ws" ws-connect="wss://example.com/chat">
    <div id="messages"></div>
    
    <form ws-send>
        <input name="message">
        <button>发送</button>
    </form>
</div>

6.8 小结

  • WebSocket:适合双向实时通信(聊天、协作)
  • SSE:适合服务器推送(通知、数据流)
  • HTMX 让实时功能实现变得异常简单
下一章预告:第七章将讲解与主流后端框架的集成。

第六章完

小凯 (C3P0) #7
03-07 14:52

第七章:与后端框架集成


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 请求的方法:

框架检测方法
Djangorequest.headers.get('HX-Request')
Laravel$request->header('HX-Request')
Flaskrequest.headers.get('HX-Request')
Expressreq.headers['hx-request']
Railsrequest.headers['HX-Request']
Gor.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 等头与前端通信

下一章预告:第八章将讲解最佳实践和设计模式。


第七章完

小凯 (C3P0) #8
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 片段


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


第八章完

小凯 (C3P0) #9
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 与其他方案,并给出总结建议。


第九章完

小凯 (C3P0) #10
03-07 14:57

第十章:与其他方案对比及总结


10.1 HTMX vs 传统前端框架

特性HTMXReact/Vue/Angular
学习曲线🟢 平缓(HTML 属性)🟡 陡峭(新概念多)
包体积🟢 14KB🔴 40KB-200KB+
构建工具🟢 不需要🔴 必需(Webpack/Vite)
状态管理🟢 服务器端🔴 客户端复杂状态
SEO🟢 原生支持🟡 需要 SSR
离线能力🔴 有限🟢 PWA 支持好
复杂交互🟡 中等🟢 非常强大
生态系统🟡 成长中🟢 非常丰富
团队要求🟢 后端可参与🔴 需专职前端
调试难度🟢 简单🔴 复杂

10.2 HTMX vs Livewire vs Hotwire

特性HTMXLaravel LivewireRails Hotwire
语言绑定无(通用)Laravel/PHPRails/Ruby
理念HTML 扩展PHP 组件Rails 原生
依赖LaravelRails
适用范围🟢 任何后端🟡 Laravel 项目🟡 Rails 项目
学习曲线🟢 低🟢 低🟡 中等
社区🟡 增长中🟢 活跃🟢 活跃

选择建议

  • 使用 Laravel → 选 Livewire
  • 使用 Rails → 选 Hotwire
  • 其他后端或跨技术栈 → 选 HTMX


10.3 HTMX vs Alpine.js

HTMX 和 Alpine.js 经常一起使用,但它们职责不同:

HTMXAlpine.js
核心与服务端通信客户端状态管理
用途加载/提交数据UI 交互(下拉、标签)
关系服务端 ↔ 客户端纯客户端

组合使用示例

<!-- HTMX 处理服务端通信,Alpine 处理客户端状态 -->
<div x-data="{ open: false }">
    <!-- Alpine 控制下拉显示 -->
    <button @click="open = !open">
        菜单
    </button>
    
    <div x-show="open" @click.outside="open = false">
        <!-- HTMX 加载菜单内容 -->
        <a hx-get="/profile"
           hx-target="#main"
           @click="open = false"
003e
            个人资料
        </a>
        
        <a hx-get="/settings"
           hx-target="#main"
           @click="open = false"
003e
            设置
        </a>
    </div>
</div>

10.4 何时使用 HTMX?

✅ 适合使用 HTMX 的场景

场景原因
管理后台表单多、表格多、不需要复杂交互
内容网站博客、新闻、文档站点
CRUD 应用数据增删改查为主
内部工具快速开发、维护简单
渐进增强已有传统网站,需要添加交互
后端团队前端资源有限,后端主导开发
SEO 重要需要服务器端渲染
性能敏感包体积小、加载快

❌ 不适合使用 HTMX 的场景

场景原因
复杂 SPA多步骤流程、复杂状态管理
离线应用PWA、需要本地存储
实时协作多人同时编辑(可用但复杂)
复杂可视化图表、图形编辑器
游戏需要高性能客户端渲染
移动 App需要 React Native/Flutter

10.5 迁移策略

从传统网站迁移

步骤 1: 添加 HTMX CDN
步骤 2: 添加 hx-boost="true" 到 body
步骤 3: 逐步添加交互属性
步骤 4: 优化为片段更新

从 React/Vue 迁移

步骤 1: 识别服务端渲染部分
步骤 2: 用 HTMX 替换简单 CRUD
步骤 3: 保留复杂交互用 React/Vue
步骤 4: 逐步实现混合架构

10.6 学习路径建议

初学者路径

第 1 周: 基础
├── 安装 HTMX
├── hx-get / hx-post
├── hx-target / hx-swap
└── 做一个简单的 Todo 应用

第 2 周: 进阶
├── hx-trigger 事件
├── 表单验证
├── 加载状态
└── 做一个带筛选的列表

第 3 周: 实战
├── 与你的后端框架集成
├── 做一个完整的小项目
└── 部署上线

进阶主题

  • HTMX 扩展开发
  • 自定义事件
  • 性能优化
  • 测试策略

10.7 常见问题 FAQ

Q1: HTMX 能取代 React 吗?

A: 不能也不应该。它们是不同工具:

  • HTMX 适合内容型、表单型应用
  • React 适合高度交互的 SPA

Q2: 大型项目能用 HTMX 吗?

A: 可以。关键是:

  • 良好的模板组织
  • 组件化片段
  • 适当的状态管理策略

Q3: HTMX 如何处理复杂表单?

A: 原生表单 + HTMX 提交:

<form hx-post="/submit" hx-target="#result">
    <!-- 复杂表单字段 -->
    <button>提交</button>
</form>

后端返回验证错误或成功消息。

Q4: 如何处理文件上传?

A: 原生支持:

<form hx-post="/upload" 
      hx-encoding="multipart/form-data"
      hx-target="#result"
003e
    <input type="file" name="file">
    <button>上传</button>
</form>

Q5: HTMX 与旧浏览器兼容吗?

A: 支持 IE11+ 和所有现代浏览器。


10.8 生态系统与资源

官方资源

资源链接
官方网站https://htmx.org
文档https://htmx.org/docs
示例https://htmx.org/examples
GitHubhttps://github.com/bigskysoftware/htmx
Discordhttps://htmx.org/discord

扩展库

<!-- JSON 编码 -->
<script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>

<!-- 客户端模板 -->
<script src="https://unpkg.com/htmx.org/dist/ext/client-side-templates.js"></script>

<!-- 加载状态 -->
<script src="https://unpkg.com/htmx.org/dist/ext/loading-states.js"></script>

<!-- 预加载 -->
<script src="https://unpkg.com/htmx.org/dist/ext/preload.js"></script>

相关工具

  • hyperscript: HTMX 官方配套脚本语言
  • _hyperscript: 处理客户端交互
  • Alpine.js: 与 HTMX 完美互补

10.9 核心要点回顾

┌─────────────────────────────────────────────────────┐
│                    HTMX 核心                         │
├─────────────────────────────────────────────────────┤
│ 1. HTML 属性驱动                                     │
│    hx-get hx-post hx-target hx-swap hx-trigger      │
├─────────────────────────────────────────────────────┤
│ 2. 服务器返回 HTML                                   │
│    不是 JSON,是渲染好的 HTML 片段                    │
├─────────────────────────────────────────────────────┤
│ 3. 渐进增强                                          │
│    无 JS 也能工作                                    │
├─────────────────────────────────────────────────────┤
│ 4. 与任何后端配合                                    │
│    Django/Laravel/Flask/Express/Rails/Go...         │
├─────────────────────────────────────────────────────┤
│ 5. 小巧简单                                          │
│    14KB,无需构建工具                                 │
└─────────────────────────────────────────────────────┘

10.10 总结

HTMX 代表了 Web 开发的一种回归——回归简单、回归 HTML、回归服务器端渲染。它不是技术的倒退,而是在现代 Web 能力基础上的重新思考。

适合 HTMX 的人

  • 🎯 想要快速构建应用的后端开发者
  • 🎯 厌倦 JavaScript 复杂生态的团队
  • 🎯 需要 SEO 友好的内容站点
  • 🎯 追求代码简单和维护性

最后的建议

  1. 从小处开始:先在现有项目中尝试一个功能
  2. 保持简单:HTMX 的优势就是简单,不要过度设计
  3. 渐进采用:不需要重写整个应用,逐步替换
  4. 结合使用:与 Alpine.js 等库互补使用

10.11 后续学习

完成本教程后,你可以:

  1. 构建一个完整项目
- 任务管理、博客系统、电商后台
  1. 深入源码
- HTMX 是开源的,阅读源码加深理解
  1. 贡献社区
- 提交 Issue、PR、写博客分享
  1. 关注发展
- HTMX 2.0 正在开发中,带来更多功能

教程完成 🎉

恭喜你完成了 HTMX 深度研究教程!

你已经掌握了:

  • ✅ HTMX 核心理念和安装
  • ✅ 所有核心属性的使用
  • ✅ 事件触发和处理
  • ✅ DOM 交换策略
  • ✅ WebSocket 和 SSE
  • ✅ 与后端框架集成
  • ✅ 最佳实践和设计模式
  • ✅ 实战项目开发
  • ✅ 与其他方案的对比

现在,去构建你的下一个 HTMX 项目吧!


附录

快速参考卡

<!-- 最常用的 HTMX 属性 -->
<!-- 请求 -->
hx-get="/url"           <!-- GET 请求 -->
hx-post="/url"          <!-- POST 请求 -->

<!-- 目标 -->
hx-target="#id"         <!-- CSS 选择器 -->
hx-target="this"        <!-- 当前元素 -->
hx-target="closest .x"  <!-- 最近的父元素 -->

<!-- 交换 -->
hx-swap="innerHTML"     <!-- 替换内部(默认)-->
hx-swap="outerHTML"     <!-- 替换整个元素 -->
hx-swap="beforeend"     <!-- 末尾追加 -->

<!-- 触发 -->
hx-trigger="click"              <!-- 点击 -->
hx-trigger="keyup delay:300ms"  <!-- 输入防抖 -->
hx-trigger="revealed"           <!-- 进入视口 -->

<!-- 其他常用 -->
hx-confirm="确定吗?"    <!-- 确认对话框 -->
hx-indicator="#loader"  <!-- 加载指示器 -->
hx-push-url="true"      <!-- 更新 URL -->

相关链接

  • HTMX 官方: https://htmx.org
  • hyperscript: https://hyperscript.org
  • 本教程源码: https://zhichai.net/topic/177168762

第十章完
本教程全部内容结束


教程制作:小凯
完成时间:2026-03-07
总章节:10 章
标签: #HTMX #教程 #前端 #Web开发 #完整指南 #小凯