事件系统与扩展
事件系统与扩展
htmx 的声明式属性覆盖了大部分交互需求,但在某些场景下你需要更精细的控制:在请求发送前添加认证头、在交换完成后执行初始化代码、拦截错误做统一处理。htmx 的自定义事件系统让你可以介入请求生命周期的每个阶段。
事件生命周期概览
htmx 在 AJAX 请求的完整生命周期中触发一系列事件,按时间顺序排列如下:
版本注意:htmx 2.x 中所有事件名统一使用 kebab-case(短横线连接),如
htmx:after-swap。如果你从 htmx 1.x 迁移而来,需注意 1.x 使用的 camelCase(如htmx:afterSwap)在 2.x 中已不再支持。
htmx:confirm → 用户确认阶段
htmx:before-request → 请求发送前
htmx:before-send → XHR 发送前(可修改请求)
htmx:xhr:loadstart → XHR 开始加载
htmx:xhr:progress → XHR 进度更新(文件上传)
htmx:xhr:loadend → XHR 加载结束
htmx:after-request → 请求完成后
htmx:before-swap → DOM 交换前
htmx:after-swap → DOM 交换后
htmx:after-settle → 新内容完全稳定后
htmx:response-error → HTTP 错误响应
htmx:send-error → 网络发送失败
htmx:swap-error → DOM 交换失败
htmx:target-error → 目标元素未找到
htmx:oob-error-no-target → OOB 目标未找到核心事件详解
htmx
在请求即将发送前触发。可以用来阻止请求或修改请求参数。
document.body.addEventListener('htmx:before-request', function(evt) {
const btn = evt.detail.elt; // 触发元素
const target = evt.detail.target; // 目标元素
const xhr = evt.detail.xhr; // XMLHttpRequest 对象
const requestConfig = evt.detail.requestConfig; // 请求配置
console.log('即将请求:', requestConfig.path);
// 阻止请求示例:如果用户未登录
if (!isLoggedIn()) {
evt.preventDefault();
showLoginModal();
}
});htmx
请求完成后触发(无论成功或失败),此时响应已接收但尚未进行 DOM 交换。
document.body.addEventListener('htmx:after-request', function(evt) {
const xhr = evt.detail.xhr;
const successful = evt.detail.successful; // 是否成功
if (successful) {
console.log('请求成功,状态码:', xhr.status);
} else {
console.log('请求失败,状态码:', xhr.status);
}
});htmx
在 DOM 交换前触发,是修改响应内容的最后机会。
document.body.addEventListener('htmx:before-swap', function(evt) {
const xhr = evt.detail.xhr;
const shouldSwap = evt.detail.shouldSwap; // 是否执行交换
const target = evt.detail.target;
// 根据响应状态码决定是否交换
if (xhr.status === 404) {
evt.detail.shouldSwap = true;
evt.detail.serverResponse = '<div class="error">页面未找到</div>';
}
// 对特定目标做特殊处理
if (target.id === 'modal-content') {
// 可以在这里修改响应内容
console.log('即将更新模态框内容');
}
});注意:
htmx:before-swap中的shouldSwap默认为true,设置为false可以阻止 DOM 交换。
htmx
DOM 交换完成后触发。适合执行需要访问新插入 DOM 的操作,如重新绑定第三方库、初始化图表等。
document.body.addEventListener('htmx:after-swap', function(evt) {
const target = evt.detail.target;
// 如果更新的区域包含代码块,高亮显示
target.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
// 如果更新了表单,重新聚焦第一个输入框
const firstInput = target.querySelector('input, textarea, select');
if (firstInput) {
firstInput.focus();
}
});htmx
在内容完全”稳定”后触发。与 afterSwap 的区别在于,afterSettle 等待所有 CSS 过渡和动画完成后再触发。
document.body.addEventListener('htmx:after-settle', function(evt) {
// 执行需要等待动画完成的操作
scrollToElement(evt.detail.target);
});错误事件
// HTTP 错误响应(4xx, 5xx)
document.body.addEventListener('htmx:response-error', function(evt) {
const xhr = evt.detail.xhr;
console.error('服务器返回错误:', xhr.status, xhr.statusText);
if (xhr.status === 401) {
window.location.href = '/login';
}
if (xhr.status === 403) {
alert('没有权限执行此操作');
}
});
// 网络错误(无法连接到服务器)
document.body.addEventListener('htmx:send-error', function(evt) {
console.error('网络连接失败');
showOfflineNotification();
});事件过滤与委托
htmx 事件支持标准的事件委托模式,你可以精确监听特定元素上的事件。
按元素类型过滤
// 只监听表单提交相关的事件
document.querySelectorAll('form').forEach(form => {
form.addEventListener('htmx:after-request', function(evt) {
if (evt.detail.successful) {
// 表单提交成功后的操作
form.reset();
}
});
});使用事件委托
// 监听整个文档,但只在特定条件下处理
document.addEventListener('htmx:after-swap', function(evt) {
// 检查目标元素是否匹配选择器
if (evt.detail.target.matches('[data-auto-focus]')) {
const input = evt.detail.target.querySelector('input');
if (input) input.focus();
}
});使用 hx-on 内联处理
htmx 提供了 hx-on: 属性,允许在 HTML 中直接绑定事件处理器,无需编写外部 JavaScript。
基础用法
<form hx-post="/api/submit"
hx-on::after-request="this.reset()">
<input type="text" name="title" required />
<button type="submit">提交</button>
</form>hx-on::after-request 中的双冒号分隔命名空间 htmx 和事件名 after-request。this 指向触发请求的元素(即该表单)。
多事件绑定
<button hx-get="/api/data"
hx-on::before-request="this.disabled = true"
hx-on::after-request="this.disabled = false"
hx-target="#result">
加载数据
</button>访问事件详情
<div hx-get="/api/content"
hx-on::after-request="console.log(event.detail.xhr.status)"
hx-target="#output">
点击加载
</div>内联处理器中通过 event 对象访问事件详情。
扩展机制简介
htmx 的扩展系统允许你封装可复用的行为。扩展通过 htmx.defineExtension() 注册,然后通过 hx-ext 属性启用。
使用内置扩展
htmx 自带了一些有用的扩展:
<!-- json-enc 扩展:以 JSON 格式发送请求 -->
<form hx-post="/api/create"
hx-ext="json-enc"
hx-target="#result">
<input type="text" name="name" />
<button type="submit">创建</button>
</form>| 内置扩展 | 功能 |
|---|---|
json-enc | 以 application/json 编码发送表单数据 |
path-deps | 当指定路径的请求成功时自动刷新当前元素 |
loading-states | 更丰富的加载状态管理 |
class-tools | 延迟添加/移除 CSS 类 |
response-targets | 根据响应状态码路由到不同目标 |
自定义扩展
以下是一个简单的自定义扩展示例,它自动为所有请求添加一个 CSRF Token:
htmx.defineExtension('csrf-token', {
onEvent: function(name, evt) {
if (name === 'htmx:config-request') {
const token = document.querySelector('meta[name="csrf-token"]')?.content;
if (token) {
evt.detail.headers['X-CSRF-Token'] = token;
}
}
}
});启用扩展:
<meta name="csrf-token" content="abc123...">
<body hx-ext="csrf-token">
<!-- 此 body 内所有 htmx 请求都会自动携带 CSRF Token -->
</body>扩展生命周期钩子
自定义扩展可以定义以下钩子:
| 钩子 | 触发时机 |
|---|---|
init | 扩展初始化时 |
onEvent | 任何 htmx 事件发生时 |
transformResponse | 响应到达后、交换前 |
isInlineSwap | 判断是否为内联交换策略 |
handleSwap | 自定义交换逻辑 |
完整示例:全局错误处理
实现一个统一的全局错误提示系统:
<!-- 页面顶部的错误容器 -->
<div id="global-error"></div>
<script>
// 统一的错误处理
document.body.addEventListener('htmx:response-error', function(evt) {
const xhr = evt.detail.xhr;
let message = '请求失败,请稍后重试';
switch (xhr.status) {
case 400:
message = '请求参数错误';
break;
case 401:
message = '请先登录';
setTimeout(() => window.location.href = '/login', 1500);
break;
case 403:
message = '没有权限执行此操作';
break;
case 404:
message = '请求的资源不存在';
break;
case 422:
message = '数据验证失败,请检查输入';
break;
case 500:
message = '服务器内部错误';
break;
}
const errorContainer = document.getElementById('global-error');
errorContainer.innerHTML = `<div class="alert alert-error">${message}</div>`;
});
// 网络断开处理
document.body.addEventListener('htmx:send-error', function(evt) {
const errorContainer = document.getElementById('global-error');
errorContainer.innerHTML = '<div class="alert alert-warning">网络连接失败,请检查网络后重试</div>';
});
// 请求开始时清除旧错误
document.body.addEventListener('htmx:before-request', function(evt) {
const errorContainer = document.getElementById('global-error');
errorContainer.innerHTML = '';
});
</script>完整示例:请求加载状态
自动为所有 htmx 请求添加加载指示器:
<style>
.htmx-request-loading {
opacity: 0.6;
pointer-events: none;
}
</style>
<script>
document.body.addEventListener('htmx:before-request', function(evt) {
const elt = evt.detail.elt;
elt.classList.add('htmx-request-loading');
});
document.body.addEventListener('htmx:after-request', function(evt) {
const elt = evt.detail.elt;
elt.classList.remove('htmx-request-loading');
});
</script>总结
htmx 的事件系统是其声明式模型的有力补充:
- 完整生命周期:从
beforeRequest到afterSettle,覆盖请求每个阶段 - 灵活的事件处理:支持标准 DOM 事件委托、
evt.preventDefault()阻止默认行为 hx-on属性:简单场景下无需外部 JavaScript 文件- 扩展系统:通过
htmx.defineExtension()封装可复用的行为 - 实用场景:全局错误处理、认证头注入、第三方库初始化、加载状态管理
事件系统让 htmx 在保持声明式简洁的同时,拥有命令式编程的灵活性。下一篇文章将学习 历史管理与浏览器导航,理解如何让 htmx 应用支持浏览器前进/后退。
评论
Written by
AI-Writer