Введение в htmx

Источник: «An Introduction to htmx, the HTML-focused Dynamic UI Library»
Современные веб-пользователи ожидают от одностраничных приложений (SPA) плавных и динамичных эффектов. Однако для создания SPA часто используются сложные фреймворки, такие, как React и Angular, которые могут быть сложны для изучения и работы с ними. Появилась htmx — библиотека, позволяющая по-новому взглянуть на создание динамических веб-приложений, используя такие возможности, как Ajax и CSS-переходы, непосредственно в HTML.

В этом руководстве мы рассмотрим возможности htmx, то, как он упрощает динамическую веб-разработку, и как можно использовать его потенциал для улучшения процесса веб-разработки.

Что такое htmx и как он работает

При создании интерактивных веб-приложений у разработчиков традиционно есть два основных варианта, каждый из которых имеет свои компромиссы. С одной стороны, существуют многостраничные приложения (MPA), обновляющие всю страницу при каждом взаимодействии с ней пользователя. Такой подход гарантирует, что сервер контролирует состояние приложения, а клиент достоверно представляет его. Однако полная перезагрузка страницы может привести к медленной и неуклюжей работе пользователя.

С другой стороны, существуют одностраничные приложения (SPA), которые для управления состоянием приложения полагаются на JavaScript, выполняемый в браузере. Они взаимодействуют с сервером с помощью вызовов API, возвращающих данные, часто в формате JSON. Затем SPA использует эти данные для обновления пользовательского интерфейса без обновления страницы, что обеспечивает более плавное взаимодействие с пользователем, в чем-то схожее с нативным настольным или мобильным приложением. Однако и этот подход не идеален. Вычислительные затраты обычно выше из-за значительной обработки данных на стороне клиента, время начальной загрузки может быть медленнее, поскольку клиенту приходится загружать и разбирать большие пакеты JavaScript перед рендерингом первой страницы, а настройка среды разработки часто связана со сложными инструментами и рабочими процессами сборки.

htmx представляет собой нечто среднее между этими двумя крайностями. Он обеспечивает преимущества SPA, не требуя полной перезагрузки страницы, и в то же время сохраняет простоту MPA на стороне сервера. В этой модели вместо возврата данных, которые клиент должен интерпретировать и отображать, сервер отвечает HTML-фрагментами. htmx просто меняет эти фрагменты местами для обновления пользовательского интерфейса.

Такой подход упрощает процесс разработки за счёт минимизации сложностей на стороне клиента, а также значительного использования JavaScript, характерного для SPA. Он не требует сложной настройки и обеспечивает плавное и отзывчивое взаимодействие с пользователем.

Установка htmx

Существует несколько способов включить htmx в свой проект. Можно загрузить его непосредственно со страницы проекта на GitHub, а если вы работаете с Node.js, то можете установить его через npm с помощью команды:

npm install htmx.org

Однако самый простой способ, который мы будем использовать в данном руководстве, — это включение его через сеть доставки контента (CDN). Это позволяет начать использовать htmx без каких-либо настроек или процесса установки. Просто включите следующий тег script в свой HTML-файл:

<script src="https://unpkg.com/htmx.org@1.9.4"></script>

Данный тег скрипта указывает на версию 1.9.4, но вы можете заменить "1.9.4" на последнюю версию, если доступна более новая.

htmx очень лёгкий, его минифицированная и gzipped версия весит ~14 КБ. Он не имеет зависимостей и совместим со всеми основными браузерами, включая IE11.

После добавления htmx в проект необходимо проверить правильность его работы. Проверить это можно с помощью следующего простого примера:

<button
hx-get="https://v2.jokeapi.dev/joke/Any?format=txt&safe-mode&type=single"
hx-target="#joke-container"
>

Make me laugh!
</button>

<p id="joke-container">Click the button to load a joke...</p>

При нажатии на кнопку, если htmx работает корректно, он отправит GET-запрос к Joke API и заменит содержимое тега <p> ответом сервера.

Ajax-запросы: htmx-подход

Одним из основных преимуществ htmx является то, что он даёт разработчикам возможность отправлять Ajax-запросы непосредственно из HTML-элементов, используя набор отдельных атрибутов. Каждый атрибут представляет собой отдельный метод HTTP-запроса:

Эти атрибуты принимают URL, на который будет отправлен Ajax-запрос. По умолчанию Ajax-запросы вызываются "естественным" событием HTML-элемента (например, кликом в случае кнопки или событием изменения в случае поля ввода).

Рассмотрим следующее:

<button hx-get="/api/resource">Load Data</button>

В приведённом примере элементу button присвоен атрибут hx-get. После нажатия на кнопку выполняется GET-запрос к URL /api/resource.

Что произойдёт, когда данные вернутся с сервера? По умолчанию htmx вводит этот ответ непосредственно в инициирующий элемент — в нашем примере в button. Однако htmx не ограничивается таким поведением и предоставляет возможность указывать различные элементы в качестве адресата ответных данных. Подробнее об этой возможности мы поговорим в следующих разделах.

Триггерные запросы с помощью htmx

htmx инициирует Ajax-запрос в ответ на определённые события, происходящие с определёнными элементами:

Продемонстрируем это на примере примера с анекдотами, приведённом выше, чтобы пользователь мог искать анекдоты, содержащие определённое слово:

<label>Keyword:
<input
type="text"
placeholder="Enter a keyword..."
hx-get="https://v2.jokeapi.dev/joke/Any?format=txt&safe-mode"
hx-target="#joke-container"
name="contains"
/>

</label>

<p id="joke-container">Results will appear here</p>

Для запуска поиска необходимо вызвать событие change. Для элементов <input> это происходит, когда элемент теряет фокус после изменения своего значения. Итак, наберите в поле что-нибудь (например, "bar"), кликните в другом месте страницы, и в элементе <p> должна появиться шутка.

Это хорошо, но обычно пользователи хотят, чтобы результаты поиска обновлялись по мере ввода. Для этого мы можем добавить атрибут htmx trigger к элементу <input>:

<input
...
hx-trigger="keyup"
/>

Теперь результаты обновляются немедленно. Это хорошо, но возникает новая проблема: мы выполняем вызов API при каждом нажатии клавиши. Чтобы избежать этого, можно использовать модификатор для изменения поведения триггера. htmx предлагает следующее:

В данном случае, похоже, delay — это то, что нам нужно:

<input
...
hx-trigger="keyup delay:500ms"
/>

И теперь при вводе в поле (попробуйте ввести более длинное слово, например "developer") запрос выполняется только после паузы или окончания ввода.

See the Pen

Как видите, это позволяет реализовать паттерн активного окна поиска всего в нескольких строках кода на стороне клиента.

Индикаторы запроса

В веб-разработке обратная связь с пользователем очень важна, особенно когда речь идёт о действиях, выполнение которых может занять заметное время, например, о выполнении сетевого запроса. Для обеспечения такой обратной связи обычно используются индикаторы запросов — визуальные подсказки, указывающие на то, что операция находится в процессе выполнения.

В htmx реализована поддержка индикаторов запросов, что позволяет обеспечить обратную связь с пользователями. Для указания элемента, который будет служить индикатором запроса, используется класс hx-indicator. Непрозрачность любого элемента с этим классом по умолчанию равна 0, что делает его невидимым, но присутствующим в DOM.

Когда htmx выполняет Ajax-запрос, он применяет класс htmx-request к инициирующему элементу. Класс htmx-request заставит этот элемент или любой дочерний элемент с классом htmx-indicator перейти к непрозрачности 1.

Например, рассмотрим элемент, у которого в качестве индикатора запроса установлен загрузочный спиннер:

<button hx-get="/api/data">
Load data
<img class="htmx-indicator" src="/spinner.gif" alt="Loading spinner">
</button>

Когда button с атрибутом hx-get кликнута и начинается запрос, кнопка получает класс htmx-request. Это приводит к тому, что изображение будет отображаться до тех пор, пока запрос не завершится и класс не будет удалён.

Также можно использовать атрибут htmx-indicator для указания того, какой элемент должен получить класс htmx-request.

Продемонстрируем это на примере Joke API:

<input
...
hx-indicator=".loader"
/>


<span class="loader htmx-indicator"></span>

Примечание: мы можем взять некоторые CSS-стили для спиннера из CSS Loaders & Spinners. Там есть множество вариантов; просто кликните на одном из них, чтобы получить HTML и CSS.

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

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

Или, просто для развлечения (то есть не делайте этого на реальном приложении), мы можем настроить htmx на имитацию некоторой сетевой задержки:

function sleep(milliseconds) {
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < milliseconds);
}

document.body.addEventListener('htmx:afterOnLoad', () => {
sleep(2000);
});

При этом используется система событий htmx, которую мы можем использовать для модификации и улучшения его поведения. Здесь мы используем событие htmx:afterOnLoad, которое запускается после завершения Ajax onload. Я также использую функцию sleep из статьи на эту же тему.

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

See the Pen

Таргетинг элементов и замена контента

В некоторых случаях требуется обновить элемент, отличный от того, который инициировал запрос. htmx позволяет нацеливать определённые элементы на Ajax-ответ с помощью атрибута hx-target. Этот атрибут может принимать CSS-селектор, и htmx будет использовать его для поиска элемента (элементов) для обновления. Например, если у нас есть форма, публикующая новый комментарий в нашем блоге, мы можем захотеть добавить новый комментарий в список комментариев, а не обновлять саму форму.

Мы видели это в первом примере:

<button
hx-get="https://v2.jokeapi.dev/joke/Any?format=txt&safe-mode&type=single"
hx-target="#joke-container"
>

Make me laugh!
</button>

Вместо того чтобы кнопка заменяла собственное содержимое, атрибут hx-target указывает, что ответ должен заменить содержимое элемента с идентификатором "joke-container".

Расширенные CSS селекторы

htmx также предлагает несколько более сложных способов выбора элементов, в которые должно быть загружено содержимое. К ним относятся this, close, next, previous и find.

Ссылаясь на наш предыдущий пример, мы также можем написать hx-target="next p", чтобы не указывать идентификатор.

Замена содержимого

По умолчанию htmx заменяет содержимое целевого элемента ответом Ajax. Но что если мы хотим добавить новое содержимое вместо замены? Здесь на помощь приходит атрибут hx-swap. Этот атрибут позволяет указать, как новое содержимое должно быть вставлено в целевой элемент. Возможные значения: outerHTML, innerHTML, beforebegin, afterbegin, beforeend и afterend. Например, при использовании hx-swap="beforeend" новое содержимое будет добавлено в конец целевого элемента, что идеально подходит для нашего сценария нового комментария.

CSS-переходы с помощью htmx

CSS-переходы позволяют плавно изменять стиль элемента от одного состояния к другому без использования JavaScript. Эти переходы могут быть как простыми, например, изменение цвета, так и сложными, например, полная 3D-трансформация.

htmx позволяет легко использовать CSS Переходы в нашем коде: все, что нам нужно сделать, это поддерживать постоянный идентификатор элемента в HTTP-запросах.

Рассмотрим этот HTML-контент:

<button hx-get="/new-content" hx-target="#content">
Fetch Data
</button>

<div id="content">
Initial Content
</div>

После выполнения htmx Ajax-запроса к /new-content сервер возвращает следующее:

<div id="content" class="fadeIn">
New Content
</div>

Несмотря на изменение содержимого, <div> сохраняет тот же идентификатор. Однако к новому содержимому был добавлен класс fadeIn.

Теперь мы можем создать CSS-переход, плавно переходящий из начального состояния в новое:

.fadeIn {
animation: fadeIn 2.5s;
}

@keyframes fadeIn {
0% {opacity: 0;}
100% {opacity: 1;}
}

Когда htmx загружает новое содержимое, он запускает CSS-переход, создавая плавный визуальный переход к обновлённому состоянию.

Использование View Transitions API

Новый View Transitions API предоставляет возможность анимировать различные состояния элемента DOM. В отличие от CSS-переходов, которые связаны с изменением CSS-свойств элемента, переходы вида анимируют изменения содержимого элемента.

View Transitions API - это новая, экспериментальная функция, находящаяся в стадии активной разработки. На момент написания статьи этот API реализован в Chrome 111+, в будущем ожидается добавление поддержки в других браузерах (поддержку можно проверить на сайте caniuse). htmx предоставляет интерфейс для работы с View Transitions API и возвращается к механизму без переходов в тех браузерах, где этот API недоступен.

В htmx существует несколько способов использования View Transitions API:

Переходы представления могут быть определены и настроены с помощью CSS. Вот пример перехода "bounce", при котором старое содержимое вылетает, а новое вплывает:

@keyframes bounce-in {
0% { transform: scale(0.1); opacity: 0; }
60% { transform: scale(1.2); opacity: 1; }
100% { transform: scale(1); }
}

@keyframes bounce-out {
0% { transform: scale(1); }
45% { transform: scale(1.3); opacity: 1; }
100% { transform: scale(0); opacity: 0; }
}

.bounce-it {
view-transition-name: bounce-it;
}

::view-transition-old(bounce-it) {
animation: 600ms cubic-bezier(0.4, 0, 0.2, 1) both bounce-out;
}

::view-transition-new(bounce-it) {
animation: 600ms cubic-bezier(0.4, 0, 0.2, 1) both bounce-in;
}

В коде htmx мы используем опцию transition:true в атрибуте hx-swap и применяем класс bounce-it к содержимому, которое хотим анимировать:

<button
hx-get="https://v2.jokeapi.dev/joke/Any?format=txt&safe-mode"
hx-swap="innerHTML transition:true"
hx-target="#joke-container"
>

Load new joke
</button>

<div id="joke-container" class="bounce-it">
<p>Initial joke content goes here...</p>
</div>

В данном примере при обновлении содержимого <div> старое содержимое вылетает, а новое вплывает, создавая приятный и привлекательный визуальный эффект.

See the Pen

Следует иметь в виду, что в настоящее время эта демонстрация работает только в браузерах на базе Chromium.

Валидация форм

htmx хорошо интегрируется с HTML5 Validation API и предотвращает отправку запросов формы, если вводимые пользователем данные не прошли валидацию.

Например, когда пользователь кликает на кнопку Submit, POST-запрос будет отправлен на /contact только в том случае, если в поле ввода содержится правильный адрес электронной почты:

<form hx-post="/contact">
<label>Email:
<input type="email" name="email" required>
</label>
<button>Submit</button>
</form>

Если бы мы хотели сделать ещё один шаг вперёд, мы могли бы добавить некоторую проверку сервера, чтобы гарантировать, что принимаются только адреса gmail.com:

<form hx-post="/contact">
<div hx-target="this" hx-swap="outerHTML">
<label>Email:
<input type="email" name="email" required hx-post="/contact/email">
</label>
</div>
<button>Submit</button>
</form>

В данном случае мы добавили родительский элемент (div#wrapper), который объявляет себя получателем запроса (с помощью ключевого слова this) и использует стратегию подстановки outerHTML. Это означает, что весь <div> будет заменён ответом сервера, даже если он не является фактическим элементом, вызвавшим запрос.

Мы также добавили hx-post="/contact/email" к полю ввода, это означает, что когда это поле будет терять фокус, оно будет отправлять POST-запрос к конечной точке /contact/email. Этот запрос будет содержать значение нашего поля.

На сервере (по адресу /contact/email) мы могли бы выполнить проверку с помощью PHP:

<?php
// Получаем сообщения электронной почты из формы отправки
$email = $_POST['email'];

// Проверяем, что домен - это "gmail.com", используя regex
$pattern = "/@gmail\.com$/i"; // Нечувствительное к регистру совпадение для "@gmail.com" в конце адреса электронной почты
$error = !preg_match($pattern, $email);

// Дезинфекция электронной почты во избежание XSS
$sanitizedEmail = htmlspecialchars($email, ENT_QUOTES, 'UTF-8');

// Создание сообщения об ошибке в случае её возникновения
$errorMessage = $error ? '<div class="error-message">Only Gmail addresses accepted!</div>' : '';

// Конструируем HTML-шаблон с условным сообщением об ошибке
$template = <<<EOT
<div hx-target="this" hx-swap="outerHTML">
<label>Email:
<input type="email" name="email" hx-post="/contact/email" value="$sanitizedEmail">
$errorMessage
</label>
</div>
EOT;


// возвращаем шаблон
echo $template;
?>

Как видно, htmx ожидает от сервера ответа в виде HTML (а не JSON), который затем вставляет в страницу в указанном месте.

Если запустить приведённый выше код, ввести в поле ввода адрес, не относящийся к gmail.com, а затем потерять фокус, то под полем появится сообщение об ошибке Принимаются только адреса Gmail!.

Примечание: при динамической вставке содержимого в DOM следует также подумать о том, как это будет интерпретировано программой чтения с экрана. В приведённом выше примере сообщение об ошибке находится внутри нашего тега label, поэтому оно будет прочитано программой чтения с экрана, когда поле в следующий раз получит фокус. Если сообщение об ошибке будет вставлено в другое место, то следует использовать атрибут aria-describedby, чтобы связать его с нужным полем.

Стоит также отметить, что htmx запускает набор событий, связанных с процессом проверки, которые мы можем использовать для добавления собственной логики проверки и методов обработки ошибок. Например, если бы мы хотели реализовать проверку электронной почты в коде JavaScript, мы могли бы сделать следующее:

<form hx-post="/contact">
<label>Email:
<input type="email" name="email" required>
</label>
<button>Submit</button>
</form>

<script>
const emailInput = document.querySelector('input[type="email"]');

emailInput.addEventListener('htmx:validation:validate', function() {
const pattern = /@gmail\.com$/i;

if (!pattern.test(this.value)) {
this.setCustomValidity('Only Gmail addresses accepted!');
this.reportValidity();
}
});
</script>

Здесь мы используем событие htmx:validation:validate, которое вызывается перед вызовом метода checkValidity() элемента.

Теперь при попытке отправить форму с адресом, отличным от gmail.com, мы увидим то же самое сообщение об ошибке.

Что ещё может делать htmx

htmx — это универсальная библиотека, созданная для расширения возможностей HTML и обеспечения простого и мощного способа обработки динамического обновления содержимого в нашем веб-приложении. Её функциональность выходит за рамки описанных здесь возможностей и позволяет создать более интерактивный и отзывчивый сайт, не прибегая к помощи тяжёлых JavaScript-фреймворков.

Прежде чем мы закончим, давайте кратко рассмотрим некоторые из этих дополнительных возможностей.

Расширения

Расширения являются мощным инструментом в арсенале htmx. Эти настраиваемые JavaScript-компоненты позволяют дополнить и адаптировать поведение библиотеки к нашим конкретным потребностям. Расширения включают в себя включение JSON-кодирования в запросах, управление добавлением и удалением классов в HTML-элементах, отладку элементов, поддержку обработки шаблонов на стороне клиента и многое другое. Имея в своём распоряжении все эти возможности, мы можем настраивать htmx до мельчайших деталей.

Список доступных расширений можно найти на сайте htmx.

Boosting

Функциональность htmx "boosting" позволяет улучшать стандартные HTML-анкеры и формы, превращая их в Ajax-запросы (аналогично технологиям типа pjax прошлых лет):

<div hx-boost="true">
<a href="/blog">Blog</a>
</div>

Тег a в этом div выполнит Ajax GET-запрос к /blog и подставит HTML-ответ в тег <body>.

Используя эту возможность, мы можем создавать более плавную навигацию и отправку форм для наших пользователей, делая наши веб-приложения более похожими на SPA.

Управление историей

К слову о SPA, htmx также имеет встроенную поддержку управления историей, согласованную со стандартным history API браузера. Благодаря этому мы можем вводить URL-адреса в навигационную панель браузера и сохранять текущее состояние страницы в истории браузера, гарантируя, что кнопка "Назад" будет вести себя так, как ожидают пользователи. Это позволяет создавать веб-страницы, похожие на SPA, сохраняя состояние и обрабатывая навигацию без перезагрузки всей страницы.

Использование со сторонними библиотеками

Одним из достоинств htmx является его способность хорошо взаимодействовать с другими. Он может легко интегрироваться со многими сторонними библиотеками, используя их события для запуска запросов. Хорошим примером этого является демонстрация SortableJS на сайте htmx.

Есть также пример confirm, показывающий, как использовать sweetalert2 для подтверждения действий htmx (хотя в этом случае также используется hyperscript, экспериментальный язык сценариев, разработанный для выразительных и легко внедряемых непосредственно в HTML).

Заключение

htmx — это универсальный, лёгкий и простой в использовании инструмент. Он успешно сочетает в себе простоту HTML и динамические возможности, часто присущие сложным библиотекам JavaScript, предлагая убедительную альтернативу для создания интерактивных веб-приложений.

Однако это не универсальное решение. Для более сложных приложений может потребоваться JavaScript-фреймворк. Но если ваша цель — создать быстрое, интерактивное и удобное веб-приложение без лишних сложностей, htmx определённо заслуживает внимания.

По мере развития веб-разработки такие инструменты, как htmx, предоставляют новые интересные возможности для создания лучшего опыта для пользователей. Почему бы не попробовать его в одном из будущих проектов и не посмотреть, что htmx может сделать для вас?

Дополнительные материалы

Предыдущая Статья

Руководство по оптимизации JavaScript файлов

Следующая Статья

Преимущества пользовательских свойств в CSS