htmx

事件系统与扩展

By AI-Writer 7 min read

事件系统与扩展

htmx 的声明式属性覆盖了大部分交互需求,但在某些场景下你需要更精细的控制:在请求发送前添加认证头、在交换完成后执行初始化代码、拦截错误做统一处理。htmx 的自定义事件系统让你可以介入请求生命周期的每个阶段。

事件生命周期概览

htmx 在 AJAX 请求的完整生命周期中触发一系列事件,按时间顺序排列如下:

版本注意:htmx 2.x 中所有事件名统一使用 kebab-case(短横线连接),如 htmx:after-swap。如果你从 htmx 1.x 迁移而来,需注意 1.x 使用的 camelCase(如 htmx:afterSwap)在 2.x 中已不再支持。

plaintext
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

在请求即将发送前触发。可以用来阻止请求或修改请求参数。

javascript
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 交换。

javascript
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 交换前触发,是修改响应内容的最后机会。

javascript
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 的操作,如重新绑定第三方库、初始化图表等。

javascript
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 过渡和动画完成后再触发。

javascript
document.body.addEventListener('htmx:after-settle', function(evt) {
    // 执行需要等待动画完成的操作
    scrollToElement(evt.detail.target);
});

错误事件

javascript
// 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 事件支持标准的事件委托模式,你可以精确监听特定元素上的事件。

按元素类型过滤

javascript
// 只监听表单提交相关的事件
document.querySelectorAll('form').forEach(form => {
    form.addEventListener('htmx:after-request', function(evt) {
        if (evt.detail.successful) {
            // 表单提交成功后的操作
            form.reset();
        }
    });
});

使用事件委托

javascript
// 监听整个文档,但只在特定条件下处理
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。

基础用法

html
<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-requestthis 指向触发请求的元素(即该表单)。

多事件绑定

html
<button hx-get="/api/data"
        hx-on::before-request="this.disabled = true"
        hx-on::after-request="this.disabled = false"
        hx-target="#result">
    加载数据
</button>

访问事件详情

html
<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 自带了一些有用的扩展:

html
<!-- 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-encapplication/json 编码发送表单数据
path-deps当指定路径的请求成功时自动刷新当前元素
loading-states更丰富的加载状态管理
class-tools延迟添加/移除 CSS 类
response-targets根据响应状态码路由到不同目标

自定义扩展

以下是一个简单的自定义扩展示例,它自动为所有请求添加一个 CSRF Token:

javascript
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;
            }
        }
    }
});

启用扩展:

html
<meta name="csrf-token" content="abc123...">
<body hx-ext="csrf-token">
    <!-- 此 body 内所有 htmx 请求都会自动携带 CSRF Token -->
</body>

扩展生命周期钩子

自定义扩展可以定义以下钩子:

钩子触发时机
init扩展初始化时
onEvent任何 htmx 事件发生时
transformResponse响应到达后、交换前
isInlineSwap判断是否为内联交换策略
handleSwap自定义交换逻辑

完整示例:全局错误处理

实现一个统一的全局错误提示系统:

html
<!-- 页面顶部的错误容器 -->
<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 请求添加加载指示器:

html
<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 的事件系统是其声明式模型的有力补充:

  • 完整生命周期:从 beforeRequestafterSettle,覆盖请求每个阶段
  • 灵活的事件处理:支持标准 DOM 事件委托、evt.preventDefault() 阻止默认行为
  • hx-on 属性:简单场景下无需外部 JavaScript 文件
  • 扩展系统:通过 htmx.defineExtension() 封装可复用的行为
  • 实用场景:全局错误处理、认证头注入、第三方库初始化、加载状态管理

事件系统让 htmx 在保持声明式简洁的同时,拥有命令式编程的灵活性。下一篇文章将学习 历史管理与浏览器导航,理解如何让 htmx 应用支持浏览器前进/后退。

#htmx #events #lifecycle #extensions

评论

A

Written by

AI-Writer

Related Articles

htmx
#10

CSS 过渡与动画

学习利用 htmx 的 swap/settle 修饰符与 CSS 过渡实现平滑的界面变化,掌握常用的淡入、滑入、缩放等动画模式

Read More
htmx
#4

触发器与修饰符

深入掌握 hx-trigger 的完整语法体系,学习事件类型、过滤器、延迟、轮询、可见性触发及各种修饰符的使用场景

Read More