关于 htmx-expert
面向 htmx 开发者的指导 Skill,涵盖请求属性、响应交换、事件、扩展和安全实践。适用于构建超媒体应用、排查请求响应问题,以及将 SPA 模式改为 htmx 方案。
主要功能
- 01htmx 属性与请求控制参考
- 02响应内容交换与状态管理
- 03事件和扩展配置指导
- 04安全与渐进增强实践
- 05调试及开发服务器说明
适用场景
- 01实现 htmx 表单、搜索或轮询
- 02排查 AJAX 请求与响应交换
- 03将 SPA 交互改为超媒体模式
- 04为 htmx 内容配置安全措施
MCP 相关
README 未说明与 MCP 的关系。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lullabot/ htmx-expert --skill htmx-expert安装后在 Claude Code 中输入/htmx-expert确认已加载。
附带文件references/attributes-quick-ref.mdreferences/server-patterns.md
详细介绍
htmx 专家
本技能全面介绍 htmx 开发。htmx 是一个扩展 HTML 的库,让你无需 JavaScript 即可直接使用现代浏览器功能。
核心理念
htmx 代表着一种转向超媒体优先 Web 开发的范式变革。它不再将 HTML 视为搭配 JSON API 的展示层,而是扩展 HTML,使其能够直接处理 AJAX 请求、CSS 过渡、WebSocket 和服务器发送事件。服务器返回的是 HTML 片段,而不是 JSON。
何时使用本技能
- 实现 htmx 属性和交互
- 构建由超媒体驱动的应用
- 调试 htmx 请求/响应周期
- 将 SPA 模式转换为 htmx 方案
- 了解 htmx 事件和生命周期
- 配置 htmx 扩展
- 实施恰当的安全措施
核心属性参考
HTTP 动词属性
| 属性 | 用途 | 默认触发方式 |
|---|---|---|
hx-get | 发起 GET 请求 | 点击 |
hx-post | 发起 POST 请求 | 点击(表单:提交) |
hx-put | 发起 PUT 请求 | 点击 |
hx-patch | 发起 PATCH 请求 | 点击 |
hx-delete | 发起 DELETE 请求 | 点击 |
请求控制
-
hx-trigger:自定义请求触发时机
- 修饰符:
changed、delay:Xms、throttle:Xms、once - 特殊触发器:
load、revealed、every Xs - 扩展选项:
from:<selector>、target:<selector>
- 修饰符:
-
hx-include:在请求中包含其他元素的值
-
hx-params:筛选要发送的参数(
*、none、not <param>、<param>) -
hx-headers:添加自定义标头(JSON 格式)
-
hx-vals:向请求添加值(JSON 格式)
-
hx-encoding:设置编码(上传文件时使用
multipart/form-data)
响应处理
-
hx-target:指定响应内容的放置位置
- 扩展选择器:
this、closest <sel>、next <sel>、previous <sel>、find <sel>
- 扩展选择器:
-
hx-swap:设置内容插入方式
innerHTML(默认)、outerHTML、beforebegin、afterbegin、beforeend、afterend、delete、none- 修饰符:
swap:Xms、settle:Xms、scroll:top、show:top
-
hx-select:选择响应中要交换的部分
-
hx-select-oob:选择要进行带外交换的元素
状态管理
- hx-push-url:将 URL 压入浏览器历史记录
- hx-replace-url:替换历史记录中的当前 URL
- hx-history:控制历史快照行为
- hx-history-elt:指定要生成快照的元素
UI 指示器
- hx-indicator:指定请求期间要显示的元素(添加
htmx-indicator类) - hx-disabled-elt:指定请求期间要禁用的元素
安全与控制
- hx-confirm:请求前显示确认对话框
- hx-validate:在非表单元素上启用 HTML5 验证
- hx-disable:禁用元素及其后代元素上的 htmx 处理
- hx-sync:协调不同元素之间的请求
实现模式
基本 AJAX 模式
<button hx-get="/api/data"
hx-target="#result"
hx-swap="innerHTML">
Load Data
</button>
<div id="result"></div>
主动搜索
<input type="search"
name="q"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#search-results">
<div id="search-results"></div>
无限滚动
<div hx-get="/items?page=2"
hx-trigger="revealed"
hx-swap="afterend">
Loading more...
</div>
轮询
<div hx-get="/status"
hx-trigger="every 5s"
hx-swap="innerHTML">
Status: Unknown
</div>
表单提交
<form hx-post="/submit"
hx-target="#response"
hx-swap="outerHTML">
<input name="email" type="email" required>
<button type="submit">Submit</button>
</form>
带外更新
服务器响应可以更新多个元素:
<div id="main-content">Updated content</div>
<div id="notification" hx-swap-oob="true">New notification!</div>
<span id="counter" hx-swap-oob="true">42</span>
加载指示器
<button hx-get="/slow-endpoint"
hx-indicator="#spinner">
Load
</button>
<img id="spinner" class="htmx-indicator" src="/spinner.gif">
指示器的 CSS:
.htmx-indicator {
opacity: 0;
transition: opacity 200ms ease-in;
}
.htmx-request .htmx-indicator {
opacity: 1;
}
服务器响应模式
返回 HTML 片段
服务器端点返回 HTML,而不是 JSON:
# Flask example
@app.route('/search')
def search():
q = request.args.get('q', '')
results = search_database(q)
return render_template('_search_results.html', results=results)
响应标头
htmx 可识别以下特殊标头:
| 标头 | 用途 |
|---|---|
HX-Location | 客户端重定向(带上下文) |
HX-Push-Url | 将 URL 压入历史记录 |
HX-Redirect | 完整页面重定向 |
HX-Refresh | 刷新页面 |
HX-Reswap | 覆盖 hx-swap 值 |
HX-Retarget | 覆盖 hx-target 值 |
HX-Trigger | 触发客户端事件 |
HX-Trigger-After-Settle | 在 settle 后触发 |
HX-Trigger-After-Swap | 在 swap 后触发 |
检测 htmx 请求
检查 HX-Request 标头,以区分 htmx 请求和常规请求:
if request.headers.get('HX-Request'):
return render_template('_partial.html')
else:
return render_template('full_page.html')
事件
关键事件
| 事件 | 触发时机 |
|---|---|
htmx:load | 元素加载到 DOM 中时 |
htmx:configRequest | 发送请求前(修改参数/请求头) |
htmx:beforeRequest | AJAX 请求发送前 |
htmx:afterRequest | AJAX 请求完成后 |
htmx:beforeSwap | 内容交换前 |
htmx:afterSwap | 内容交换后 |
htmx:afterSettle | DOM 稳定后 |
htmx:confirm | 确认对话框显示前 |
htmx:validation:validate | 自定义验证钩子 |
事件处理
使用 hx-on*:
<button hx-get="/data"
hx-on:htmx:before-request="console.log('Starting...')"
hx-on:htmx:after-swap="console.log('Done!')">
Load
</button>
使用 JavaScript:
document.body.addEventListener('htmx:configRequest', function(evt) {
evt.detail.headers['X-Custom-Header'] = 'value';
});
安全最佳实践
- 转义所有用户内容:通过服务端模板转义防止 XSS
- 使用 hx-disable:阻止 htmx 处理不可信内容
- 限制请求来源:
htmx.config.selfRequestsOnly = true; - 禁用脚本处理:
htmx.config.allowScriptTags = false; - 包含 CSRF 令牌:
<body hx-headers='{"X-CSRF-Token": "{{ csrf_token }}"}'> - 内容安全策略:叠加浏览器级保护
配置
关键的 htmx.config 选项:
htmx.config.defaultSwapStyle = 'innerHTML';
htmx.config.timeout = 0; // Request timeout (0 = none)
htmx.config.historyCacheSize = 10;
htmx.config.globalViewTransitions = false;
htmx.config.scrollBehavior = 'instant'; // or 'smooth', 'auto'
htmx.config.selfRequestsOnly = false;
htmx.config.allowScriptTags = true;
htmx.config.allowEval = true;
或通过 meta 标签:
<meta name="htmx-config" content='{"selfRequestsOnly":true}'>
扩展
加载扩展
<script src="https://unpkg.com/htmx-ext-<name>@<version>/<name>.js"></script>
<body hx-ext="extension-name">
常见扩展
- head-support:合并不同请求中的 head 标签信息
- idiomorph:形态转换式交换(保留元素状态)
- sse:支持服务器发送事件
- ws:支持 WebSocket
- preload:预加载内容
- response-targets:基于 HTTP 状态码进行目标定位
调试
启用日志记录:
htmx.logAll();
在 Network 标签页中检查请求头:
HX-Request: trueHX-Target: <target-id>HX-Trigger: <trigger-id>HX-Current-URL: <page-url>
渐进增强
为优雅降级构建结构:
<form action="/search" method="POST">
<input name="q"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results">
<button type="submit">Search</button>
</form>
<div id="results"></div>
非 JavaScript 用户会提交表单;JavaScript 用户则会使用 AJAX。
第三方集成
在 htmx 加载的内容上初始化库:
htmx.onLoad(function(content) {
content.querySelectorAll('.datepicker').forEach(el => {
new Datepicker(el);
});
});
对于通过编程方式添加的 htmx 内容:
htmx.process(document.getElementById('new-content'));
常见注意事项
- ID 稳定性:保持元素 ID 稳定,以便使用 CSS 过渡效果
- 交换时机:默认交换延迟为 0ms;使用
swap:100ms实现过渡效果 - 事件冒泡:htmx 事件会冒泡;使用
event.detail获取数据 - 表单数据:请求中只包含带有 name 的输入项
- 历史记录:历史快照存储的是 innerHTML,而非完整 DOM 状态
开发环境要求
htmx 需要 HTTP(而非 file://)
直接从文件系统打开 HTML 文件(file:// URL)时,htmx 将无法运行。这会导致 htmx:invalidPath 错误,原因如下:
- 浏览器会阻止来自
file://URL 的跨源请求 - htmx 需要向端点发送 HTTP 请求
解决方案:始终通过 HTTP 服务端提供 htmx 应用:
# Simple Python server (recommended for development)
python3 -m http.server 8000
# Or create a custom server with API endpoints
python3 server.py
最简开发服务端模式
对于 htmx 示例和原型,请创建一个简单的 Python 服务端,以便:
- 提供静态文件(HTML、CSS、JS)
- 提供返回 HTML 片段的 API 端点
from http.server import HTTPServer, SimpleHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
class HtmxHandler(SimpleHTTPRequestHandler):
def do_GET(self):
path = urlparse(self.path).path
if path.startswith("/api/"):
# Return HTML fragment
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(b"<div>Response HTML</div>")
else:
# Serve static files
super().do_GET()
HTTPServer(("", 8000), HtmxHandler).serve_forever()
实践经验
使用 CSS 加载指示器
使用纯 CSS 加载动画,而非图像文件,以获得更好的性能:
<button hx-get="/api/slow"
hx-indicator="#spinner">
Load
<span id="spinner" class="spinner htmx-indicator"></span>
</button>
<style>
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-block; }
.spinner {
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3d72d7;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
使用合适的触发器进行输入搜索
使用 input changed 而非 keyup changed,以获得更好的用户体验(可捕获粘贴和自动填充):
<input type="search"
name="q"
hx-get="/api/search"
hx-trigger="input changed delay:300ms, search"
hx-target="#results">
search 触发器会处理搜索输入框的清除按钮(X)。
轮询时以自身为目标
对于会替换自身的元素(轮询),请使用 hx-target="this":
<div hx-get="/api/time"
hx-trigger="load, every 2s"
hx-target="this"
hx-swap="innerHTML">
Loading...
</div>
使用 closest 更新行
对于每行都有独立更新按钮的列表项:
<li id="item-1">
<span>Item 1</span>
<button hx-get="/api/update-item/1"
hx-target="closest li"
hx-swap="outerHTML">
Update
</button>
</li>
服务端会返回完整的 <li> 元素,并保留其中新的 htmx 属性。
事件属性语法
hx-on:: 语法使用双冒号表示 htmx 事件:
<button hx-on::before-request="console.log('starting')">
<button hx-on:htmx:before-request="console.log('starting')">
组合多个触发器
使用逗号分隔触发器:
<div hx-get="/api/data"
hx-trigger="load, every 5s, click from:#refresh-btn">
带加载状态的表单 POST
组合使用 hx-indicator 和 hx-disabled-elt,以提供完整的用户体验:
<form hx-post="/api/submit"
hx-target="#result"
hx-indicator="#spinner"
hx-disabled-elt="find button">
<input name="email" required>
<button type="submit">
Submit
<span id="spinner" class="spinner htmx-indicator"></span>
</button>
</form>
其他资源
如需详细参考,请查阅: