htmx

自定义扩展开发

By AI-Writer 8 min read

自定义扩展开发

htmx 的核心设计保持了精简,而将额外功能留给扩展系统实现。通过 htmx.defineExtension(),你可以创建可复用的自定义行为,封装特定场景的交互逻辑。本文将深入讲解扩展开发的完整技术细节。

扩展的本质

htmx 扩展是一个 JavaScript 对象,它通过注册一系列生命周期钩子来介入 htmx 的运行过程。扩展既可以全局启用(影响页面上所有 htmx 元素),也可以局部启用(只影响特定元素及其子树)。

注册扩展

javascript
htmx.defineExtension('my-extension', {
    init: function(api) {
        console.log('扩展已初始化');
    },
    onEvent: function(name, evt) {
        console.log('事件:', name);
    }
});

第一个参数是扩展的唯一标识符,第二个参数是包含钩子函数的对象。注册后,通过 hx-ext 属性启用:

html
<!-- 全局启用 -->
<body hx-ext="my-extension">

<!-- 局部启用 -->
<div hx-ext="my-extension">
    <button hx-get="/api/data">此按钮受扩展影响</button>
</div>

扩展生命周期钩子

扩展可以定义以下钩子函数,按调用顺序排列:

init(api)

扩展初始化时调用,每个扩展实例只调用一次。api 参数提供了与 htmx 内部交互的方法。

javascript
htmx.defineExtension('logger', {
    init: function(api) {
        // api 可用的方法:
        // api.getTarget(elt)       - 获取元素的目标
        // api.getAttributeValue(elt, name) - 获取属性值
        // api.addClassToElement(elt, className) - 添加 CSS 类
        // api.removeClassFromElement(elt, className) - 移除 CSS 类
        // api.swap(...)            - 手动执行交换
        console.log('Logger 扩展已加载');
    }
});

onEvent(name, evt)

最核心的钩子,htmx 触发的每个事件都会经过这里。你可以监听、修改甚至阻止事件。

javascript
htmx.defineExtension('request-logger', {
    onEvent: function(name, evt) {
        if (name === 'htmx:before-request') {
            console.log('发送请求到:', evt.detail.requestConfig.path);
        }
        if (name === 'htmx:after-request') {
            console.log('请求完成,成功:', evt.detail.successful);
        }
    }
});

注意onEvent 对所有 htmx 事件都会被调用,包括 htmx:loadhtmx:config-requesthtmx:after-swap 等。通过事件名过滤来处理你关心的事件。

transformResponse(text, xhr, elt)

在响应内容被交换到 DOM 之前调用,允许你修改服务器返回的 HTML

javascript
htmx.defineExtension('auto-link', {
    transformResponse: function(text, xhr, elt) {
        // 自动将纯文本 URL 转换为可点击链接
        return text.replace(
            /(https?:\/\/[^\s<]+)/g,
            '<a href="$1" target="_blank">$1</a>'
        );
    }
});

isInlineSwap(swapStyle)

当 htmx 需要判断某个交换策略是否为”内联交换”时调用。返回 true 表示该策略需要特殊处理。

javascript
htmx.defineExtension('morphdom', {
    isInlineSwap: function(swapStyle) {
        return swapStyle === 'morphdom';
    }
});

handleSwap(swapStyle, target, fragment, settleInfo)

最强大也最复杂的钩子,让你可以完全接管 DOM 交换逻辑。当 isInlineSwap 返回 true 时,htmx 会调用这个钩子来执行实际的交换。

javascript
htmx.defineExtension('custom-swap', {
    isInlineSwap: function(swapStyle) {
        return swapStyle === 'fade';
    },
    handleSwap: function(swapStyle, target, fragment, settleInfo) {
        if (swapStyle === 'fade') {
            // 先淡出旧内容
            target.style.transition = 'opacity 200ms';
            target.style.opacity = '0';

            setTimeout(() => {
                // 清空并插入新内容
                target.innerHTML = '';
                target.appendChild(fragment);

                // 淡入新内容
                target.style.opacity = '1';
            }, 200);

            return true; // 告诉 htmx 交换已被处理
        }
    }
});

关键handleSwap 返回 true 表示扩展已处理了交换,htmx 不再执行默认交换逻辑。返回 falseundefined 则让 htmx 继续默认处理。

完整扩展示例

示例一:自动 CSRF Token

将之前文章中提到的 CSRF 逻辑封装为正式扩展:

javascript
htmx.defineExtension('auto-csrf', {
    onEvent: function(name, evt) {
        if (name === 'htmx:config-request') {
            const meta = document.querySelector('meta[name="csrf-token"]');
            const input = document.querySelector('input[name="csrfmiddlewaretoken"]');
            const token = meta?.content || input?.value;

            if (token) {
                evt.detail.headers['X-CSRF-Token'] = token;
            }
        }
    }
});
html
<head>
    <meta name="csrf-token" content="abc123">
</head>
<body hx-ext="auto-csrf">
    <!-- 此页面所有 htmx 请求自动携带 CSRF Token -->
</body>

示例二:请求超时自动重试

javascript
htmx.defineExtension('auto-retry', {
    onEvent: function(name, evt) {
        if (name === 'htmx:send-error') {
            const elt = evt.detail.elt;
            const retryCount = parseInt(elt.getAttribute('data-retry-count') || '0');
            const maxRetries = parseInt(elt.getAttribute('data-max-retries') || '3');

            if (retryCount < maxRetries) {
                elt.setAttribute('data-retry-count', retryCount + 1);
                console.log(`请求失败,${retryCount + 1}/${maxRetries} 次重试...`);

                setTimeout(() => {
                    elt.dispatchEvent(new Event('retry-trigger'));
                }, 1000 * (retryCount + 1)); // 指数退避
            } else {
                console.error('请求失败,已达到最大重试次数');
                elt.removeAttribute('data-retry-count');
            }
        }

        if (name === 'htmx:after-request' && evt.detail.successful) {
            // 成功时重置重试计数
            evt.detail.elt.removeAttribute('data-retry-count');
        }
    }
});
html
<button hx-get="/api/unreliable"
        hx-target="#result"
        hx-ext="auto-retry"
        data-max-retries="5">
    加载(支持自动重试)
</button>

示例三:加载状态增强

比内置的 hx-indicator 更丰富的加载状态管理:

javascript
htmx.defineExtension('loading-states', {
    onEvent: function(name, evt) {
        const elt = evt.detail.elt;

        if (name === 'htmx:before-request') {
            // 为触发元素添加加载类
            elt.classList.add('is-loading');
            elt.setAttribute('aria-busy', 'true');

            // 禁用表单内的所有输入
            const form = elt.closest('form');
            if (form) {
                form.querySelectorAll('input, button, select, textarea').forEach(field => {
                    field.disabled = true;
                });
            }
        }

        if (name === 'htmx:after-request') {
            elt.classList.remove('is-loading');
            elt.removeAttribute('aria-busy');

            const form = elt.closest('form');
            if (form) {
                form.querySelectorAll('input, button, select, textarea').forEach(field => {
                    field.disabled = false;
                });
            }
        }
    }
});
css
.is-loading {
    opacity: 0.7;
    cursor: wait;
}

.is-loading::after {
    content: '';
    display: inline-block;
    width: 12px;
    height: 12px;
    margin-left: 8px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

示例四:响应状态码路由

根据 HTTP 状态码将响应路由到不同的目标元素:

javascript
htmx.defineExtension('response-targets', {
    onEvent: function(name, evt) {
        if (name !== 'htmx:before-swap') return;

        const xhr = evt.detail.xhr;
        const elt = evt.detail.elt;
        const status = xhr.status;

        // 查找状态码对应的自定义目标
        const targetSelectors = {
            '422': '[data-error-target="validation"]',
            '401': '[data-error-target="auth"]',
            '500': '[data-error-target="server"]'
        };

        const selector = targetSelectors[status];
        if (selector) {
            const newTarget = document.querySelector(selector);
            if (newTarget) {
                evt.detail.target = newTarget;
                evt.detail.shouldSwap = true;
            }
        }
    }
});
html
<form hx-post="/api/submit"
      hx-target="#success-area"
      hx-ext="response-targets">
    <input type="email" name="email" required />
    <button type="submit">提交</button>
</form>

<!-- 成功时显示在这里 -->
<div id="success-area"></div>

<!-- 验证错误时显示在这里 -->
<div data-error-target="validation" class="error-box"></div>

<!-- 认证错误时显示在这里 -->
<div data-error-target="auth" class="auth-error"></div>

多个扩展的组合

可以在同一个元素上启用多个扩展,用逗号分隔:

html
<div hx-ext="auto-csrf, loading-states, response-targets">
    <form hx-post="/api/action" hx-target="#result">
        ...
    </form>
</div>

扩展按注册顺序依次调用各自的钩子。

扩展现有内置扩展

htmx 自带了一些扩展,你可以基于它们进行二次开发。内置扩展源码在 htmx.org/dist/ext/ 目录下。

javascript
// 基于 json-enc 扩展添加自定义功能
htmx.defineExtension('json-enc-enhanced', {
    onEvent: function(name, evt) {
        // 先调用 json-enc 的逻辑
        if (name === 'htmx:config-request') {
            // 自定义 JSON 编码逻辑
            const elt = evt.detail.elt;
            if (elt.getAttribute('hx-ext')?.includes('json-enc-enhanced')) {
                // 添加自定义请求头
                evt.detail.headers['Content-Type'] = 'application/json';
            }
        }
    }
});

扩展的加载时机

扩展必须在 htmx 处理页面之前注册,通常的加载顺序:

html
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script>
    // 在 htmx 加载后、处理 DOM 前注册扩展
    htmx.defineExtension('my-ext', { ... });
</script>

如果扩展定义在外部文件中,确保在 DOMContentLoaded 之前加载:

html
<script src="htmx.min.js"></script>
<script src="my-extension.js"></script>
<!-- htmx 在 DOMContentLoaded 时自动处理页面 -->

扩展的调试技巧

javascript
// 开发模式下打印所有事件
htmx.defineExtension('debug', {
    onEvent: function(name, evt) {
        if (console.debug) {
            console.debug(`[htmx] ${name}`, evt.detail);
        }
    }
});
html
<!-- 开发环境启用 -->
<body hx-ext="debug">

总结

htmx 的扩展系统提供了强大而灵活的定制能力:

  • init(api):扩展初始化,获取内部 API
  • onEvent(name, evt):监听所有 htmx 事件,是大多数扩展的核心
  • transformResponse(text, xhr, elt):修改响应内容
  • isInlineSwap(swapStyle) & handleSwap(...):完全自定义交换逻辑
  • 全局/局部启用hx-extbody 上全局启用,在任意元素上局部启用
  • 组合使用:多个扩展可以同时作用于同一个元素

扩展开发让你能够在不修改 htmx 核心代码的情况下,为项目添加任何定制行为。下一篇文章将学习 htmx 与现代前端框架混用

#htmx #extensions #advanced #api

评论

A

Written by

AI-Writer

Related Articles

htmx
#9

与后端框架集成

学习 htmx 与 Django、Flask、FastAPI、Laravel 等主流后端框架的配合模式,掌握 HX-Request 头识别、模板片段组织等核心集成技巧

Read More
htmx
#7

事件系统与扩展

深入理解 htmx 的自定义事件体系,掌握请求生命周期事件的拦截与处理,以及扩展机制的基础用法

Read More
htmx
#4

触发器与修饰符

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

Read More