Привет, Хаброжители!
Опытные программисты, выросшие вместе со Всемирной паутиной, не уделяли идеям гипермедиа особого внимания. А молодые веб-разработчики не знают ничего, кроме одностраничных приложений и фреймворков, используемых для их создания.
Устаревшая технология, подходящая только для создания документов со ссылками, текстом и графикой? Ничего подобного! В вашем распоряжении — эффективная технология для построения приложений.
Познакомьтесь с новыми инструментами — htmx и Hyperview, которые используют гипермедиа в качестве системной архитектуры. Научитесь строить сложные пользовательские интерфейсы с использованием гипермедиа как базовой технологии: на базе htmx для веб-приложений и на базе Hyperview для мобильных. А затем исследуйте прикладные современные подходы к построению веб-приложений, в которых эта архитектура используется.
Гипермедиа-управляемая архитектура подойдет не для каждого приложения, но повышенная гибкость и простота станут огромным преимуществом. Даже если этот подход не улучшит вашу программу, вам стоит понять его суть, сильные и слабые стороны и отличия от традиционно применяемой методики. Веб-среда росла быстрее, чем любая другая распределенная система в истории, и веб-разработчики должны уметь использовать сильные стороны базовых технологий, которые сделали возможным этот рост.
В этой главе мы еще больше углубимся в инструменты htmx. Даже с теми из них, что вы уже узнали, можно сделать очень много. Тем не менее при разработке приложений HDA встречаются ситуации, в которых приходится искать дополнительные решения и средства.
В этой главе будут рассмотрены более сложные атрибуты htmx, а также продвинутые возможности использованных ранее атрибутов.
Кроме того, будет рассмотрена функциональность, предоставляемая htmx помимо простых атрибутов HTML: как htmx расширяет стандартные запросы и ответы HTTP, как htmx работает с событиями (и генерирует их) и что делать, если на странице нет простого отдельного целевого элемента, который можно обновить.
Наконец, мы рассмотрим некоторые практические вопросы разработки htmx: как эффективно заниматься отладкой приложения на основе htmx, какие соображения безопасности необходимо учитывать при работе с htmx и как настраивать поведение htmx.
С инструментами и возможностями, описанными в этой главе, вы сможете создавать довольно сложные пользовательские интерфейсы, используя только htmx и, возможно, некоторые подходящие для гипермедиа скрипты на стороне клиента.
До сих пор мы использовали около 15 атрибутов htmx. Самыми важными из них были:
Начнем с атрибута hx-swap. Он часто не включается в элементы, выдающие запросы на основе htmx, потому что поведение по умолчанию — innerHTML, заменяющее внутреннюю разметку HTML элемента, — подходит для большинства практических сценариев.
Мы уже познакомились с ситуациями, в которых требовалось переопределить поведение по умолчанию и использовать, например, outerHTML. В главе 2 также были представлены другие варианты замены — beforebegin, afterend и т. д.
В главе 5 мы рассмотрели модификатор задержки swap для hx-swap, при помощи которого можно воспроизвести эффект постепенного скрытия контента перед его удалением из DOM.
Кроме того, hx-swap предлагает дополнительные возможности управления со следующими модификаторами:
Листинг 126. Прокрутка к верху страницы
❶ Сообщает htmx показать верхнюю границу body после замены.
За более подробной информацией и другими примерами обращайтесь к электронной документации hx-swap.
Как и hx-swap, атрибут hx-trigger часто опускается при работе с htmx, потому что обычно востребовано его поведение по умолчанию. Напомним, что инициирующие события по умолчанию определяются типом элемента:
Листинг 127. Поле ввода с активным поиском
❶ Расширенная спецификация триггера.
В этом примере используются два модификатора, доступных для атрибута hx-trigger:
Другие модификаторы, доступные для hx-trigger:
Атрибут hx-trigger также позволяет задать фильтр для событий. Для этого после имени события в квадратных скобках указывается выражение JavaScript.
Представим сложный сценарий, в котором получение контактов должно быть разрешено только при определенных условиях. Имеется функция JavaScript contactRetrievalEnabled(), которая возвращает логический признак: true, если получение контактов разрешено, и false в остальных случаях. Как использовать эту функцию для ограничения доступа к кнопке, выдающей запрос к /contacts?
Чтобы решить эту задачу с использованием фильтра событий в htmx, напишите следующую разметку HTML.
Листинг 128. Поле ввода с активным поиском
❶ Запрос выдается по событию click только в том случае, если contactRetrievalEnabled() возвращает true.
Кнопка не выдает запрос, если contactRetrievalEnabled() возвращает false, что позволяет динамически управлять возможностью выдачи запросов. Многие типичные сценарии требуют использования триггера события, тогда как запрос должен выдаваться только в определенных обстоятельствах:
Кроме перечисленных модификаторов, hx-trigger предоставляет несколько «синтетических» событий, то есть событий, не являющихся частью обычного DOM API. События load и revealed уже встречались в примерах отложенной загрузки и бесконечной прокрутки, но htmx также предоставляет событие intersect, которое срабатывает при пересечении элемента с его родительским элементом.
Это синтетическое событие использует современный API наблюдателей пересечения, о котором можно больше узнать из MDN.
Пересечения предоставляют возможность более точно контролировать, когда именно должен инициироваться запрос. Например, можно установить порог и указать, что запрос должен выдаваться только в том случае, если элемент виден на 50 %.
Безусловно, hx-trigger — самый сложный из атрибутов htmx. За более подробной информацией о нем и другими примерами обращайтесь к электронной документации.
Htmx предоставляет множество других, реже используемых атрибутов для настройки поведения гипермедиа-управляемых приложений.
Ниже перечислены самые полезные из этих атрибутов:
Листинг 129. Две конкурирующие кнопки
Все работает нормально, но что, если пользователь щелкнет на кнопке Get Contacts (Контакты), а ответ на запрос будет получен не сразу? И за это время он успеет щелкнуть по кнопке Get Settings (Настройки)? В таком случае будут существовать два необработанных запроса одновременно.
Предположим, запрос /settings завершается первым и выводит информацию о настройках. Пользователь начинает вносить изменения, но тут его ждет сюрприз — запрос к /contacts завершается и заменяет все тело документа информацией о контактах!
Чтобы решить эту проблему, можно воспользоваться hx-indicator и уведомить пользователя о том, что выполняется определенный процесс, снижая вероятность щелчка второй кнопки. Но если необходимо действительно предусмотреть, чтобы в любой момент времени между этими кнопками существовал только один запрос, следует воспользоваться атрибутом hx-sync. Заключим обе кнопки в тег div и исключим лишнюю спецификацию hx-target, переместив атрибут вверх в div. Затем можно использовать hx-sync с div, чтобы координировать запросы между двумя кнопками.
Обновленный код выглядит так:
❶ Повторяющиеся атрибуты hx-target поднимаются в родительский элемент div.
❷ Синхронизация по родительскому div.
Размещая в div атрибут hx-sync со значением this, мы говорим: «Синхронизировать все запросы htmx, инициируемые в этом элементе div, по отношению друг к другу». Это означает, что, если одна кнопка уже выдала незавершенный запрос, другие кнопки внутри div не смогут выдавать запросы до его завершения.
Атрибут hx-sync также поддерживает другие стратегии, которые позволяют, например, заменить существующий запрос «на лету» или ставить запросы в очередь с конкретной стратегией организации очереди. Полную документацию вместе с примерами можно найти на странице hx-sync на сайте htmx.org.
Как видите, htmx предлагает значительную функциональность, управляемую атрибутами, для более мощных гипермедиа-управляемых приложений. Полную справку по всем атрибутам htmx можно найти на сайте htmx.
До сих пор мы работали с событиями JavaScript в htmx в основном через атрибут hx-trigger. Этот атрибут предоставляет эффективный механизм управления приложением с использованием декларативного синтаксиса, хорошо сочетающегося с HTML.
Тем не менее на этом возможности событий не исчерпаны. События играют важнейшую роль в расширении HTM как среды гипермедиа, а также, как вы вскоре убедитесь, в дружественных ей скриптах.
События — это «клей», связывающий воедино DOM, HTML, htmx и скрипты. Можно даже рассматривать DOM как сложную «шину событий» для приложений.
Очень важно: чтобы строить продвинутые гипермедиа-управляемые приложения, необходимо хорошо разобраться в событиях. Поверьте, вы не пожалеете о потраченном времени.
Кроме простой обработки событий, htmx генерирует много полезных событий. Эти события могут использоваться для добавления новой функциональности в приложения — либо через саму библиотеку htmx, либо через скрипты.
Некоторые события, чаще всего генерируемые в htmx:
Рассмотрим пример использования событий, генерируемых htmx. Мы воспользуемся событием htmx:configRequest для настройки запроса HTTP.
Представьте следующую ситуацию: команда, пишущая код на стороне сервера, решила, что для повышения безопасности в каждый запрос должен включаться маркер, сгенерированный сервером. Маркер будет храниться в локальном хранилище (localStorage) браузера, в слоте special-token.
Маркер будет назначаться кодом JavaScript (пока не думайте о подробностях) при первом входе пользователя в приложение.
Листинг 130. Получение маркера в JavaScript
❶ Получает значение маркера, а затем сохраняет его в localStorage.
Серверная команда требует, чтобы вы включали этот специальный маркер при каждом запросе, выполняемом htmx, в заголовке X-SPECIAL-TOKEN. Как этого добиться? Один из способов основан на перехвате события htmx:configRequest и обновлении объекта detail.headers маркером из localStorage.
В «базовом» JS это будет выглядеть примерно так (код включается в тег script в теге документа HTML):
Листинг 131. Добавление заголовка X-SPECIAL-TOKEN
❶ Получает значение из локального хранилища и записывает его в заголовок.
Как видите, мы добавляем новое значение в свойство headers свойства detail события. После выполнения обработчика события свойство headers читается htmx и используется для построения заголовков генерируемого запроса AJAX.
Свойство detail события htmx:configRequest содержит полезные атрибуты, которые можно обновлять для изменения «структуры» запроса, в том числе:
Листинг 132. Добавление параметра token
❶ Получает значение из локального хранилища и присваивает его параметру.
Как видите, это решение обеспечивает значительную гибкость при обновлении запроса AJAX, выдаваемого htmx.
Полная документация по событию htmx:configRequest (и другим событиям, которые могут вас заинтересовать) находится на сайте htmx.
В htmx можно прослушивать множество полезных событий и реагировать на них при помощи hx-trigger. А что еще можно делать с событиями?
Сама библиотека htmx прослушивает одно специальное событие htmx:abort. Когда htmx получает это событие для незавершенного запроса, этот запрос отменяется.
Рассмотрим сценарий, в котором имеется потенциально долго выполняющийся запрос к /contacts и необходимо предоставить пользователю возможность отменить этот запрос. Для этого нужны кнопка, выдающая запрос (конечно, под управлением htmx), и другая кнопка, которая отправляет событие htmx:abort первой кнопке.
Код может выглядеть примерно так:
Листинг 133. Кнопка с возможностью отмены
❶ Обычный запрос GET на основе htmx к /contacts.
❷ Код JavaScript для поиска кнопки и отправки ей события htxm:abort.
Если пользователь щелкает на кнопке Get Contacts и запрос занимает некоторое время, пользователь может щелкнуть на кнопке Cancel и отменить запрос. Конечно, в более сложном пользовательском интерфейсе кнопка Cancel должна блокироваться при отсутствии незавершенного запроса HTTP, но реализация этой функциональности на «чистом» HTML будет слишком хлопотной.
К счастью, в _hyperscript реализация достаточно проста. Результат будет выглядеть примерно так:
Листинг 134. Кнопка с возможностью отмены на основе _hyperscript
Теперь кнопка Cancel блокируется только при наличии незавершенного запроса от кнопки contacts-btn. И чтобы это решение работало, мы используем события, генерируемые и обрабатываемые средствами htmx, а также синтаксис _hyperscript, хорошо сочетающийся с событиями.
В следующем разделе мы продолжим разговор о том, как htmx расширяет обычные запросы и ответы HTTP, но так как этот механизм основан на событиях, рассмотрим один заголовок ответа HTTP, поддерживаемый htmx: HX-Trigger. Ранее вы узнали, как запросы и ответы HTTP поддерживают заголовки — пары «имя-значение», содержащие метаданные о запросе или ответе. В частности, мы рассмотрели заголовок запроса HX-Trigger, включающий идентификатор элемента, инициирующего заданный запрос.
Кроме заголововка запроса, htmx поддерживает заголовок ответа с именем HX-Trigger. Заголовок ответа позволяет инициировать событие для элемента, отправившего запрос AJAX. Это мощный механизм координации элементов в DOM без сильной связанности.
Чтобы понять, как он работает, рассмотрим следующий сценарий: имеется кнопка, которая получает новые контакты с удаленной системы на сервере. Подробности реализации на стороне сервера нас сейчас не интересуют, но мы знаем, что при выдаче запроса POST к пути /sync будет инициирована синхронизация с системой.
Синхронизация может привести (а может и не привести) к созданию новых контактов. При создании новых контактов необходимо обновить таблицу контактов. Если же контакты не создаются, то обновлять таблицу не нужно.
Чтобы реализовать эту функциональность, можно по условию добавить заголовок ответа HX-Trigger со значением contacts-updated.
Листинг 135. Условное инициирование события contacts-updated
❶ Вызов к удаленной системе, который синхронизирует базу данных контактов.
❷ Если какие-либо контакты были обновлены, клиент инициирует событие contacts-updated по условию.
Это значение инициирует событие contacts-updated для кнопки, которая выдает запрос AJAX к /sync. Затем можно воспользоваться модификатором from: атрибута hx-trigger для прослушивания этого события. С таким паттерном можно фактически инициировать запросы htmx со стороны сервера.
Код на стороне клиента может выглядеть так:
Листинг 136. Таблица Contacts
❶ Ответ на этот запрос может инициировать событие contacts-updated по условию.
❷ Таблица прослушивает событие и обновляется при его возникновении.
Таблица прослушивает событие contacts-updated, причем делает это на уровне элемента body. Она прослушивает элемент body, так как событие всплывает вверх от кнопки, и это позволяет избежать сильной связанности кнопки с таблицей: кнопку и таблицу можно перемещать как угодно, но благодаря событиям нужное поведение продолжит нормально работать. Кроме того, может потребоваться, чтобы событие contacts-updated инициировалось другими элементами или запросами, поэтому эта схема обеспечивает обобщенный механизм обновления таблицы контактов в приложении.
Вы уже видели одну расширенную возможность ответов HTTP, поддерживаемую htmx, — заголовок ответа HX-Trigger; но htmx поддерживает и другие заголовки запросов и ответов. В главе 4 рассматривались заголовки, присутствующие в за просах HTTP. Вот некоторые важные заголовки, используемые для изменения поведения htmx с ответами HTTP:
Пожалуй, в отношении информации, передаваемой клиенту, коды ответов HTTP даже важнее заголовков ответов. Коды ответов HTTP рассматривались в главе 3. В целом htmx обрабатывает коды ответов ожидаемым образом: для всех кодов ответов уровня 200 выполняется замена контента, а для остальных ничего не происходит. Тем не менее существуют два «специальных» кода ответов уровня 200:
Предположим, что вместо того, чтобы ничего не делать при получении кода 404, вы хотите оповестить пользователя о возникшей ошибке при помощи вызова метода JavaScript showNotFoundError(). Для этого добавим следующий фрагмент кода в событие htmx:beforeSwap.
Листинг 137. Вывод диалогового окна 404
❶ Подключение к событию htmx:beforeSwap.
❷ Если код ответа равен 404, вывести диалоговое окно.
Для определения того, должен ли ответ подставляться в DOM и какой элемент назначать целевым для ответа, также можно воспользоваться событием htmx:beforeSwap. Оно обеспечивает некоторую свободу выбора способа использования кодов ответов HTTP в приложении. Полная документация для события htmx:beforeSwap доступна на сайте htmx.org.
Выше мы увидели, как использовать событие, инициируемое сервером через заголовок ответа HX-Trigger, чтобы обновить блок DOM, который основан на ответе другого блока DOM. Этот прием помогает решить общую проблему гипермедиа-управляемых приложений: как обновить остальной контент? Ведь в обычных запросах HTTP существует только одна «цель» — весь экран; аналогичным образом в запросах на основе htmx есть только одна цель: явная или неявная цель элемента.
Рассмотрим несколько вариантов того, как обновить другой контент в htmx.
Первый и самый простой вариант — «расширение цели». Иначе говоря, вместо того чтобы заменять небольшую часть экрана, расширяйте цель запроса на основе htmx, пока она не будет включать все элементы, которые должны обновляться на экране. У такого решения есть два огромных преимущества — простота и надежность. Недостаток заключается в том, что оно не предоставляет желаемого взаимодействия с пользователем и не всегда хорошо сочетается с некоторыми макетами шаблонов на стороне сервера. Тем не менее мы всегда рекомендуем для начала хотя бы рассмотреть возможность применения этого подхода.
Второй, чуть более сложный вариант основан на использовании поддержки «внеполосного» (Out Of Band) контента в htmx. При получении ответа htmx проверяет его и ищет контент верхнего уровня, включающий атрибут hx-swap-oob. Этот контент исключается из ответа и не подставляется в DOM обычным образом. Вместо этого он заменяет контент с совпадающим идентификатором.
Рассмотрим конкретный пример. Вспомните ситуацию, описанную выше, когда таблица контактов должна была обновляться при создании новых контактов. Тогда проблема решалась с использованием событий и событием, инициируемым сервером через заголовок ответа HX-Trigger.
На этот раз мы воспользуемся атрибутом hx-swap-oob в ответе на запрос POST к /integrations/1. Новый контент таблицы контактов присоединяется к ответу.
Листинг 138. Обновленная таблица контактов
❶ Кнопка все еще выдает запрос POST к /integrations/1.
❷ Таблица уже не прослушивает событие, но теперь ей назначается идентификатор.
Ответ на запрос POST к /integrations/1 будет включать контент, который должен подставляться в кнопку с использованием обычного механизма htmx. Однако он включает новую, обновленную версию таблицы контактов, которая помечается атрибутом hx-swap-oob=«true». Контент будет удален из ответа, чтобы он не был вставлен в кнопку. Вместо этого он подставляется в DOM на место существующей таблицы, так как их идентификаторы совпадают.
Листинг 139. Ответ с внеполосным контентом
❶ Этот контент будет помещен в кнопке.
❷ Этот контент будет удален из ответа и заменен в соответствии с идентификатором.
Используя этот прием, можно обновлять контент страницы там, где потребуется. Атрибут hx-swap-oob предоставляет другие возможности, но все они документированы.
В зависимости от того, насколько точно работает технология шаблонов на стороне сервера и какой уровень интерактивности необходим приложению, внеполосная замена может стать мощным механизмом обновления контента.
Наконец, самый сложный механизм обновления контента был описан в разделе, посвященном событиям: обновление элементов с помощью событий, инициируемых сервером. Этот механизм может быть оптимальным, но он требует глубокого концептуального знания HTML и событий, а также соблюдения событийного подхода. Хотя нам нравится такой стиль разработки, он не для всех. Обычно мы рекомендуем этот паттерн только в том случае, если философия htmx событийного гипермедиа действительно пришлась вам по душе.
Но если она пришлась вам по душе — выбирайте ее. Мы создавали очень сложные и гибкие пользовательские интерфейсы, используя этот механизм, и он нам очень нравится.
Все решения проблемы «обновления остального контента» работают, и часто работают хорошо. Однако может наступить момент, когда будет проще выбрать другой подход, например реактивный. Как бы нам ни нравились решения гипермедиа, реальность такова, что некоторые паттерны UX просто невозможно с легкостью реализовать в этой среде. Классическим примером таких паттернов, уже упоминавшимся выше, является электронная онлайн-таблица: ее пользовательский интерфейс слишком сложен и содержит слишком много взаимозависимостей, чтобы его можно было качественно реализовать через обмен контентом гипермедиа с сервером.
В таких случаях (и каждый раз, когда вы чувствуете, что решение на основе htmx оказывается более сложным, чем другой подход) мы рекомендуем рассмотреть другие технологии. Будьте прагматичны и выбирайте подходящий инструмент для работы. Вы всегда можете использовать htmx для тех частей приложения, которые менее сложны и не требуют всех возможностей реактивного фреймворка, и сэкономить бюджет сложности для других частей.
Мы рекомендуем изучить больше разных веб-технологий, обращая внимание на сильные и слабые стороны каждой. Так у вас сформируется обширный инструментарий, к которому вы сможете обратиться при работе над очередной задачей. По нашему опыту, с htmx гипермедиа станет тем инструментом, к которому вы будете обращаться чаще всего.
Нам не стыдно признаться: мы большие поклонники событий. Эта технология лежит в основе практически каждого интересного пользовательского интерфейса. После того как вы получите доступ к обобщенному использованию событий в HTML, они станут особенно полезны в DOM. События позволяют строить слабо связанные программные системы, при этом часто сохраняя локальность поведения, которую мы так ценим.
Тем не менее события неидеальны. Одной из областей, в которых события создают особенно много проблем, является отладка: часто требуется узнать, почему событие не инициируется. Но как установить точку останова для чего-то, что не происходит? Никак (по крайней мере, пока).
Существуют два приема, которые могут помочь с отладкой. Первый предоставляется htmx, а другой — Chrome, браузером от Google.
Первый прием, предоставляемый самой библиотекой htmx, заключается в вызове метода htmx.logAll(). При этом htmx регистрирует в журнале все внутренние события, происходящие при выполнении бизнес-логики, загрузке контента, реакции на события и т. д.
Это может привести к информационной перегрузке, но грамотная фильтрация поможет справиться с проблемой. Вот как выглядит журнал (вернее, его небольшая часть), сохраняемый по щелчку на ссылке «docs» на сайте htmx.org при включенном режиме logAll().
Листинг 140. Журнал htmx
Не самое легкое чтиво, правда?
Но если сделать глубокий вдох и присмотреться, вы увидите, что не все так плохо: перед вами серия событий htmx, и некоторые из них уже вам знакомы (как htmx:configRequest!), вместе с элементами, которые их инициировали.
Немного привыкнув к чтению и фильтрации информации в журналах, вы научитесь ориентироваться в потоке событий. Это поможет вам в отладке проблем, связанных с htmx.
Описанный метод полезен, если проблема где-то внутри htmx, но что, если htmx вообще не инициируется? Такое иногда случается, например, когда вы неправильно ввели имя события.
В таких случаях приходится пользоваться инструментами, доступными в самом браузере. К счастью, браузер Google Chrome предоставляет очень полезную функцию monitorEvents(), которая позволяет наблюдать за всеми событиями, инициируемыми для элемента.
Данная возможность доступна только в консоли, так что вы не сможете использовать ее в коде страницы. Но если вы работаете с htmx в Chrome и вас интересует, почему событие не инициируется для элемента, откройте консоль разработчика и введите следующую команду:
Листинг 141. Мониторинг htmx
Команда выводит все события, инициируемые для элемента с идентификатором some-element, на консоль. Полученная информация поможет понять, на какие события необходимо реагировать в htmx, или разобраться, почему ожидаемое событие не происходит.
Использование этих двух методов поможет диагностировать (хочется надеяться, нечасто) проблемы, связанные с событиями, при разработке с использованием htmx.
В общем случае решения на основе htmx и гипермедиа оказываются более безопасными, чем подходы к построению веб-приложений, основанные на использовании JavaScript. Дело в том, что благодаря перемещению существенной доли обработки в бэкенд в решениях гипермедиа конечные пользователи обычно не имеют доступа к значительной части системы, чтобы проводить разного рода манипуляции и махинации.
Однако даже в случае использования гипермедиа встречаются ситуации, требующие от разработчика осторожности. Особого внимания заслуживает показ контента, сгенерированного пользователем, другим пользователям: хитрый злоумышленник может вставить в контент код htmx и обманом заставить других пользователей щелкнуть на нем, чтобы запустить действия, которые пользователи выполнять не собирались.
В общем случае весь контент, генерируемый пользователем, должен экранироваться на стороне сервера, а большинство фреймворков рендеринга на стороне сервера предоставляют функциональность для обработки таких ситуаций. Однако всегда существует риск упустить какую-нибудь мелочь.
Чтобы упростить жизнь разработчикам, htmx предоставляет атрибут hx-disable. Если установить этот атрибут для элемента, то все атрибуты htmx внутри этого элемента будут игнорироваться.
Политика безопасности контента, или CSP (Content Security Policy), — браузерная технология, позволяющая обнаруживать и предотвращать некоторые виды атак, основанные на внедрении контента. Полное обсуждение CSP выходит за рамки этой книги, но мы рекомендуем ознакомиться со статьей в Mozilla Developer Network для получения дополнительной информации.
Типичный пример функциональности, блокируемой CSP, — функция eval() в JavaScript, позволяющая выполнить произвольный код JavaScript, содержащийся в строке. Известно, что данная возможность создает риск для безопасности, и многие команды разработчиков решили, что доступность eval() в веб-приложениях того не стоит.
В htmx функция eval() почти не используется, поэтому CSP с таким ограничением будет нормально работать. От eval() зависят только фильтры событий, о которых было рассказано выше. Если вы решите заблокировать eval() в своем веб-приложении, то не сможете пользоваться синтаксисом фильтрации событий.
В htmx доступны многочисленные параметры конфигурации. Несколько примеров того, что можно настраивать в приложениях:
Htmx обычно настраивается в теге meta, находящемся в заголовке страницы. Тегу meta должно быть присвоено имя htmx-config, а атрибут content должен содержать переопределения конфигурации в формате JSON. Пример:
Листинг 142. Конфигурация htmx в теге meta
В данном случае мы переопределяем стиль замены по умолчанию с обычного innerHTML на outerHTML. Это может быть полезно, если вы обнаружите, что в ваших приложениях outerHTML используется чаще innerHTML и лучше избежать необходимости явно задавать это значение.
Тенденция «использовать семантический HTML» вместо того, чтобы «читать спецификации», привела к тому, что многие стремятся угадать смысл тегов («По-моему, очень семантично!») вместо того, чтобы заглянуть в спецификацию.
Мы рекомендуем обсуждать и писать соответствующий HTML. Используйте элементы так, как описано в спецификации HTML, и пусть программные системы извлекают из них смысл на свое усмотрение.
Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Разработка
Опытные программисты, выросшие вместе со Всемирной паутиной, не уделяли идеям гипермедиа особого внимания. А молодые веб-разработчики не знают ничего, кроме одностраничных приложений и фреймворков, используемых для их создания.
Устаревшая технология, подходящая только для создания документов со ссылками, текстом и графикой? Ничего подобного! В вашем распоряжении — эффективная технология для построения приложений.
Познакомьтесь с новыми инструментами — htmx и Hyperview, которые используют гипермедиа в качестве системной архитектуры. Научитесь строить сложные пользовательские интерфейсы с использованием гипермедиа как базовой технологии: на базе htmx для веб-приложений и на базе Hyperview для мобильных. А затем исследуйте прикладные современные подходы к построению веб-приложений, в которых эта архитектура используется.
Гипермедиа-управляемая архитектура подойдет не для каждого приложения, но повышенная гибкость и простота станут огромным преимуществом. Даже если этот подход не улучшит вашу программу, вам стоит понять его суть, сильные и слабые стороны и отличия от традиционно применяемой методики. Веб-среда росла быстрее, чем любая другая распределенная система в истории, и веб-разработчики должны уметь использовать сильные стороны базовых технологий, которые сделали возможным этот рост.
Структура книги
Книга включает три части:
Тем не менее книга создавалась с расчетом на последовательное изучение, и в разделах, посвященных htmx и Hyperview, используется приложение Web 1.0, описанное в конце части I.
Более того, даже если вы хорошо разбираетесь во всех концепциях гипермедиа и деталях HTML и HTTP, мы рекомендуем хотя бы бегло просмотреть несколько первых глав, чтобы освежить эти темы в памяти.
- Введение в гипермедиа, в котором особое внимание уделяется темам HTML и HTTP. Обзор основных концепций гипермедиа завершается созданием простого приложения для управления контактами «в стиле Web 1.0», Contact.app.
- Затем вы научитесь использовать htmx — гипермедиа-ориентированную библиотеку JavaScript, разработанную авторами этой книги, и с ее помощью улучшите приложение Contact.app. Благодаря htmx мы добьемся уровня интерактивности, который многие разработчики посчитали бы невозможным без большой, сложной интерфейсной библиотеки, такой как React. С htmx мы сделаем это, используя гипермедиа в качестве системной архитектуры.
- Наконец, мы рассмотрим Hyperview — совершенно особую мобильную систему гипермедиа, которая имеет отношение к веб-технологиям, но отличается от них. Эту систему создал один из авторов этой книги, Адам Степински. Она поддерживает специфические мобильные возможности за счет предоставления не только мобильных гипермедиа-технологий, но и мобильного гипермедиа-клиента. Эти новые компоненты в сочетании с любым сервером HTTP позволяют создавать мобильные гипермедиа-управляемые приложения.
Тем не менее книга создавалась с расчетом на последовательное изучение, и в разделах, посвященных htmx и Hyperview, используется приложение Web 1.0, описанное в конце части I.
Более того, даже если вы хорошо разбираетесь во всех концепциях гипермедиа и деталях HTML и HTTP, мы рекомендуем хотя бы бегло просмотреть несколько первых глав, чтобы освежить эти темы в памяти.
ХИТРЫЕ ПРИЕМЫ HTMX
Расширенные возможности htmx
В этой главе мы еще больше углубимся в инструменты htmx. Даже с теми из них, что вы уже узнали, можно сделать очень много. Тем не менее при разработке приложений HDA встречаются ситуации, в которых приходится искать дополнительные решения и средства.
В этой главе будут рассмотрены более сложные атрибуты htmx, а также продвинутые возможности использованных ранее атрибутов.
Кроме того, будет рассмотрена функциональность, предоставляемая htmx помимо простых атрибутов HTML: как htmx расширяет стандартные запросы и ответы HTTP, как htmx работает с событиями (и генерирует их) и что делать, если на странице нет простого отдельного целевого элемента, который можно обновить.
Наконец, мы рассмотрим некоторые практические вопросы разработки htmx: как эффективно заниматься отладкой приложения на основе htmx, какие соображения безопасности необходимо учитывать при работе с htmx и как настраивать поведение htmx.
С инструментами и возможностями, описанными в этой главе, вы сможете создавать довольно сложные пользовательские интерфейсы, используя только htmx и, возможно, некоторые подходящие для гипермедиа скрипты на стороне клиента.
Атрибуты htmx
До сих пор мы использовали около 15 атрибутов htmx. Самыми важными из них были:
- hx-get, hx-post и т. д. — для определения запроса AJAX, который должен инициироваться элементом;
- hx-trigger — для определения события, инициирующего запрос;
- hx-swap — для описания подстановки возвращаемого контента HTML в DOM;
- hx-target — для определения того, в какой позиции DOM должен подставляться возвращаемый контент HTML.
hx-swap
Начнем с атрибута hx-swap. Он часто не включается в элементы, выдающие запросы на основе htmx, потому что поведение по умолчанию — innerHTML, заменяющее внутреннюю разметку HTML элемента, — подходит для большинства практических сценариев.
Мы уже познакомились с ситуациями, в которых требовалось переопределить поведение по умолчанию и использовать, например, outerHTML. В главе 2 также были представлены другие варианты замены — beforebegin, afterend и т. д.
В главе 5 мы рассмотрели модификатор задержки swap для hx-swap, при помощи которого можно воспроизвести эффект постепенного скрытия контента перед его удалением из DOM.
Кроме того, hx-swap предлагает дополнительные возможности управления со следующими модификаторами:
- settle — как и swap, позволяет применить установленную задержку между моментом замены контента в DOM и «стабилизацией» его атрибутов (то есть обновления их прежних значений новыми значениями). Это дает возможность точного управления переходами CSS;
- show — позволяет задать элемент, который должен быть показан (с возможной прокруткой в область просмотра браузера, если потребуется) при завершении запроса;
- scroll — позволяет задать прокручиваемый элемент (то есть элемент с полосами прокрутки), который должен быть прокручен к верхней или нижней границе при завершении запроса;
- focus-scroll — позволяет указать, что при завершении запроса htmx следует выполнить прокрутку к элементу, обладающему фокусом. По умолчанию этот модификатор установлен в false.
Листинг 126. Прокрутка к верху страницы
<button hx-get="/contacts" hx-target="#content-div"
hx-swap="innerHTML show:body:top"> ❶
Get Contacts
</button>
❶ Сообщает htmx показать верхнюю границу body после замены.
За более подробной информацией и другими примерами обращайтесь к электронной документации hx-swap.
hx-trigger
Как и hx-swap, атрибут hx-trigger часто опускается при работе с htmx, потому что обычно востребовано его поведение по умолчанию. Напомним, что инициирующие события по умолчанию определяются типом элемента:
- запросы элементов input, textarea и select инициируются событием change;
- запросы элементов form инициируются событием submit;
- запросы всех остальных элементов инициируются событием click.
Листинг 127. Поле ввода с активным поиском
<input id="search" type="search" name="q" value="{{ request.args.get('q')
or '' }}"
hx-get="/contacts"
hx-trigger="search, keyup delay:200ms changed"/> ❶
❶ Расширенная спецификация триггера.
В этом примере используются два модификатора, доступных для атрибута hx-trigger:
- delay — задает задержку перед выдачей запроса. Если событие произойдет повторно в течение заданного интервала, то первое событие отбрасывается и таймер обнуляется. Это позволяет устранять «дребезг» события.
- changed — указывает, что запрос должен выдаваться только при изменении свойства value заданного элемента.
Другие модификаторы, доступные для hx-trigger:
- once — заданное событие инициирует запрос только один раз.
- throttle — позволяет регулировать события, выдавая их не чаще одного раза в заданный промежуток времени. В отличие от delay, первое событие инициируется немедленно, но все последующие события будут инициироваться только по завершении времени регулировки.
- from — селектор CSS, который позволяет выбрать другой элемент для прослушивания событий. Пример использования будет приведен позже в этой главе.
- target — селектор CSS, позволяющий фильтровать только те события, которые происходят в границах заданного элемента. В DOM события «всплывают» к своим родительским элементам, так что событие click для кнопки также будет инициировать событие click для родительского элемента div, и так на всем пути к элементу body. Иногда требуется задать событие непосредственно для конкретного элемента, и модификатор target позволяет это сделать.
- consume — если модификатор имеет значение true, то инициирующее событие будет отменено и не будет распространяться в родительские элементы.
- queue — определяет, как должны формироваться очереди событий в htmx. По умолчанию при получении события-триггера htmx выдает запрос и запускает очередь событий. Если запрос еще не обработан на момент получения следующего события, то событие помещается в очередь, а при завершении запроса выдается новый запрос. По умолчанию в очереди хранится только последнее полученное событие, но такое поведение можно изменить при помощи этого модификатора: например, можно присвоить значение none и игнорировать все события-триггеры, происходящие во время обработки запроса.
Фильтры триггеров
Атрибут hx-trigger также позволяет задать фильтр для событий. Для этого после имени события в квадратных скобках указывается выражение JavaScript.
Представим сложный сценарий, в котором получение контактов должно быть разрешено только при определенных условиях. Имеется функция JavaScript contactRetrievalEnabled(), которая возвращает логический признак: true, если получение контактов разрешено, и false в остальных случаях. Как использовать эту функцию для ограничения доступа к кнопке, выдающей запрос к /contacts?
Чтобы решить эту задачу с использованием фильтра событий в htmx, напишите следующую разметку HTML.
Листинг 128. Поле ввода с активным поиском
<script>
function contactRetrievalEnabled() {
// Код, проверяющий, разрешено ли получение контактов
...
}
</script>
<button hx-get="/contacts" hx-trigger="click[contactRetrievalEnabled()]"> ❶
Get Contacts
</button>
❶ Запрос выдается по событию click только в том случае, если contactRetrievalEnabled() возвращает true.
Кнопка не выдает запрос, если contactRetrievalEnabled() возвращает false, что позволяет динамически управлять возможностью выдачи запросов. Многие типичные сценарии требуют использования триггера события, тогда как запрос должен выдаваться только в определенных обстоятельствах:
- если определенный элемент обладает фокусом;
- если заданная форма содержит проверенные данные;
- если группа полей ввода содержит конкретные значения.
Синтетические события
Кроме перечисленных модификаторов, hx-trigger предоставляет несколько «синтетических» событий, то есть событий, не являющихся частью обычного DOM API. События load и revealed уже встречались в примерах отложенной загрузки и бесконечной прокрутки, но htmx также предоставляет событие intersect, которое срабатывает при пересечении элемента с его родительским элементом.
Это синтетическое событие использует современный API наблюдателей пересечения, о котором можно больше узнать из MDN.
Пересечения предоставляют возможность более точно контролировать, когда именно должен инициироваться запрос. Например, можно установить порог и указать, что запрос должен выдаваться только в том случае, если элемент виден на 50 %.
Безусловно, hx-trigger — самый сложный из атрибутов htmx. За более подробной информацией о нем и другими примерами обращайтесь к электронной документации.
Другие атрибуты
Htmx предоставляет множество других, реже используемых атрибутов для настройки поведения гипермедиа-управляемых приложений.
Ниже перечислены самые полезные из этих атрибутов:
- hx-push-url — «проталкивает» URL запроса (или другое значение) в адресную строку.
- hx-preserve — сохраняет часть DOM между запросами; исходный контент будет сохранен независимо от того, что будет возвращено.
- hx-sync — синхронизирует запросы между двумя и более элементами.
- hx-disable — отключает поведение htmx для этого элемента и всех его дочерних элементов. Мы вернемся к этой теме, когда будем обсуждать проблему безопасности.
Листинг 129. Две конкурирующие кнопки
<button hx-get="/contacts" hx-target="body">
Get Contacts
</button>
<button hx-get="/settings" hx-target="body">
Get Settings
</button>
Все работает нормально, но что, если пользователь щелкнет на кнопке Get Contacts (Контакты), а ответ на запрос будет получен не сразу? И за это время он успеет щелкнуть по кнопке Get Settings (Настройки)? В таком случае будут существовать два необработанных запроса одновременно.
Предположим, запрос /settings завершается первым и выводит информацию о настройках. Пользователь начинает вносить изменения, но тут его ждет сюрприз — запрос к /contacts завершается и заменяет все тело документа информацией о контактах!
Чтобы решить эту проблему, можно воспользоваться hx-indicator и уведомить пользователя о том, что выполняется определенный процесс, снижая вероятность щелчка второй кнопки. Но если необходимо действительно предусмотреть, чтобы в любой момент времени между этими кнопками существовал только один запрос, следует воспользоваться атрибутом hx-sync. Заключим обе кнопки в тег div и исключим лишнюю спецификацию hx-target, переместив атрибут вверх в div. Затем можно использовать hx-sync с div, чтобы координировать запросы между двумя кнопками.
Обновленный код выглядит так:
<div hx-target="body" ❶
hx-sync="this"> ❷
<button hx-get="/contacts"> ❶
Get Contacts
</button>
<button hx-get="/settings"> ❶
Get Settings
</button>
</div>
❶ Повторяющиеся атрибуты hx-target поднимаются в родительский элемент div.
❷ Синхронизация по родительскому div.
Размещая в div атрибут hx-sync со значением this, мы говорим: «Синхронизировать все запросы htmx, инициируемые в этом элементе div, по отношению друг к другу». Это означает, что, если одна кнопка уже выдала незавершенный запрос, другие кнопки внутри div не смогут выдавать запросы до его завершения.
Атрибут hx-sync также поддерживает другие стратегии, которые позволяют, например, заменить существующий запрос «на лету» или ставить запросы в очередь с конкретной стратегией организации очереди. Полную документацию вместе с примерами можно найти на странице hx-sync на сайте htmx.org.
Как видите, htmx предлагает значительную функциональность, управляемую атрибутами, для более мощных гипермедиа-управляемых приложений. Полную справку по всем атрибутам htmx можно найти на сайте htmx.
События
До сих пор мы работали с событиями JavaScript в htmx в основном через атрибут hx-trigger. Этот атрибут предоставляет эффективный механизм управления приложением с использованием декларативного синтаксиса, хорошо сочетающегося с HTML.
Тем не менее на этом возможности событий не исчерпаны. События играют важнейшую роль в расширении HTM как среды гипермедиа, а также, как вы вскоре убедитесь, в дружественных ей скриптах.
События — это «клей», связывающий воедино DOM, HTML, htmx и скрипты. Можно даже рассматривать DOM как сложную «шину событий» для приложений.
Очень важно: чтобы строить продвинутые гипермедиа-управляемые приложения, необходимо хорошо разобраться в событиях. Поверьте, вы не пожалеете о потраченном времени.
События, генерируемые htmx
Кроме простой обработки событий, htmx генерирует много полезных событий. Эти события могут использоваться для добавления новой функциональности в приложения — либо через саму библиотеку htmx, либо через скрипты.
Некоторые события, чаще всего генерируемые в htmx:
- htmx:load — инициируется при загрузке нового контента в DOM библиотекой htmx;
- htmx:configRequest — инициируется перед выдачей запроса, позволяя запрограммировать запрос или полностью отменить его;
- htmx:afterRequest — инициируется после ответа на запрос;
- htmx:abort — нестандартное событие, которое может быть отправлено элементу на основе htmx для отмены открытого запроса.
Использование события htmx:configRequest
Рассмотрим пример использования событий, генерируемых htmx. Мы воспользуемся событием htmx:configRequest для настройки запроса HTTP.
Представьте следующую ситуацию: команда, пишущая код на стороне сервера, решила, что для повышения безопасности в каждый запрос должен включаться маркер, сгенерированный сервером. Маркер будет храниться в локальном хранилище (localStorage) браузера, в слоте special-token.
Маркер будет назначаться кодом JavaScript (пока не думайте о подробностях) при первом входе пользователя в приложение.
Листинг 130. Получение маркера в JavaScript
let response = await fetch("/token"); ❶
localStorage['special-token'] = await response.text();
❶ Получает значение маркера, а затем сохраняет его в localStorage.
Серверная команда требует, чтобы вы включали этот специальный маркер при каждом запросе, выполняемом htmx, в заголовке X-SPECIAL-TOKEN. Как этого добиться? Один из способов основан на перехвате события htmx:configRequest и обновлении объекта detail.headers маркером из localStorage.
В «базовом» JS это будет выглядеть примерно так (код включается в тег script в теге документа HTML):
Листинг 131. Добавление заголовка X-SPECIAL-TOKEN
document.body.addEventListener("htmx:configRequest", function(configEvent){
configEvent.detail.headers['X-SPECIAL-TOKEN'] = localStorage['special-
token']; ❶
})
❶ Получает значение из локального хранилища и записывает его в заголовок.
Как видите, мы добавляем новое значение в свойство headers свойства detail события. После выполнения обработчика события свойство headers читается htmx и используется для построения заголовков генерируемого запроса AJAX.
Свойство detail события htmx:configRequest содержит полезные атрибуты, которые можно обновлять для изменения «структуры» запроса, в том числе:
- detail.parameters — позволяет добавлять или удалять параметры запроса;
- detail.target — позволяет обновить цель запроса;
- detail.verb — позволяет обновить «команду» HTTP запроса (то есть GET).
Листинг 132. Добавление параметра token
document.body.addEventListener("htmx:configRequest", function(configEvent){
configEvent.detail.parameters['token'] = localStorage['special-token'];
❶
})
❶ Получает значение из локального хранилища и присваивает его параметру.
Как видите, это решение обеспечивает значительную гибкость при обновлении запроса AJAX, выдаваемого htmx.
Полная документация по событию htmx:configRequest (и другим событиям, которые могут вас заинтересовать) находится на сайте htmx.
Отмена запроса с использованием htmx:abort
В htmx можно прослушивать множество полезных событий и реагировать на них при помощи hx-trigger. А что еще можно делать с событиями?
Сама библиотека htmx прослушивает одно специальное событие htmx:abort. Когда htmx получает это событие для незавершенного запроса, этот запрос отменяется.
Рассмотрим сценарий, в котором имеется потенциально долго выполняющийся запрос к /contacts и необходимо предоставить пользователю возможность отменить этот запрос. Для этого нужны кнопка, выдающая запрос (конечно, под управлением htmx), и другая кнопка, которая отправляет событие htmx:abort первой кнопке.
Код может выглядеть примерно так:
Листинг 133. Кнопка с возможностью отмены
<button id="contacts-btn" hx-get="/contacts" hx-target="body"> ❶
Get Contacts
</button>
<button onclick="document.getElementById('contacts-btn').dispatchEvent(new
Event('htmx:abort'))"> ❷
Cancel
</button>
❶ Обычный запрос GET на основе htmx к /contacts.
❷ Код JavaScript для поиска кнопки и отправки ей события htxm:abort.
Если пользователь щелкает на кнопке Get Contacts и запрос занимает некоторое время, пользователь может щелкнуть на кнопке Cancel и отменить запрос. Конечно, в более сложном пользовательском интерфейсе кнопка Cancel должна блокироваться при отсутствии незавершенного запроса HTTP, но реализация этой функциональности на «чистом» HTML будет слишком хлопотной.
К счастью, в _hyperscript реализация достаточно проста. Результат будет выглядеть примерно так:
Листинг 134. Кнопка с возможностью отмены на основе _hyperscript
<button id="contacts-btn" hx-get="/contacts" hx-target="body">
Get Contacts
</button>
<button _="on click send htmx:abort to #contacts-btn
on htmx:beforeRequest from #contacts-btn remove @disabled from me
on htmx:afterRequest from #contacts-btn add @disabled to me">
Cancel
</button>
Теперь кнопка Cancel блокируется только при наличии незавершенного запроса от кнопки contacts-btn. И чтобы это решение работало, мы используем события, генерируемые и обрабатываемые средствами htmx, а также синтаксис _hyperscript, хорошо сочетающийся с событиями.
События, генерируемые сервером
В следующем разделе мы продолжим разговор о том, как htmx расширяет обычные запросы и ответы HTTP, но так как этот механизм основан на событиях, рассмотрим один заголовок ответа HTTP, поддерживаемый htmx: HX-Trigger. Ранее вы узнали, как запросы и ответы HTTP поддерживают заголовки — пары «имя-значение», содержащие метаданные о запросе или ответе. В частности, мы рассмотрели заголовок запроса HX-Trigger, включающий идентификатор элемента, инициирующего заданный запрос.
Кроме заголововка запроса, htmx поддерживает заголовок ответа с именем HX-Trigger. Заголовок ответа позволяет инициировать событие для элемента, отправившего запрос AJAX. Это мощный механизм координации элементов в DOM без сильной связанности.
Чтобы понять, как он работает, рассмотрим следующий сценарий: имеется кнопка, которая получает новые контакты с удаленной системы на сервере. Подробности реализации на стороне сервера нас сейчас не интересуют, но мы знаем, что при выдаче запроса POST к пути /sync будет инициирована синхронизация с системой.
Синхронизация может привести (а может и не привести) к созданию новых контактов. При создании новых контактов необходимо обновить таблицу контактов. Если же контакты не создаются, то обновлять таблицу не нужно.
Чтобы реализовать эту функциональность, можно по условию добавить заголовок ответа HX-Trigger со значением contacts-updated.
Листинг 135. Условное инициирование события contacts-updated
@app.route('/sync', methods=["POST"])
def sync_with_server():
contacts_updated = RemoteServer.sync() ❶
resp = make_response(render_template('sync.html'))
if contacts_updated ❷
resp.headers['HX-Trigger'] = 'contacts-updated'
return resp
❶ Вызов к удаленной системе, который синхронизирует базу данных контактов.
❷ Если какие-либо контакты были обновлены, клиент инициирует событие contacts-updated по условию.
Это значение инициирует событие contacts-updated для кнопки, которая выдает запрос AJAX к /sync. Затем можно воспользоваться модификатором from: атрибута hx-trigger для прослушивания этого события. С таким паттерном можно фактически инициировать запросы htmx со стороны сервера.
Код на стороне клиента может выглядеть так:
Листинг 136. Таблица Contacts
<button hx-post="/integrations/1"> ❶
Pull Contacts From Integration
</button>
...
<table hx-get="/contacts/table" hx-trigger="contacts-updated from:body"> ❷
...
</table>
❶ Ответ на этот запрос может инициировать событие contacts-updated по условию.
❷ Таблица прослушивает событие и обновляется при его возникновении.
Таблица прослушивает событие contacts-updated, причем делает это на уровне элемента body. Она прослушивает элемент body, так как событие всплывает вверх от кнопки, и это позволяет избежать сильной связанности кнопки с таблицей: кнопку и таблицу можно перемещать как угодно, но благодаря событиям нужное поведение продолжит нормально работать. Кроме того, может потребоваться, чтобы событие contacts-updated инициировалось другими элементами или запросами, поэтому эта схема обеспечивает обобщенный механизм обновления таблицы контактов в приложении.
Запросы и ответы HTTP
Вы уже видели одну расширенную возможность ответов HTTP, поддерживаемую htmx, — заголовок ответа HX-Trigger; но htmx поддерживает и другие заголовки запросов и ответов. В главе 4 рассматривались заголовки, присутствующие в за просах HTTP. Вот некоторые важные заголовки, используемые для изменения поведения htmx с ответами HTTP:
- HX-Location — инициирует перенаправление на стороне клиента;
- HX-Push-Url — заносит новый URL в адресную строку браузера;
- HX-Refresh — обновляет текущую страницу;
- HX-Retarget — позволяет задать новую цель для замены контента ответа на стороне клиента.
Коды ответов HTTP
Пожалуй, в отношении информации, передаваемой клиенту, коды ответов HTTP даже важнее заголовков ответов. Коды ответов HTTP рассматривались в главе 3. В целом htmx обрабатывает коды ответов ожидаемым образом: для всех кодов ответов уровня 200 выполняется замена контента, а для остальных ничего не происходит. Тем не менее существуют два «специальных» кода ответов уровня 200:
- 204 No Content (Нет содержимого) — при получении этого кода ответа htmx не подставляет контент в DOM (даже если у ответа есть тело);
- 286 — при получении этого кода ответа на запрос, для которого выполняется опрос (polling), htmx прерывает опрос.
Предположим, что вместо того, чтобы ничего не делать при получении кода 404, вы хотите оповестить пользователя о возникшей ошибке при помощи вызова метода JavaScript showNotFoundError(). Для этого добавим следующий фрагмент кода в событие htmx:beforeSwap.
Листинг 137. Вывод диалогового окна 404
document.body.addEventListener('htmx:beforeSwap', function(evt) { ❶
if(evt.detail.xhr.status === 404){ ❷
showNotFoundError();
}
});
❶ Подключение к событию htmx:beforeSwap.
❷ Если код ответа равен 404, вывести диалоговое окно.
Для определения того, должен ли ответ подставляться в DOM и какой элемент назначать целевым для ответа, также можно воспользоваться событием htmx:beforeSwap. Оно обеспечивает некоторую свободу выбора способа использования кодов ответов HTTP в приложении. Полная документация для события htmx:beforeSwap доступна на сайте htmx.org.
Обновление остального контента
Выше мы увидели, как использовать событие, инициируемое сервером через заголовок ответа HX-Trigger, чтобы обновить блок DOM, который основан на ответе другого блока DOM. Этот прием помогает решить общую проблему гипермедиа-управляемых приложений: как обновить остальной контент? Ведь в обычных запросах HTTP существует только одна «цель» — весь экран; аналогичным образом в запросах на основе htmx есть только одна цель: явная или неявная цель элемента.
Рассмотрим несколько вариантов того, как обновить другой контент в htmx.
Расширение выделения
Первый и самый простой вариант — «расширение цели». Иначе говоря, вместо того чтобы заменять небольшую часть экрана, расширяйте цель запроса на основе htmx, пока она не будет включать все элементы, которые должны обновляться на экране. У такого решения есть два огромных преимущества — простота и надежность. Недостаток заключается в том, что оно не предоставляет желаемого взаимодействия с пользователем и не всегда хорошо сочетается с некоторыми макетами шаблонов на стороне сервера. Тем не менее мы всегда рекомендуем для начала хотя бы рассмотреть возможность применения этого подхода.
Внеполосная замена
Второй, чуть более сложный вариант основан на использовании поддержки «внеполосного» (Out Of Band) контента в htmx. При получении ответа htmx проверяет его и ищет контент верхнего уровня, включающий атрибут hx-swap-oob. Этот контент исключается из ответа и не подставляется в DOM обычным образом. Вместо этого он заменяет контент с совпадающим идентификатором.
Рассмотрим конкретный пример. Вспомните ситуацию, описанную выше, когда таблица контактов должна была обновляться при создании новых контактов. Тогда проблема решалась с использованием событий и событием, инициируемым сервером через заголовок ответа HX-Trigger.
На этот раз мы воспользуемся атрибутом hx-swap-oob в ответе на запрос POST к /integrations/1. Новый контент таблицы контактов присоединяется к ответу.
Листинг 138. Обновленная таблица контактов
<button hx-post="/integrations/1"> ❶
Pull Contacts From Integration
</button>
...
<table id="contacts-table"> ❷
...
</table>
❶ Кнопка все еще выдает запрос POST к /integrations/1.
❷ Таблица уже не прослушивает событие, но теперь ей назначается идентификатор.
Ответ на запрос POST к /integrations/1 будет включать контент, который должен подставляться в кнопку с использованием обычного механизма htmx. Однако он включает новую, обновленную версию таблицы контактов, которая помечается атрибутом hx-swap-oob=«true». Контент будет удален из ответа, чтобы он не был вставлен в кнопку. Вместо этого он подставляется в DOM на место существующей таблицы, так как их идентификаторы совпадают.
Листинг 139. Ответ с внеполосным контентом
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
...
Pull Contacts From Integration ❶
<table id="contacts-table" hx-swap-oob="true"> ❷
...
</table>
❶ Этот контент будет помещен в кнопке.
❷ Этот контент будет удален из ответа и заменен в соответствии с идентификатором.
Используя этот прием, можно обновлять контент страницы там, где потребуется. Атрибут hx-swap-oob предоставляет другие возможности, но все они документированы.
В зависимости от того, насколько точно работает технология шаблонов на стороне сервера и какой уровень интерактивности необходим приложению, внеполосная замена может стать мощным механизмом обновления контента.
События
Наконец, самый сложный механизм обновления контента был описан в разделе, посвященном событиям: обновление элементов с помощью событий, инициируемых сервером. Этот механизм может быть оптимальным, но он требует глубокого концептуального знания HTML и событий, а также соблюдения событийного подхода. Хотя нам нравится такой стиль разработки, он не для всех. Обычно мы рекомендуем этот паттерн только в том случае, если философия htmx событийного гипермедиа действительно пришлась вам по душе.
Но если она пришлась вам по душе — выбирайте ее. Мы создавали очень сложные и гибкие пользовательские интерфейсы, используя этот механизм, и он нам очень нравится.
Прагматичный подход
Все решения проблемы «обновления остального контента» работают, и часто работают хорошо. Однако может наступить момент, когда будет проще выбрать другой подход, например реактивный. Как бы нам ни нравились решения гипермедиа, реальность такова, что некоторые паттерны UX просто невозможно с легкостью реализовать в этой среде. Классическим примером таких паттернов, уже упоминавшимся выше, является электронная онлайн-таблица: ее пользовательский интерфейс слишком сложен и содержит слишком много взаимозависимостей, чтобы его можно было качественно реализовать через обмен контентом гипермедиа с сервером.
В таких случаях (и каждый раз, когда вы чувствуете, что решение на основе htmx оказывается более сложным, чем другой подход) мы рекомендуем рассмотреть другие технологии. Будьте прагматичны и выбирайте подходящий инструмент для работы. Вы всегда можете использовать htmx для тех частей приложения, которые менее сложны и не требуют всех возможностей реактивного фреймворка, и сэкономить бюджет сложности для других частей.
Мы рекомендуем изучить больше разных веб-технологий, обращая внимание на сильные и слабые стороны каждой. Так у вас сформируется обширный инструментарий, к которому вы сможете обратиться при работе над очередной задачей. По нашему опыту, с htmx гипермедиа станет тем инструментом, к которому вы будете обращаться чаще всего.
Отладка
Нам не стыдно признаться: мы большие поклонники событий. Эта технология лежит в основе практически каждого интересного пользовательского интерфейса. После того как вы получите доступ к обобщенному использованию событий в HTML, они станут особенно полезны в DOM. События позволяют строить слабо связанные программные системы, при этом часто сохраняя локальность поведения, которую мы так ценим.
Тем не менее события неидеальны. Одной из областей, в которых события создают особенно много проблем, является отладка: часто требуется узнать, почему событие не инициируется. Но как установить точку останова для чего-то, что не происходит? Никак (по крайней мере, пока).
Существуют два приема, которые могут помочь с отладкой. Первый предоставляется htmx, а другой — Chrome, браузером от Google.
Регистрация событий htmx
Первый прием, предоставляемый самой библиотекой htmx, заключается в вызове метода htmx.logAll(). При этом htmx регистрирует в журнале все внутренние события, происходящие при выполнении бизнес-логики, загрузке контента, реакции на события и т. д.
Это может привести к информационной перегрузке, но грамотная фильтрация поможет справиться с проблемой. Вот как выглядит журнал (вернее, его небольшая часть), сохраняемый по щелчку на ссылке «docs» на сайте htmx.org при включенном режиме logAll().
Листинг 140. Журнал htmx
htmx:configRequest
<a href="/docs/">
Object { parameters: {}, unfilteredParameters: {}, headers: {…}, target:
body, verb: "get", errors: [], withCredentials: false, timeout: 0, path:
"/docs/", triggeringEvent: a
, … }
htmx.js:439:29
htmx:beforeRequest
<a href="/docs/">
Object { xhr: XMLHttpRequest, target: body, requestConfig: {…}, etc: {},
pathInfo: {…}, elt: a
}
htmx.js:439:29
htmx:beforeSend
<a class="htmx-request" href="/docs/">
Object { xhr: XMLHttpRequest, target: body, requestConfig: {…}, etc: {},
pathInfo: {…}, elt: a.htmx-request
}
htmx.js:439:29
htmx:xhr:loadstart
<a class="htmx-request" href="/docs/">
Object { lengthComputable: false, loaded: 0, total: 0, elt: a.htmx-request
}
htmx.js:439:29
htmx:xhr:progress
<a class="htmx-request" href="/docs/">
Object { lengthComputable: true, loaded: 4096, total: 19915, elt: a.htmx-
request
}
htmx.js:439:29
htmx:xhr:progress
<a class="htmx-request" href="/docs/">
Object { lengthComputable: true, loaded: 19915, total: 19915, elt: a.htmx-
request
}
htmx.js:439:29
htmx:beforeOnLoad
<a class="htmx-request" href="/docs/">
Object { xhr: XMLHttpRequest, target: body, requestConfig: {…}, etc: {},
pathInfo: {…}, elt: a.htmx-request
}
htmx.js:439:29
htmx:beforeSwap
<body hx-ext="class-tools, preload">
Не самое легкое чтиво, правда?
Но если сделать глубокий вдох и присмотреться, вы увидите, что не все так плохо: перед вами серия событий htmx, и некоторые из них уже вам знакомы (как htmx:configRequest!), вместе с элементами, которые их инициировали.
Немного привыкнув к чтению и фильтрации информации в журналах, вы научитесь ориентироваться в потоке событий. Это поможет вам в отладке проблем, связанных с htmx.
Мониторинг событий в Chrome
Описанный метод полезен, если проблема где-то внутри htmx, но что, если htmx вообще не инициируется? Такое иногда случается, например, когда вы неправильно ввели имя события.
В таких случаях приходится пользоваться инструментами, доступными в самом браузере. К счастью, браузер Google Chrome предоставляет очень полезную функцию monitorEvents(), которая позволяет наблюдать за всеми событиями, инициируемыми для элемента.
Данная возможность доступна только в консоли, так что вы не сможете использовать ее в коде страницы. Но если вы работаете с htmx в Chrome и вас интересует, почему событие не инициируется для элемента, откройте консоль разработчика и введите следующую команду:
Листинг 141. Мониторинг htmx
monitorEvents(document.getElementById("some-element"));
Команда выводит все события, инициируемые для элемента с идентификатором some-element, на консоль. Полученная информация поможет понять, на какие события необходимо реагировать в htmx, или разобраться, почему ожидаемое событие не происходит.
Использование этих двух методов поможет диагностировать (хочется надеяться, нечасто) проблемы, связанные с событиями, при разработке с использованием htmx.
Соображения безопасности
В общем случае решения на основе htmx и гипермедиа оказываются более безопасными, чем подходы к построению веб-приложений, основанные на использовании JavaScript. Дело в том, что благодаря перемещению существенной доли обработки в бэкенд в решениях гипермедиа конечные пользователи обычно не имеют доступа к значительной части системы, чтобы проводить разного рода манипуляции и махинации.
Однако даже в случае использования гипермедиа встречаются ситуации, требующие от разработчика осторожности. Особого внимания заслуживает показ контента, сгенерированного пользователем, другим пользователям: хитрый злоумышленник может вставить в контент код htmx и обманом заставить других пользователей щелкнуть на нем, чтобы запустить действия, которые пользователи выполнять не собирались.
В общем случае весь контент, генерируемый пользователем, должен экранироваться на стороне сервера, а большинство фреймворков рендеринга на стороне сервера предоставляют функциональность для обработки таких ситуаций. Однако всегда существует риск упустить какую-нибудь мелочь.
Чтобы упростить жизнь разработчикам, htmx предоставляет атрибут hx-disable. Если установить этот атрибут для элемента, то все атрибуты htmx внутри этого элемента будут игнорироваться.
Политики безопасности контента и htmx
Политика безопасности контента, или CSP (Content Security Policy), — браузерная технология, позволяющая обнаруживать и предотвращать некоторые виды атак, основанные на внедрении контента. Полное обсуждение CSP выходит за рамки этой книги, но мы рекомендуем ознакомиться со статьей в Mozilla Developer Network для получения дополнительной информации.
Типичный пример функциональности, блокируемой CSP, — функция eval() в JavaScript, позволяющая выполнить произвольный код JavaScript, содержащийся в строке. Известно, что данная возможность создает риск для безопасности, и многие команды разработчиков решили, что доступность eval() в веб-приложениях того не стоит.
В htmx функция eval() почти не используется, поэтому CSP с таким ограничением будет нормально работать. От eval() зависят только фильтры событий, о которых было рассказано выше. Если вы решите заблокировать eval() в своем веб-приложении, то не сможете пользоваться синтаксисом фильтрации событий.
Конфигурация
В htmx доступны многочисленные параметры конфигурации. Несколько примеров того, что можно настраивать в приложениях:
- стиль замены по умолчанию;
- задержку замены по умолчанию;
- тайм-аут запросов AJAX по умолчанию.
Htmx обычно настраивается в теге meta, находящемся в заголовке страницы. Тегу meta должно быть присвоено имя htmx-config, а атрибут content должен содержать переопределения конфигурации в формате JSON. Пример:
Листинг 142. Конфигурация htmx в теге meta
<meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML"}'>
В данном случае мы переопределяем стиль замены по умолчанию с обычного innerHTML на outerHTML. Это может быть полезно, если вы обнаружите, что в ваших приложениях outerHTML используется чаще innerHTML и лучше избежать необходимости явно задавать это значение.
Заметки об HTML: семантический HTML
Тенденция «использовать семантический HTML» вместо того, чтобы «читать спецификации», привела к тому, что многие стремятся угадать смысл тегов («По-моему, очень семантично!») вместо того, чтобы заглянуть в спецификацию.
Я считаю, что когда вам предлагают писать содержательный HTML, дело вовсе не в смысле текста для человека. Дело в использовании тегов для цели, описанной в спецификации, — удовлетворения потребностей таких программных систем, как браузеры, технологии доступности и поисковые системы.
t-ravis.com/post/doc/semantic_the_8_letter_s-word
Мы рекомендуем обсуждать и писать соответствующий HTML. Используйте элементы так, как описано в спецификации HTML, и пусть программные системы извлекают из них смысл на свое усмотрение.
Об авторах
Карсон Гросс — сеньор-разработчик с богатым 30-летним опытом создания как фронтенд-, так и бэкенд-приложений. Сооснователь и технический директор компании LeadDyno. Читает курсы в Университете Монтаны (MSU, Montana State University). Главная сфера интересов — языки программирования и веб-разработка на основе технологий гипермедиа.
Адам Степински — директор по инженерным вопросам в компании Instawork. У него более 15 лет опыта разработки и масштабирования технологических платформ в различных компаниях, от стартапов до Google. Адам — создатель Hyperview, мобильной системы гипермедиа.
Денис Акшимшек — инженер-разработчик, увлекающийся гипермедиа и вопросами доступности. Работал в компаниях Rebase Ventures (Великобритания), Big Sky Software (США) и Commspace (ЮАР), где создавал фулстек-приложения, веб-сайты и скрипты.
Адам Степински — директор по инженерным вопросам в компании Instawork. У него более 15 лет опыта разработки и масштабирования технологических платформ в различных компаниях, от стартапов до Google. Адам — создатель Hyperview, мобильной системы гипермедиа.
Денис Акшимшек — инженер-разработчик, увлекающийся гипермедиа и вопросами доступности. Работал в компаниях Rebase Ventures (Великобритания), Big Sky Software (США) и Commspace (ЮАР), где создавал фулстек-приложения, веб-сайты и скрипты.
Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Разработка