Команда htmx объявила о выходе версии 4.0.0. Релиз стал итогом восьми месяцев работы (не считая отдельно разработанной игры), и результатом в команде довольны.
Идея htmx 4 начала формироваться, когда один из разработчиков занялся созданием проекта fixi и в процессе глубже разобрался с fetch() API и асинхронным программированием в JavaScript. Раньше htmx всегда использовал XMLHttpRequest — из соображений обратной совместимости.
Однажды вечером с ним связался Кристиан, у которого были интересные идеи по поводу потоковой передачи HTML. Это натолкнуло на мысль, что перенос внутренней логики на fetch() упростит задачу и для него, и для библиотеки в целом.
После некоторой работы к проекту удалось подключить Майкла и Алекса, и разработка пошла полным ходом.
Процесс шёл гладко. Начали с портирования fixi и тестового набора htmx. Со временем команда заново открыла для себя причины, по которым htmx в своё время был устроен именно так, и новая реализация постепенно приближалась к старой. На данный момент поведенческие различия между 2.x и 4.x относительно невелики, а там, где они всё же есть, были сделаны осознанные решения — с расчётом на то, что приложения на htmx останутся жизнеспособными в духе концепции веб-сервисов на 100 лет.
Версия 4.0 не помечена в NPM как latest — команда не хочет принудительно обновлять пользователей, полагающихся на неверсионированные CDN-ссылки на htmx. Тег latest пока сохранится за веткой 2.x, а 4.0 будет помечена как next примерно до начала 2027 года. При этом сам сайт будет ссылаться на версию 4.0.
Основные изменения
Как уже отмечалось, с точки зрения пользователя htmx 4 почти идентичен htmx 2. Есть три главных изменения:
- Наследование атрибутов теперь по умолчанию явное, а не неявное (это самый трудоёмкий пункт при обновлении)
- Названия событий htmx стандартизированы и приведены в порядок. Некоторым продвинутым пользователям придётся изменить обработчики событий
- Поддержка истории по умолчанию больше не использует
localStorage(что раньше было источником немалого числа проблем). Большинство пользователей вообще не заметят этой перемены
Внутри библиотека перешла с XMLHttpRequest на fetch(), но для большинства пользователей htmx это должно быть незаметно.
Наследование атрибутов
В htmx 2 многие атрибуты по умолчанию «наследовались». Это позволяло размещать атрибуты на родительских элементах, а их поведение распространялось на дочерние. Такой подход, доставшийся в наследство от intercooler.js, был вдохновлён CSS и, что неудивительно, дал похожий результат: мощно, но временами трудно понять.
В htmx 4 атрибуты не наследуются, если это не указано явно — добавлением суффикса :inherited после имени атрибута:
<!-- htmx 2 -->
<div hx-confirm="Are you sure?">
<button hx-delete="/item/1">Delete</button>
</div>
<!-- htmx 4 -->
<div hx-confirm:inherited="Are you sure?">
<button hx-delete="/item/1">Delete</button>
</div>
Это станет самым трудоёмким пунктом при переходе с htmx 2 на htmx 4. Чтобы упростить процесс, разработчики подготовили утилиту командной строки, которая находит места, требующие пометки как наследуемые.
Атрибуты вроде hx-disinherit больше не нужны, и их следует удалить.
События
Набор событий, которые генерировал htmx 2, разрастался органически на протяжении всей истории библиотеки и не был особенно хорошо организован — было сложно точно понять, какое событие срабатывает в какой момент.
В htmx 4 все события теперь следуют схеме htmx:phase:action[:sub-action]:
| htmx 2 | htmx 4 |
|---|---|
htmx:beforeRequest | htmx:before:request |
htmx:afterRequest | htmx:after:request |
htmx:beforeSwap | htmx:before:swap |
htmx:afterSwap | htmx:after:swap |
htmx:configRequest | htmx:config:request |
Кроме того, внесены следующие изменения:
- Большинство событий об ошибках объединены в
htmx:error. HTTP-ошибки ответа сервера генерируютhtmx:response:error - События
htmx:xhr:*удалены — htmx 4 используетfetch() - События
htmx:validation:*удалены в пользу нативной валидации форм браузера
Полная таблица соответствий приведена в разделе «Что нового в htmx 4».
Утилита проверки при обновлении помечает старые названия событий в атрибутах hx-on и в JavaScript-коде, если может их найти.
История
Поддержка истории браузера всегда была частью htmx и позволяла реализовать логику, реагирующую на кнопку «назад», с помощью простых атрибутов. В htmx 2 для восстановления страниц использовался кэш в localStorage, куда сохранялся снимок DOM. К сожалению, немалая часть проблем была связана с тем, что этот снимок мог включать мутации DOM, внесённые сторонними JavaScript-библиотеками. При восстановлении страницы эти мутации оставались, а вот логика JavaScript, которая их производила, — нет.
htmx 4 больше не кэширует страницы в localStorage. При переходе назад htmx заново запрашивает страницу и подставляет её в <body> либо в элемент [hx-history-elt], если он присутствует. Благодаря этому сторонние JavaScript-библиотеки в большинстве случаев работают «из коробки», а при хорошем кэшировании запросов процесс происходит очень быстро.
Для тех, кто предпочитает локальное кэширование, теперь доступно расширение hx-history-cache — оно восстанавливает историю из sessionStorage и спроектировано так, чтобы хорошо интегрироваться с такими решениями для скриптинга, как Alpine.js.
Новые возможности
В htmx 4 появились две крупные новые функции, которыми в команде особенно гордятся.
Morph-свопы
Теперь htmx поддерживает morph-свопы «из коробки». Ранее был создан проект idiomorph, который чуть было не включили в htmx 2.x, но тогда от этой идеи отказались. В htmx 4 Майкл проделал большую работу по улучшению этого алгоритма и его бесшовной интеграции в библиотеку.
Тег <hx-partial>
Ещё одна значимая новая функция — тег <hx-partial>. Он похож на внеполосные свопы (out-of-band swaps), но гораздо нагляднее, когда требуется сделать что-то более сложное, чем просто заменить один элемент новой версией самого себя:
<hx-partial hx-target="#messages" hx-swap="beforeend">
<div>New message</div>
</hx-partial>
<hx-partial hx-target="#count">
<span>5</span>
</hx-partial>
Расширения
Значительная часть новинок htmx 4 связана именно с расширениями. Переход на fetch() изнутри позволил переосмыслить, как расширения могут и должны работать, что дало толчок к созданию (и пересозданию) множества новых расширений, например:
hx-preload— предзагрузка контента (например, поmouseover) для ускорения запросовhx-download— нативная загрузка файлов на базеfetch()hx-alpine-compat— сглаживает проблемы совместимости между htmx и Alpine.jshx-history-cache— кэширует историю вsessionStorage, обеспечивает совместимость с Alpine.js
Кроме того, появились три новых или обновлённых расширения для потоковой передачи HTML:
hx-sse— потоковая передача черезtext/event-streamhx-ws— потоковая передача и отправка через WebSocketshx-multipart— потоковая передача черезmultipart/mixed
Наконец, команда решила, что настало время попробовать создать собственное небольшое решение для скриптинга на фронтенде, тесно интегрированное с htmx. hx-live вдохновлён Alpine.js, jQuery и hyperscript и делает написание фронтенд-скриптов приятным занятием. Он даже поддерживает то, что в команде называют DOM-based, HATEOAS-дружественной реактивностью.
В дистрибутив также добавлен новый пакет htmax.js, который объединяет htmx с наиболее популярными из этих расширений в одном файле — на случай, если не хочется выбирать нужные вручную.
Обновление
Полное руководство по обновлению доступно в разделе «Что нового в htmx 4».
Как уже упоминалось, для помощи в обновлении предоставляется специальная утилита:
$ npx htmx.org@4.0.0 upgrade-check -- ./templates
File extensions: .html, .php, .js, .ts, .jinja, .jinja2, .j2, .erb, .hbs
Use --ext to add more (e.g. --ext .vue --ext .svelte)
Scanning 1 file(s)...
Found 8 issue(s) in 1 of 1 file(s).
templates/index.html:1: [inheritance] hx-headers needs :inherited suffix (descendant on line 3 has hx-delete) (this looks like a CSRF token; without :inherited the header does not reach child elements and the server rejects the request)
templates/index.html:2: [inheritance] hx-target needs :inherited suffix (descendant on line 3 has hx-delete)
templates/index.html:2: [inheritance] hx-confirm needs :inherited suffix (descendant on line 3 has hx-delete)
templates/index.html:3: [renamed-attr] hx-disable -> rename to hx-ignore (hx-disable now means 'disable during request')
templates/index.html:4: [removed-attr] hx-vars is removed -> use hx-vals with js: prefix
templates/index.html:4: [removed-attr] hx-prompt is removed -> load the hx-prompt extension to keep the same syntax
templates/index.html:9: [old-event] old event name "htmx:afterRequest" -> "htmx:after:request"
templates/index.html:9: [old-api] htmx.addClass() is removed -> use element.classList.add()
Также подготовлен специальный навык (skill) для агентов, который помогает с обновлением.
Установка
htmx 4.0 можно установить через пакетный менеджер, указав версию 4.0.0, либо подключить через CDN:
<script src="https://unpkg.com/htmx.org@4.0.0/dist/htmx.min.js"></script>
или скачать напрямую.
LLM
Нравится это или нет, но многие используют LLM, и для них подготовлены следующие файлы навыков:
htmx-guidance— базовые навыки разработки с htmx 4htmx-debugging— диагностика проблем htmx в процессе разработкиhtmx-extension-authoring— написание и отладка расширений htmx 4htmx-upgrade-from-htmx2— миграция кодовой базы с htmx 2.x на 4.x
Хорош или плох сам факт выпуска новой версии библиотеки в эпоху LLM — вопрос отдельный.
Заключение
Команда надеется, что htmx 4 придётся по душе пользователям. Поддержка htmx 2 продолжится на неопределённый срок, так что спешить с обновлением не обязательно.
Отдельная благодарность людям, без которых этот релиз был бы невозможен:
- Michael West — невероятный товарищ по команде и разработчик с «умом простака» (grug-brained)
- Christian Tanul — вдохновил на создание htmx 4 и возглавил работу над потоковыми и live-расширениями
- Alex Petros — держал курс проекта в ровном русле
- Stephen Mitchell — гений, стоящий за игрой
- Stu Kennedy — эксперт команды по WebSockets
- André Ahlert Jr. — обеспечил поддержку в IDE и редакторах
- Dien Hoa Truong — тестировал ранние версии htmx 4 и помог исправить множество багов
Музыка для апгрейда
Какой же релиз htmx без музыки для апгрейда: