Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи?

Написать
Войти
Дайджесты
Иллюстрация к воссозданию библиотеки htmx на чистом JavaScript

Воссоздание htmx с нуля: как устроена архитектура популярной JS-библиотеки

Разбор внутреннего устройства библиотеки htmx через написание лаконичного аналога на чистом JavaScript. Перехват браузерных событий, работа с DOM MutationObserver, AJAX-подгрузка и стратегии подстановки HTML показывают, как подходить к гибридной веб-разработке без тяжелых SPA-фреймворков.

Воссоздание htmx с нуля: как устроена архитектура популярной JS-библиотеки

Популярность библиотеки htmx в современном веб-девелопменте вызвала новый виток дискуссий вокруг архитектуры веб-приложений. На фоне усложнения клиентских SPA-фреймворков (Single Page Applications) с их реактивными сторами, виртуальным DOM, громоздкими сборочными цепочками и передачей данных в формате JSON, htmx возвращает разработчиков к базовым принципам REST и гипермедиа (HATEOAS). Основная идея заключается в том, чтобы расширить стандартный HTML декларативными атрибутами, позволяя любому элементу отправлять AJAX-запросы и подставлять полученные с сервера HTML-фрагменты прямо в страницу.

Понимание того, что под капотом htmx нет «магии», а лишь грамотное применение штатных возможностей современного браузерного JavaScript API (Vanilla JS), помогает инженерам точнее оценивать архитектурные компромиссы. Написание собственного минимального аналога позволяет заглянуть во внутреннее устройство библиотеки и разобрать ключевые механизмы: делегирование событий, сетевое взаимодействие через fetch, работу с FormData, отслеживание изменений DOM с помощью MutationObserver и подстановку узлов.

Четыре столпа гипермедиа-контракта htmx

Чтобы воссоздать базовый функционал библиотеки, необходимо сначала зафиксировать четыре независимых шага, составляющих ее жизненный цикл:

  1. Декларативное намерение в HTML-разметке: Элемент содержит кастомные атрибуты, задающие HTTP-метод, URL, целевой селектор для замены и стратегию обновления. Например, hx-get="/api/items", hx-target="#list", hx-swap="innerHTML".
  2. Перехват пользовательских событий: Клиентский скрипт отлавливает клики по кнопкам или отправку форм, предотвращая стандартное поведение браузера по умолчанию (event.preventDefault()).
  3. Сетевой запрос и получение фрагмента: Браузер выполняет асинхронный запрос к серверу и принимает от него готовую верстку в формате HTML, а не структурированные данные JSON.
  4. Обновление DOM-дерева: Полученный результат встраивается в указанный элемент-цель согласно выбранному правилу замены.

В такой модели сервер остается единственным источником истины для бизнес-логики и визуального представления, а клиент выполняет роль универсального транспорта и точки локального применения изменений.

Пошаговая сборка минимального ядра на Vanilla JS

Для написания учебного аналога достаточно воспользоваться механизмом делегирования событий (Event Delegation), чтобы не навешивать индивидуальные слушатели на каждый новый элемент страницы.

Шаг 1. Глобальный перехват событий

Создается единый слушатель событий click и submit на уровне всего документа document:

document.addEventListener('click', async (event) => {
  // Находим ближайший элемент с атрибутом hx-get или hx-post
  const triggerEl = event.target.closest('[hx-get], [hx-post]');
  if (!triggerEl) return;

  event.preventDefault();
  await processHtmxElement(triggerEl);
});

Использование метода Element.closest() критически важно: если пользователь кликает по иконке или внутреннему тегу <span>, находящемуся внутри кнопки, скрипт корректно поднимается вверх по дереву и находит родительский элемент с атрибутом hx-get.

Шаг 2. Формирование запроса и отправка данных

Внутри функции processHtmxElement считываются управляющие атрибуты. Если инициатором выступает HTML-форма или элемент внутри нее, данные собираются через объект FormData:

async function processHtmxElement(element) {
  const method = element.hasAttribute('hx-post') ? 'POST' : 'GET';
  const url = element.getAttribute('hx-get') || element.getAttribute('hx-post');
  const targetSelector = element.getAttribute('hx-target');
  const swapStrategy = element.getAttribute('hx-swap') || 'innerHTML';

  const targetEl = targetSelector ? document.querySelector(targetSelector) : element;
  if (!targetEl) return;

  try {
    const options = { method, headers: { 'HX-Request': 'true' } };
    if (method === 'POST' && element.tagName === 'FORM') {
      options.body = new FormData(element);
    }

    const response = await fetch(url, options);
    if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);

    const htmlSnippet = await response.text();
    applySwap(targetEl, htmlSnippet, swapStrategy);
  } catch (error) {
    console.error('htmx execution failed:', error);
  }
}

Заголовок HX-Request: true позволяет серверному бэкенду определить, что запрос пришел от AJAX-клиента, и вернуть не полную HTML-страницу с тегами <html> и <body>, а только локальный обновляемый фрагмент.

Шаг 3. Реализация стратегий подстановки (Swap Strategies)

В оригинальной библиотеке htmx поддерживается несколько режимов вставки. Функция applySwap демонстрирует работу базовых вариантов:

function applySwap(target, html, strategy) {
  switch (strategy) {
    case 'outerHTML':
      target.outerHTML = html;
      break;
    case 'beforebegin':
      target.insertAdjacentHTML('beforebegin', html);
      break;
    case 'afterend':
      target.insertAdjacentHTML('afterend', html);
      break;
    case 'beforeend':
      target.insertAdjacentHTML('beforeend', html);
      break;
    case 'innerHTML':
    default:
      target.innerHTML = html;
      break;
  }
}

Выбор правильной стратегии зависит от решаемой задачи:

  • innerHTML меняет только содержимое контейнера, сохраняя сам целевой элемент и его идентификатор id.
  • outerHTML полностью замещает целевой узел новым HTML-кодом, что удобно для удаления или кардинальной смены компонента.
  • beforeend и afterbegin используются для добавления новых элементов в конец или начало списка (например, в ленте сообщений или таблице).

Границы применимости: от учебного скрипта к Production

Простой скрипт из 50 строк наглядно объясняет базовую концепцию, однако для реального использования в коммерческих проектах оригинальная htmx содержит огромный пласт защитных механизмов и дополнительных функций:

1. Безопасность и CSRF-защита

При выполнении мутирующих запросов (POST, PUT, DELETE) обязательна передача CSRF-токенов в заголовках. Кроме того, динамическая вставка ответа через innerHTML требует полной уверенности в надежности сервера: если HTML содержит пользовательский ввод без экранирования, приложение становится уязвимым для XSS-атак.

2. Обработка состязательности запросов (Race Conditions)

Если пользователь быстро кликает по кнопке несколько раз подряд, браузер отправляет несколько параллельных запросов fetch. Без использования AbortController для отмены предыдущих сетевых вызовов более старый ответ может прийти позже и перезаписать свежие данные в DOM.

3. Доступность (Accessibility) и фокус

При удалении узла через outerHTML пользовательский фокус клавиатуры может «потеряться», сломав навигацию по клавише Tab. Продакшен-библиотека отслеживает активные элементы и перемещает фокус на новые узлы, а также обновляет атрибуты ARIA-live для скринридеров.

4. Управление историей браузера

Официальная библиотека htmx умеет взаимодействовать с window.history.pushState, сохраняя снимки DOM-дерева в localStorage и позволяя пользователю корректно нажимать кнопки «Назад» и «Вперед» в браузере.

Сравнительный анализ: htmx против классических SPA

Критерий сравненияКлассический SPA (React / Vue / Angular)Гипермедиа-подход (htmx / Alpine.js)
Формат обмена даннымиJSON (требуется парсинг и сборка моделей)Готовый HTML-фрагмент с сервера
Управление состояниемСложные клиенты: Redux, Zustand, PiniaСостояние хранит сервер и разметка DOM
Объем JS-бандлаСотни килобайт или мегабайты кодаОколо 14 КБ (gzipped) для htmx
Семантика разработкиВысокий порог входа, сборщики, TS-интерфейсыДекларативные HTML-атрибуты, быстрый старт
Лучшие сценарии примененияОфлайн-приложения, редакторы, личные кабинетыCRUD-системы, админки, корпоративные порталы

Инженерные выводы

htmx — это не просто библиотека, а демонстрация того, насколько мощными стали веб-стандарты. Использование декларативного HTML в сочетании со штатным fetch и DOM API позволяет закрывать до 80% стандартных задач веб-разработки без раздувания клиентского бандла. Знание базовых механизмов перехвата событий и правил подстановки узлов помогает разработчикам осознанно выбирать инструмент под конкретный проект, не перегружая простые интерфейсы лишними слоями абстракций.