HTMX возвращает веб-разработку к простой идее: сервер отдаёт HTML, браузер его показывает, а интерактивность добавляется атрибутами в разметке. Вместо того чтобы дублировать шаблоны на клиенте и строить JSON-API, вы возвращаете фрагмент страницы и указываете, куда его вставить. Для форм, списков, фильтров и пагинации этого хватает почти всегда. Разберём рабочие приёмы, серверные ответы и подводные камни.

Как это работает

Атрибут hx-get или hx-post превращает обычный элемент в источник запроса. Сервер отвечает HTML-фрагментом, а HTMX подменяет содержимое цели согласно hx-target и hx-swap. Никакого виртуального DOM и гидратации нет: браузер просто вставляет разметку.

Ключевое следствие — сервер остаётся единственным источником правды. Шаблонизатор один, состояние живёт в базе, а не в сторе на клиенте. Это упрощает отладку: в сетевой панели видно ровно тот HTML, который получил пользователь.

Подключение

Библиотека распространяется одним файлом. В продакшене лучше положить его рядом со статикой и зафиксировать точную версию, чем зависеть от внешнего CDN.

<script src="/static/js/htmx.min.js" defer></script>

<body hx-boost="true">
  <a href="/catalog">Каталог</a>
</body>

hx-boost перехватывает обычные ссылки и формы, подменяет только содержимое body и обновляет адресную строку. Страница остаётся рабочей без JavaScript, а с ним — грузится без полной перезагрузки. Это самый дешёвый способ ускорить существующий сайт.

Базовые атрибуты

  • hx-get, hx-post, hx-put, hx-delete — метод и адрес запроса.
  • hx-target — селектор цели; поддерживает this, closest, next, find.
  • hx-swap — innerHTML по умолчанию, а также outerHTML, beforeend, afterbegin, delete, none.
  • hx-trigger — событие и модификаторы: changed, delay:400ms, throttle, from:, once.
  • hx-indicator и hx-disabled-elt — индикатор загрузки и блокировка кнопки.

Живой поиск собирается из трёх атрибутов и не требует ни одной строки JavaScript. Задержка delay гасит лишние запросы при быстром наборе.

<input type="search" name="q"
       hx-get="/search"
       hx-trigger="input changed delay:400ms, search"
       hx-target="#results"
       hx-push-url="true"
       hx-indicator="#search-spin">
<span id="search-spin" class="htmx-indicator">Ищем…</span>
<div id="results" aria-live="polite"></div>

Формы и валидация

Форма отправляется асинхронно, а ответ вставляется в указанный контейнер. Атрибут hx-disabled-elt защищает от двойной отправки, пока идёт запрос.

<form hx-post="/subscribe"
      hx-target="#subscribe-result"
      hx-swap="innerHTML"
      hx-disabled-elt="button"
      hx-indicator="#spin">
  <label for="email">Почта</label>
  <input id="email" type="email" name="email" required>
  <button type="submit">Подписаться</button>
  <span id="spin" class="htmx-indicator">Отправляем…</span>
</form>
<div id="subscribe-result" aria-live="polite"></div>

Важная деталь: HTMX 2 по умолчанию не подменяет содержимое при ответах 4xx и 5xx. Для ошибок валидации либо возвращайте 200 с фрагментом сообщения, либо настраивайте htmx.config.responseHandling, добавляя код 422 в список подменяемых. Молчаливый отказ формы без объяснения — типичная ошибка новичков.

Серверная часть

На сервере нет ничего специфического: тот же шаблонизатор, только для фрагмента. Пример на Flask показывает возврат частичного шаблона и заголовок-событие.

from flask import Flask, request, render_template, make_response

app = Flask(__name__)

@app.post("/subscribe")
def subscribe():
    email = (request.form.get("email") or "").strip()
    if "@" not in email:
        return render_template("partials/subscribe_error.html"), 200
    save_subscriber(email)
    resp = make_response(render_template("partials/subscribe_ok.html"))
    resp.headers["HX-Trigger"] = "subscriber-added"
    return resp

В Django аналог — render_to_string и HttpResponse; пакет django-htmx добавляет удобные хелперы и определение HTMX-запроса по заголовку HX-Request. В Go достаточно html/template и ExecuteTemplate для именованного блока.

Out-of-band обновления

Иногда один ответ должен изменить несколько участков страницы: добавить товар в корзину, обновить счётчик и показать уведомление. Для этого служит hx-swap-oob: элемент с этим атрибутом заменяет одноимённый элемент на странице, а не вставляется в цель.

<div id="cart-count" hx-swap-oob="true">3</div>

<div id="cart-list">
  <p>Товар добавлен</p>
</div>

<div id="toasts" hx-swap-oob="beforeend:#toasts">
  <div class="toast">Сохранено</div>
</div>

Это снимает необходимость в глобальном состоянии на клиенте. Ответ остаётся обычным HTML, а сервер сам решает, какие блоки обновить.

История и навигация

hx-push-url="true" записывает адрес в историю, hx-replace-url заменяет текущую запись. Чтобы кнопка «назад» работала предсказуемо, укажите контейнер истории через hx-history-elt — по умолчанию им считается body.

  • Для пагинации: кнопка с hx-get="/page/2" и hx-target="#list" hx-swap="afterend".
  • Для бесконечной прокрутки: hx-trigger="revealed" на последнем элементе.
  • Для модальных окон: ответ с hx-swap="beforeend" в контейнер оверлея.
  • Для сохранения состояния виджета: hx-preserve="true".

События и заголовки

Сервер может управлять поведением клиента заголовками: HX-Redirect уводит на другую страницу, HX-Refresh перезагружает её, HX-Retarget меняет цель, HX-Trigger поднимает пользовательское событие. На клиенте их удобно слушать обычным addEventListener.

document.body.addEventListener("htmx:responseError", (event) => {
  console.warn("Ошибка", event.detail.xhr.status, event.detail.pathInfo.requestPath);
});

document.body.addEventListener("subscriber-added", () => {
  document.querySelector("#counter")?.classList.add("bump");
});

Полезные события: htmx:beforeRequest, htmx:afterSwap, htmx:sendError, htmx:targetError. Последнее почти всегда означает опечатку в селекторе hx-target.

Безопасность

  • Экранируйте вывод. HTMX вставляет HTML как есть, поэтому пользовательский текст без экранирования превращается в XSS. Автоэкранирование шаблонизатора должно быть включено.
  • CSRF. Токен передавайте через hx-headers на body или скрытым полем формы; сервер обязан проверять его для всех изменяющих запросов.
  • Не доверяйте клиенту. hx-vals и hx-headers видны и редактируются, поэтому цены и роли проверяются на сервере.
  • CSP. hx-on выполняет код из атрибута, поэтому при строгой политике его лучше не использовать и выносить обработчики в отдельный файл.

Когда HTMX не подходит

Сложные редакторы, drag-and-drop с десятками правил, офлайн-режим и мгновенный отклик без сети требуют клиентского состояния. В таких местах HTMX сочетают с Alpine.js или точечным JavaScript: HTMX отвечает за загрузку фрагментов, а локальную логику берёт на себя небольшой скрипт. Попытка выжать из атрибутов всё подряд приводит к каше из hx-on и скрытых зависимостей.

Отладка

Первым делом включите htmx.logAll() в консоли — он покажет жизненный цикл каждого запроса. Затем проверьте в сетевой панели, что уходит заголовок HX-Request: true и приходит HTML, а не JSON. Если ответ 302, HTMX прозрачно пройдёт по редиректу и вставит финальную страницу, что не всегда ожидаемо; для смены адреса используйте HX-Redirect.

htmx.logAll();
htmx.config.selfRequestsOnly = true;
fetch("/search?q=linux", {headers: {"HX-Request": "true"}});

Итог

HTMX выигрывает там, где интерфейс — это набор форм, списков и фильтров, а команда не хочет поддерживать второй фронтенд. Начните с hx-boost, затем переведите на атрибуты одну форму и один список, проверьте коды ответов и экранирование. Если приложение требует богатого клиентского состояния, добавьте точечный JavaScript, но не тащите фреймворк только ради модной архитектуры.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте