htmx — инструмент для создания сложных и интерактивных веб-приложений на HTML, альтернатива клиентскому рендерингу на Javascript. В этой статье рассказываем, как библиотека помогает переиспользовать элементы на сервере, сократить объем кода на Javascript и отказаться от сборки.

Это адаптированный перевод статьи htmx and HTML Driven Development  фронтенд-разработчика Раджасегара Чандрана. Повествование ведется от лица автора оригинала.

HTML как центр вселенной

Интернет в том виде, в котором мы знаем его сегодня, во многом существует благодаря HTML и CSS. Javascript (JS) мог стать связующим звеном между ними и сделать страницы более динамичными и интерактивными. Но история веб-программирования развивалась иначе — после появления клиентского рендеринга и других подобных технологий, использовать JS для создания веб-приложений стало заметно сложнее.

Что такое htmx

htmx — библиотека, которая позволяет создавать современные и мощные пользовательские интерфейсы с простой разметкой. С ее помощью AJAX-запросы, запуск CSS-переходов, вызов WebSocket и событий, отправленных сервером, можно проводить непосредственно из элементов HTML.

Я начинал с фуллстэк-разработки, поэтому использование HTML для создания веб-страниц казалось мне естественным. После перехода во фронтенд я быстро разочаровался в клиентском рендеринге на базе JS — технология создавала слишком много проблем.

После интерфейсных фреймворков (например, Ember, React, Svelte) мне гораздо приятнее работать с HTML и hyperscript в связке с htmx. Он позволяет создавать веб-приложения, ориентированные на серверный рендеринг (SSR), и сильно увеличивает мою продуктивность.

SSR-приложения

Использование htmx мотивирует постепенно отказываться от клиентского рендеринга в пользу серверного. Считается, что SSR — это последнее средство, которое используют только когда необходимо быстро повысить производительность. Однако на серверном рендеринге может строится весь пользовательский интерфейс приложения.

htmx не требует для запуска других пакетов JS и она независима от фреймворков или языков. Поэтому использовать ее можно с любой серверной платформой: например, с Node Express, RAILS, Django, Phoenix, Laravel и другими.

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

htmx позволяет переиспользовать элементы пользовательского интерфейса на стороне сервера с помощью более привычных библиотек: например, pug для Node или библиотек шаблонов для RAILS и Django. Это помогает сделать HTML сложным и динамичным.

Вот пример демонстрационного веб-приложения Rentals Listing, созданного на Express.js и HTML. Один и тот же паршиал в нем используется и для статистических, и для динамических сценариев:

ul.results
  each rental in rentals
    li
      article.rental
        button.image(type="button", _="on click toggle .large then if #view-caption.textContent === 'View Larger' then set #view-caption.textContent to 'View Smaller' else set #view-caption.textContent to 'View Larger'")
          img(src=rental.attributes.image, alt='An image of ' + rental.attributes.title)
          small#view-caption View Larger
        .details
          h3
            a(href='/rentals/' + rental.id) #{rental.attributes.title}
          .detail.owner
            span Owner:
            | #{rental.attributes.owner}
          .detail.type
            span Type:
            | #{rental.attributes.category}
          .detail.location
            span Location:
            | #{rental.attributes.city}
          .detail.bedrooms
            span Bedrooms:
            | #{rental.attributes.bedrooms}
        .map
          img(alt='A map of ' + rental.attributes.title, src=rental.mapbox, width="150",height="150")

В листинге домашней страницы для отображения паршиала я использовал include из библиотеки pug:

extends layout
block content
  .jumbo
    .right
    h2 Welcome to Super Rentals!
    p We hope you find exactly what you're looking for in a place to stay.
    a.button(href="/about") About Us
  .rentals
    label
      span Where would you like to stay?
      input.light(type="text", name="search",
       hx-post="/search" ,
       hx-trigger="keyup changed delay:500ms" ,
       hx-target=".results" ,
       hx-indicator=".htmx-indicator")

include includes/rental-list.pug

Каждый раз, когда пользователь ищет жилье в аренду на сайте, я использую один и тот же паршиал для заполнения результатов поиска. Результат выглядит так:

app.post('/search', (req, res) => {
  const { search } = req.body;

  const results = _rentals.data.filter(r => {
    const _search = search.toLowerCase();
    const _title = r.attributes.title.toLowerCase();
    return _title.includes(_search);
  });
  const template  = pug.compileFile('views/includes/rental-list.pug');
  const markup = template({ rentals: results });
  res.send(markup);
});

Маршрутизация на стороне сервера

Маршрутизация на стороне клиента создает целый набор проблем. Например, всегда существует дилемма между маршрутизацией на основе хэша и URL. Поскольку history api не поддерживается в старых браузерах (таких как Internet Explorer 11), предпочтение почти всегда отдается маршрутизации на основе хэшей, которая использует идентификаторы фрагментов в URL-адресе.

Большинство фреймворков JS реализуют собственную логику маршрутизации на стороне клиента. При этом под всеми фреймворками используется собственный API браузера: таких как window.history. Это приводит к появлению большого количества шаблонного кода в приложении.

Меньше кода на JS

На мой взгляд, главное преимущество htmx — количество кода на JS, который мы пишем и отправляем в браузер. Вместе с hyperscript библиотека позволяет создавать многофункциональные интерактивные приложения без использования клиентского кода на JS:

<!-- have a button POST a click via AJAX -->
<button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
</button>

Когда одностраничные приложения только начали входить в моду, сообщество приняло JSON в качестве стандарта обмена для данными. Теперь, чтобы реконструировать HTML из данных JSON, часто требуется обработать большой объем данных на стороне клиента, которые приходят с сервера через API. В ответах API часто содержатся либо неполные, либо избыточные данные.

Для решения этой проблемы разработаны сложные альтернативы вроде GraphQL, благодаря которым можно получать от сервера только нужные данные. htmx решает проблему лучше: достаточно заменить HTML на HTML-ответ, полученный от сервера. Больше никаких данных на стороне клиента.

Отсутствие сборки/компиляции

Еще одно преимущество htmx — отсутствие инструментов сборки веб-приложений. Вы можете использовать инструмент через CDN:

<!-- Load from unpkg -->
<script src="<https://unpkg.com/htmx.org@1.3.3>"></script>

Отсутствие сборки — глобальный тренд в разработке веб-приложений. С одной стороны, спецификации модулей ES приняли все разработчики браузеров, с другой — появились инструменты Skypack , Snowpack , Vite, которые сочетаются с подходами CDN и ESM. Все это в конечном счете приведет к тому, что мы будем видеть сборку для Javascript на стороне клиента все реже и реже. Добавьте к этому отсутствие необходимости устанавливать тысячи пакетов npm и поддерживать сложные конфигурации сборки.

Единая база кода

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

htmx позволяет объединить весь код в одном месте: поскольку рендеринг происходит на стороне сервера, отдельная база для интерфейса не нужна. В долгосрочной перспективе это поможет сэкономить много времени и денег. Кроме того, разработчики смогут действовать более согласованно — им не придется проверять два и более репозиториев.

Принцип локальности поведения (LoB)

Принцип LoB сформулировал теоретик программирования Ричард Гэбриэлл. Он гласит, что все разработчики должны стремиться к тому, что поведение каждого элемента кода должно быть очевидным при его проверке.

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

Выглядит это так:

The behaviour of a code unit should be as obvious as possible by looking only at that unit of code
<div hx-get="/clicked">Click Me</div>

LoB часто конфликтует с другими принципами разработки ПО: например, с разделением ответственности. Частично этот конфликт был разрешен, когда React представил HTML и CSS в Javascript. Создатель React Пит Хант при этом считает, что речь идет о разделении технологий:

Отсуствие проблем с синхронизацией состояний

Управление состоянием на стороне клиента создает больше проблем, чем решает. Внедрение этого принципа приводит к тому, что управлять состоянием нужно и на стороне клиента, и на стороне сервера. Альтернативное решение — хранить состояние на сервере. В этом случае клиент служит фиктивным исполнителем для рендеринга изменения состояний.

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

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

Комментарии (17)


  1. S__vet
    02.12.2021 15:16
    +1

    Мемы хорошие!


  1. Oriolidae
    02.12.2021 16:43
    +2

    Есть же ".hta" от Microsoft, зачем такое дважды? Похоже я не уловить суть


  1. CyberAP
    02.12.2021 19:06
    +2

    У всего этого есть фатальный недостаток (то, из-за чего все Rails-приложения ощущаются заторможенными) — задержка по сети: https://gist.github.com/jboner/2841832


    1. andreyverbin
      02.12.2021 22:06

      Какая разница возвращать JSON или готовый HTML? Задержки будут одинаковыми.


      1. ralder
        03.12.2021 12:00

        ну если придираться - то json по объему будет меньше)


        1. tmin10
          03.12.2021 12:15
          +2

          Его ещё парсить надо, и встраивать в DOM


      1. CyberAP
        04.12.2021 15:14

        Не понимаю этого сравнения. При логике на клиенте мы можем очень быстро реагировать на действия пользователя (например показать спиннер или реализовать Optimistic UI), запрос может выполниться гораздо позже чем пользователь увидит отклик в интерфейсе. С логикой на сервере мы этого сделать не можем, а задержки в отклике у подходов отличаются на порядки. И если задержки на клиенте мы можем оптимизировать потому что это наша зона ответственности, то с задержками по сети всё куда сложнее и уже выходит далеко за пределы области фронтенда.


        1. andreyverbin
          05.12.2021 03:03
          +3

          На сервере осталась та же логика, которая там была при SPA, просто ответ с сервера это уже готовый HTML. На клиенте теперь не надо иметь сложную систему типа React, достаточно небольшой либы, которая этот HTML куда надо положит. Можно сделать спиннеры и optimistic UI в простых случаях и запрятать в эту либо или прицепить на какие-то события из неё.

          подходи супер простой и сломается если

          • UI обновляется в куче несвязанных мест - нажали на кнопку и в результате в 10 несвязанных блоках на странице что-то поменялось.

          • Вы вынуждены хранить состояние на клиенте и только там. То есть сервер в принципе не может HTML сделать.

          • Куча интерактива не связанного с сервером. Водим курсором - падают снежинки на экране, типа такого

          Все это чаще всего присутствует в навороченном UI. А типовой UI ala панель управления облаком, админка, тикет систему и т.п. очень неплохо ложатся в этот подход.


    1. Nnnnoooo
      02.12.2021 23:48
      -1

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


    1. toxicmt
      03.12.2021 01:40
      +1

      А посмотрите как работает https://www.hey.com/ Там кстати соединение по вебсокетам идет, а не просто ajax


  1. Cerberuser
    02.12.2021 21:04
    +2

    Сейчас приходится поддерживать старый проект с большим количеством кода на Java Server Faces. Мне кажется, или это в общем и целом "те же уши, вид сбоку"?..


    1. andreyverbin
      02.12.2021 22:15

      Нет, htmx намного проще. Он просто заменят куски страницы на HTML, который вернул сервер. То есть вместо JSON возвращаем HTML и убираем весь фронтенд код.


      1. Cerberuser
        03.12.2021 07:32
        +2

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


      1. Katrychenko
        04.12.2021 00:02
        +1

        То есть ради того чтобы заменить html на html мы должны теперь использовать htmx?)))) Чувак, а кто мешает сделать запрос на сервер и вернуть готовый html прямо в json?) Что за велосипеды :)


  1. simpson
    02.12.2021 22:41
    +1

    Очень напоминает Turbo и Stimulus от Basecamp. hotwired.dev


    1. toxicmt
      03.12.2021 01:37
      +1

      Угу, они там друг другом все вдохновлялись


    1. kmmbvnr
      09.12.2021 09:04

      Turbo + Stimulus не предлагают отказываться от простого JavaScript'a, чем мне больше и нравятся. Я сторонник идеи - для каждой задачи, свой инструмент.

      С помощью turbo/frames плюс маленьких контроллеров на Stimulus, или даже стандартных веб-компонентов гарантированно можно однообразно реализовать любую функциональность. Насколько гибкий нерасширяемый htmlx - большой вопрос.