Глава 06. Больше шаблонов Htmx
Активный поиск
С Contact.app пока все хорошо: у нас есть симпатичное маленькое веб-приложение с некоторыми значительными улучшениями по сравнению с простым приложением на основе HTML. Мы добавили правильную кнопку «Удалить контакт», выполнили динамическую проверку ввода и рассмотрели различные подходы к добавлению пейджинга в приложение. Как мы уже говорили, многие веб-разработчики ожидали, что для реализации этих функций потребуется множество сценариев на основе JavaScript, но мы сделали все это в относительно чистом HTML, используя только атрибуты htmx.
Со временем мы добавим в наше приложение некоторые сценарии на стороне клиента: гипермедиа — это мощный инструмент, но он не полностью эффективен , и иногда сценарии могут быть лучшим (или единственным) способом достижения определенной цели. А пока давайте посмотрим, чего мы можем достичь с помощью гипермедиа.
Первая расширенная функция HTML, которую мы создадим, известна как шаблон «Активный поиск». Активный поиск — это когда, когда пользователь вводит текст в поле поиска, результаты этого поиска динамически отображаются. Этот шаблон стал популярным, когда Google применил его для результатов поиска, и теперь его реализуют многие приложения.
Для реализации активного поиска мы собираемся использовать методы, тесно связанные с тем, как мы выполняли проверку электронной почты в предыдущей главе. Если подумать, эти две функции во многом схожи: в обоих случаях мы хотим выдать запрос, когда пользователь вводит данные, а затем обновить какой-то другой элемент ответом. Реализации на стороне сервера, конечно, будут сильно отличаться, но код внешнего интерфейса будет выглядеть довольно похожим из-за общего подхода htmx: «выдать запрос на событие и заменить что-то на экране».
Наш текущий интерфейс поиска
Давайте вспомним, как сейчас выглядит поле поиска в нашем приложении:
Параметр
q
или «query», который наш клиентский код использует для поиска.
Напомним, что у нас есть серверный код, который ищет параметр q
и, если он присутствует, ищет контакты по этому термину.
В нынешнем виде пользователь должен нажать Enter, когда поле поиска находится в фокусе, или нажать кнопку «Поиск». Оба этих события вызовут submit
событие в форме, заставив ее выдать HTTP GET
и повторно отобразить всю страницу.
В настоящее время, благодаря hx-boost
, форма будет использовать для этого запрос AJAX GET
, но мы пока не получаем того приятного поведения поиска по мере ввода, которое нам нужно.
Добавление активного поиска
Чтобы добавить активное поведение поиска, мы добавим несколько атрибутов htmx к входным данным поиска. Мы оставим текущую форму как есть, со значками action
и method
, чтобы нормальное поведение поиска работало, даже если у пользователя не включен JavaScript. Это сделает наше улучшение «Активный поиск» приятным «прогрессивным улучшением».
Итак, в дополнение к обычному поведению формы, мы также хотим выдавать HTTP- GET
запрос при нажатии клавиши. Мы хотим отправить этот запрос на тот же URL-адрес, что и при отправке обычной формы. Наконец, мы хотим сделать это только после небольшой паузы при наборе текста.
Как мы уже говорили, эта функциональность очень похожа на то, что нам нужно для проверки электронной почты. Фактически мы можем скопировать hx-trigger
атрибут непосредственно из нашего примера проверки электронной почты с небольшой задержкой в 200 миллисекунд, чтобы позволить пользователю прекратить ввод до того, как будет запущен запрос.
Это еще один пример того, как общие шаблоны возникают снова и снова при использовании htmx.
Сохраните исходные атрибуты, чтобы поиск работал, даже если JavaScript недоступен.
Введите a
GET
на тот же URL-адрес, что и форма.Почти та же
hx-trigger
спецификация, что и для проверки ввода электронной почты.
Мы внесли небольшое изменение в hx-trigger
атрибут: заменили change
событие на search
событие. Событие search
срабатывает, когда кто-то очищает поиск или нажимает клавишу ввода. Событие нестандартное, но включить сюда не помешает. Основная функциональность функции обеспечивается вторым триггерным событием — keyup
. Как и в примере с электронной почтой, этот триггер задерживается с помощью delay:200ms
модификатора, чтобы «устранить дребезг» входных запросов и избежать перегрузки нашего сервера запросами при каждом нажатии клавиши.
Выбор правильного элемента
То, что мы имеем, близко к тому, что мы хотим, но нам нужно установить правильную цель. Напомним, что целью по умолчанию для элемента является он сам. GET
В настоящее время по этому пути будет отправлен HTTP- запрос /contacts
, который на данный момент будет возвращать весь HTML-документ с результатами поиска, а затем весь этот документ будет вставлен во внутренний HTML -код входных данных поиска.
На самом деле это чепуха: input
элементам не разрешается содержать внутри себя какой-либо HTML. Браузер, разумно, просто проигнорирует запрос htmx, чтобы поместить ответный HTML-код во входные данные. Итак, на этом этапе, когда пользователь вводит что-либо в наш ввод, будет выдан запрос (вы можете увидеть его в консоли разработки вашего браузера, если попробуете), но, к сожалению, пользователю это покажется так, как будто ничего не произошло. произошло вообще.
Чтобы решить эту проблему, на что мы хотим нацелить обновление? В идеале мы хотели бы ориентироваться только на фактические результаты: нет причин обновлять заголовок или вводимые данные для поиска, и это может вызвать раздражающую вспышку, когда фокус будет прыгать.
Атрибут hx-target
позволяет нам сделать именно это. Давайте используем его для нацеливания на тело результатов, элемент tbody
в таблице контактов:
Нацельтесь на
tbody
тег на странице.
Поскольку на странице есть только один элемент tbody
, мы можем использовать общий селектор CSS, tbody
и htmx будет нацелен на тело таблицы на странице.
Теперь, если вы попытаетесь ввести что-то в поле поиска, мы увидим некоторые результаты: делается запрос, и результаты вставляются в документ в формате tbody
. К сожалению, возвращаемый контент по-прежнему представляет собой целый HTML-документ.
Здесь мы получаем ситуацию «двойного рендеринга», когда весь документ был вставлен внутрь другого элемента, при этом вся навигация, верхние и нижние колонтитулы и т. д. повторно визуализировались внутри этого элемента. Это пример одной из тех проблем неправильного таргетинга, о которых мы упоминали ранее.
К счастью, это довольно легко исправить.
Уменьшение содержания нашего контента
Теперь мы могли бы использовать тот же прием, который мы использовали в функциях «Нажмите, чтобы загрузить» и «Бесконечная прокрутка»: атрибут hx-select
. Напомним, что hx-select
атрибут позволяет нам выбрать интересующую нас часть ответа с помощью селектора CSS.
Итак, мы могли бы добавить это к нашему входу:
Добавление
hx-select
, которое выбирает строки таблицы вtbody
ответе.
Однако это не единственное решение этой проблемы, и в данном случае оно не самое эффективное. Вместо этого давайте изменим серверную часть нашего приложения, управляемого гипермедиа, чтобы он обслуживал только необходимый HTML-контент .
Заголовки HTTP-запросов в Htmx
В этом разделе мы рассмотрим другой, более продвинутый метод решения ситуации, когда нам нужен только фрагмент HTML , а не полный документ. В настоящее время мы позволяем серверу создавать полный HTML-документ в качестве ответа, а затем на стороне клиента мы фильтруем HTML до нужных нам фрагментов. Это легко сделать, и на самом деле это может быть необходимо, если мы не контролируем серверную часть или не можем легко изменять ответы.
Однако в нашем приложении, поскольку мы занимаемся разработкой «Full Stack» (то есть: мы контролируем как интерфейсный, так и внутренний код и можем легко изменять любой из них), у нас есть другой вариант: мы можем изменить ответы нашего сервера, чтобы возвращать только необходимый контент. и избавит от необходимости выполнять фильтрацию на стороне клиента.
Это оказывается более эффективным, поскольку мы не возвращаем весь контент, окружающий интересующий нас бит, экономя пропускную способность, а также ресурсы ЦП и памяти на стороне сервера. Итак, давайте рассмотрим возврат различного HTML-содержимого на основе контекстной информации, которую htmx предоставляет с помощью HTTP-запросов.
Вот еще раз взглянем на текущий серверный код нашей логики поиска:
Здесь происходит логика поиска.
Мы просто каждый раз перерисовываем
index.html
шаблон, несмотря ни на что.
Как мы хотим это изменить? Мы хотим условно визуализировать два разных бита HTML-контента :
Если это «обычный» запрос для всей страницы, мы хотим отобразить
index.html
шаблон в текущем виде. На самом деле, мы не хотим, чтобы что-то менялось, если это «нормальный» запрос.Однако если это запрос «Активный поиск», мы хотим отображать только содержимое, находящееся внутри
tbody
, то есть только строки таблицы на странице.
Поэтому нам нужен какой-то способ точно определить, какой из этих двух разных типов запросов к /contact
URL-адресу выполняется, чтобы точно знать, какой контент мы хотим отобразить.
Оказывается, htmx помогает нам различать эти два случая, включая несколько заголовков HTTP-запросов при отправке запросов. Заголовки запросов — это функция HTTP, позволяющая клиентам (например, веб-браузерам) включать пары имя/значение метаданных, связанных с запросами, чтобы помочь серверу понять, что запрашивает клиент.
Htmx использует эту особенность HTTP и добавляет дополнительные заголовки и, следовательно, дополнительный контекст к отправляемым HTTP-запросам. Это позволяет вам проверять эти заголовки и выбирать, какую логику выполнять на сервере и какой тип HTML-ответа вы хотите отправить клиенту.
Вот таблица HTTP-заголовков, которые htmx включает в HTTP-запросы:
HX-Boosted
Это будет строка «true», если запрос выполняется через элемент с использованием hx-boost.
HX-Current-URL
Это будет текущий URL-адрес браузера.
HX-History-Restore-Request
Это будет строка «истина», если запрос предназначен для восстановления истории после промаха в локальном кеше истории.
HX-Prompt
Он будет содержать ответ пользователя на hx-подсказку.
HX-Request
Это значение всегда «истина» для запросов на основе htmx.
HX-Target
Это значение будет идентификатором целевого элемента, если он существует.
HX-Trigger-Name
Это значение будет именем триггерного элемента, если он существует.
HX-Trigger
Это значение будет идентификатором сработавшего элемента, если он существует.
Просматривая этот список заголовков, выделяется последний: у нас есть идентификатор search
в результатах поиска. Таким образом, значение заголовка HX-Trigger
должно быть установлено, search
когда запрос поступает из входных данных поиска, имеющих идентификатор search
.
Давайте добавим в наш контроллер некоторую условную логику для поиска этого заголовка, и, если значение равно search
, мы отображаем только строки, а не весь index.html
шаблон:
Если заголовок запроса
HX-Trigger
равен «поиск», мы хотим сделать что-то другое.Нам нужно научиться отображать только строки таблицы.
Хорошо, а как нам визуализировать только строки результатов?
Факторинг ваших шаблонов
Теперь мы подошли к общему шаблону в htmx: мы хотим факторизовать наши серверные шаблоны. Это означает, что мы хотим немного разбить наши шаблоны, чтобы их можно было вызывать из нескольких контекстов. В этом случае мы хотим разбить строки таблицы результатов на отдельный шаблон, который мы назовем rows.html
. Мы включим его из исходного index.html
шаблона, а также будем использовать в нашем контроллере для его рендеринга, когда мы хотим отвечать только строками на запросы активного поиска.
index.html
Вот как сейчас выглядит таблица в нашем файле:
Цикл for
в этом шаблоне создает все строки в конечном контенте, сгенерированном index.html
. Мы хотим переместить for
цикл и, следовательно, строки, которые он создает, в отдельный файл шаблона , чтобы только этот небольшой фрагмент HTML можно было визуализировать независимо от index.html
.
Опять же, давайте назовем этот новый шаблон rows.html
:
Используя этот шаблон, мы можем визуализировать только tr
элементы для данной коллекции контактов.
Конечно, мы по-прежнему хотим включить этот контент в шаблон index.html
: иногда мы будем отображать всю страницу, а иногда — только строки. Чтобы обеспечить index.html
правильную отрисовку шаблона, мы можем включить rows.html
шаблон, используя include
директиву jinja в том месте, где мы хотим вставить контент rows.html
:
Эта директива «включает»
rows.html
файл, вставляя его содержимое в текущий шаблон.
Пока все хорошо: наша /contacts
страница по-прежнему отображается правильно, как и до того, как мы разделили строки из шаблона index.html
.
Использование нашего нового шаблона
Последним шагом в факторизации наших шаблонов является изменение нашего веб-контроллера, чтобы он мог использовать преимущества нового rows.html
файла шаблона, когда он отвечает на активный поисковый запрос.
Поскольку rows.html
это всего лишь еще один шаблон, как и index.html
, все, что нам нужно сделать, это вызвать render_template
функцию, rows.html
а не index.html
. Это отобразит только содержимое строки, а не всю страницу:
Отображение нового шаблона в случае активного поиска.
Теперь, когда делается запрос активного поиска, вместо того, чтобы возвращать весь HTML-документ, мы получаем только частичный фрагмент HTML — строки таблицы для контактов, соответствующих запросу. Эти строки затем вставляются на tbody
индексную страницу без необходимости hx-select
какой-либо другой обработки на стороне клиента.
И, в качестве бонуса, старый поиск на основе форм по-прежнему работает . Мы условно отображаем строки только тогда, когда search
входные данные отправляют HTTP-запрос через htmx. Опять же, это прогрессивное улучшение нашего приложения.
HTTP-заголовки и кеширование
Одним из тонких аспектов подхода, который мы здесь используем, используя заголовки для определения содержимого возвращаемого содержимого, является функция, встроенная в HTTP: кэширование. В нашем обработчике запросов мы теперь возвращаем разное содержимое в зависимости от значения заголовка HX-Trigger
. Если бы мы использовали HTTP-кэширование, мы могли бы попасть в ситуацию, когда кто-то делает запрос , отличный от htmx (например, обновление страницы), и все же содержимое htmx возвращается из HTTP-кэша, в результате чего получается частичная страница контента для пользователь.
Обновление панели навигации с помощью «hx-push-url»
Одним из недостатков нашей текущей реализации активного поиска по сравнению с обычной отправкой формы является то, что когда вы отправляете версию формы, она обновляет панель навигации браузера, включив в нее поисковый запрос. Так, например, если вы введете в поле поиска слово «Джо», в навигационной панели вашего браузера вы получите URL-адрес, который выглядит следующим образом:
Это приятная функция браузеров: она позволяет вам добавить этот поиск в закладки или скопировать URL-адрес и отправить его кому-то другому. Все, что им нужно сделать, это нажать на ссылку, и они повторят тот же поиск. Это также связано с понятием истории браузера: если вы нажмете кнопку «Назад», вы перейдете на предыдущий URL-адрес, с которого вы пришли. Если вы отправите два поисковых запроса и захотите вернуться к первому, вы можете просто нажать «Ответ», и браузер «вернется» к этому поиску.
В настоящее время во время активного поиска мы не обновляем панель навигации браузера. Таким образом, пользователи не получают ссылки, которые можно скопировать и вставить, и вы также не получаете записи истории, что означает отсутствие поддержки кнопки «Назад». К счастью, мы уже видели, как это исправить: с помощью hx-push-url
атрибута.
Атрибут hx-push-url
позволяет вам сказать htmx: «Пожалуйста, вставьте URL-адрес этого запроса в панель навигации браузера». Push может показаться странным глаголом для использования здесь, но именно этот термин использует базовый API истории браузера, который обусловлен тем фактом, что он моделирует историю браузера как «стек» местоположений: когда вы переходите в новое место, местоположение «помещается» в стек элементов истории, и когда вы нажимаете «назад», это местоположение «выталкивается» из стека истории.
Итак, чтобы получить правильную поддержку истории для нашего активного поиска, все, что нам нужно сделать, это установить для hx-push-url
атрибута значение true
.
Добавив
hx-push-url
атрибут со значениемtrue
, htmx обновит URL-адрес при выполнении запроса.
Теперь, когда отправляются запросы Active Search, URL-адрес на панели навигации браузера обновляется, чтобы в нем содержался правильный запрос, точно так же, как при отправке формы.
Возможно, вам не понравится такое поведение. Вы можете почувствовать, что пользователей будет сбивать с толку, например, обновление панели навигации и наличие записей истории для каждого выполненного активного поиска. И это нормально: вы можете просто опустить hx-push-url
атрибут, и он вернется к желаемому поведению. Цель htmx — быть достаточно гибким для достижения желаемого UX , оставаясь при этом в рамках декларативной модели HTML.
Добавление индикатора запроса
Последний штрих к нашему шаблону активного поиска — добавление индикатора запроса, сообщающего пользователю о том, что поиск выполняется. В нынешнем виде у пользователя нет явного сигнала о том, что функция активного поиска обрабатывает запрос. Если поиск займет немного времени, пользователь может подумать, что эта функция не работает. Добавляя индикатор запроса, мы сообщаем пользователю, что гипермедийное приложение занято и ему следует подождать (надеемся, не слишком долго!) завершения запроса.
Htmx обеспечивает поддержку индикаторов запроса через hx-indicator
атрибут. Как вы уже догадались, этот атрибут принимает CSS-селектор, указывающий на индикатор для данного элемента. Индикатором может быть что угодно, но обычно это какое-то анимированное изображение, например файл gif или svg, которое вращается или иным образом визуально сообщает, что «что-то происходит».
Давайте добавим счетчик после ввода поиска:
Атрибут
hx-indicator
указывает на изображение индикатора после ввода.Индикатор представляет собой SVG-файл с вращающимся кругом и содержит класс
htmx-indicator
.
Мы добавили счетчик сразу после ввода. Это визуально объединяет индикатор запроса с элементом, отправляющим запрос, и позволяет пользователю легко увидеть, что что-то действительно происходит.
Это просто работает, но как htmx заставляет счетчик появляться и исчезать? img
Обратите внимание, что тег индикатора htmx-indicator
содержит класс. htmx-indicator
— это класс CSS, который автоматически внедряется на страницу с помощью htmx. opacity
Этот класс устанавливает для элемента значение по умолчанию 0
, которое скрывает элемент от просмотра, но в то же время не нарушает макет страницы.
Когда запускается запрос htmx, указывающий на этот индикатор, htmx-request
к индикатору добавляется еще один класс, который меняет его непрозрачность на 1. Таким образом, вы можете использовать в качестве индикатора практически что угодно, и по умолчанию он будет скрыт. Затем, когда запрос находится в работе, он будет показан. Все это делается с помощью стандартных классов CSS, что позволяет вам управлять переходами и даже механизмом отображения индикатора (например, вы можете использовать display
вместо opacity
).
Используйте индикаторы запросов!
Индикаторы запросов — важный аспект UX любого распределенного приложения. К сожалению, браузеры со временем перестали уделять внимание своим собственным индикаторам запросов, и вдвойне прискорбно, что индикаторы запросов не являются частью API-интерфейсов JavaScript ajax.
Обязательно не пренебрегайте этим важным аспектом вашего приложения. Запросы могут показаться мгновенными, когда вы работаете над приложением локально, но в реальном мире они могут занять немного больше времени из-за задержки в сети. Зачастую полезно воспользоваться инструментами разработчика браузера, которые позволяют ограничить время отклика вашего локального браузера. Это даст вам лучшее представление о том, что видят пользователи в реальном мире, и покажет вам, где индикаторы могут помочь пользователям понять, что именно происходит.
Благодаря этому индикатору запроса у нас теперь есть довольно сложный пользовательский интерфейс по сравнению с простым HTML, но мы создали все это как функцию, управляемую гипермедиа. Никакого JSON или JavaScript не видно. Преимущество нашей реализации состоит в том, что она представляет собой постепенное усовершенствование; приложение продолжит работать для клиентов, у которых не включен JavaScript.
Ленивая загрузка
Теперь, когда активный поиск позади, давайте перейдем к совершенно другому усовершенствованию: отложенной загрузке. Ленивая загрузка — это когда загрузка определенного фрагмента контента откладывается на более позднее время, когда это необходимо. Обычно это используется для повышения производительности: вы избегаете ресурсов обработки, необходимых для создания некоторых данных, до тех пор, пока эти данные действительно не потребуются.
Давайте добавим подсчет общего количества контактов в Contact.app чуть ниже нижней части нашей таблицы контактов. Это даст нам потенциально дорогостоящую операцию, которую мы можем использовать, чтобы продемонстрировать, как добавить отложенную загрузку с помощью htmx.
Сначала давайте обновим код нашего сервера в /contacts
обработчике запросов, чтобы получить подсчет общего количества контактов. Мы передадим этот счетчик в шаблон для визуализации нового HTML.
Получите общее количество контактов из модели контактов.
Передайте счетчик в
index.html
шаблон, который будет использоваться при рендеринге.
Как и в случае с остальной частью приложения, чтобы сосредоточиться на гипермедийной части Contact.app, мы пропустим детали того, как оно Contact.count()
работает. Нам просто нужно знать, что:
Он возвращает общее количество контактов в базе данных контактов.
Это может быть медленно (для нашего примера).
Далее давайте добавим в наш HTML-код index.html
, который использует преимущества этого нового фрагмента данных и отображает сообщение рядом со ссылкой «Добавить контакт» с общим количеством пользователей. Вот как выглядит наш HTML:
Простой диапазон с текстом, показывающим общее количество контактов.
Ну, это было легко, не так ли? Теперь наши пользователи будут видеть общее количество контактов рядом со ссылкой для добавления новых контактов, чтобы дать им представление о том, насколько велика база данных контактов. Такая быстрая разработка — одна из радостей разработки веб-приложений старым способом.
Вот как эта функция выглядит в нашем приложении:
Красивый.
Конечно, как вы, наверное, и подозревали, не все идеально. К сожалению, после запуска этой функции в производство мы начали получать жалобы от пользователей о том, что приложение «медленно работает». Как и все хорошие разработчики, сталкивающиеся с проблемой производительности, вместо того, чтобы гадать, в чем может быть проблема, мы пытаемся получить профиль производительности приложения, чтобы увидеть, что именно вызывает проблему.
Удивительно, но проблема в этом невинно выглядящем Contacts.count()
вызове, выполнение которого занимает до полутора секунд. К сожалению, по причинам, выходящим за рамки этой книги, невозможно ни сократить время загрузки, ни кэшировать результат.
Это оставляет нам два варианта:
Удалите функцию.
Придумайте какой-нибудь другой способ смягчить проблему с производительностью.
Давайте предположим, что мы не можем удалить эту функцию, и поэтому посмотрим, как мы можем смягчить эту проблему с производительностью, используя вместо этого htmx.
Извлечение дорогостоящего кода
Первым шагом в реализации шаблона отложенной загрузки является удаление дорогостоящего кода (то есть вызова Contacts.count()
) из обработчика запроса /contacts
конечной точки.
Давайте поместим этот вызов функции в отдельный обработчик HTTP-запроса в качестве новой конечной точки HTTP, которую мы поместим в /contacts/count
. Для этой новой конечной точки нам вообще не нужно будет отображать шаблон: его единственной задачей будет визуализация небольшого фрагмента текста, который находится в диапазоне «(всего 22 контакта)».
Вот как будет выглядеть новый код:
Мы больше не вызываем
Contacts.count()
этот обработчик.Count
больше не передается шаблону для рендеринга в обработчике/contacts
.Мы создаем новый обработчик на
/contacts/count
пути, который выполняет дорогостоящие вычисления.Верните строку с общим количеством контактов.
Итак, теперь мы устранили проблему производительности из /contacts
кода обработчика, который отображает основную таблицу контактов, и создали новую конечную точку HTTP, которая будет создавать для нас эту дорогостоящую строку счетчика.
Теперь нам нужно каким-то образом перенести содержимое из этого нового обработчика в диапазон. Как мы говорили ранее, поведение htmx по умолчанию заключается в размещении любого содержимого, которое он получает по данному запросу, в innerHTML
элементе, и это именно то, что мы здесь хотим: мы хотим получить этот текст и поместить его в элемент. span
. Таким образом, мы можем просто поместить hx-get
атрибут в диапазон, указывающий на этот новый путь, и сделать именно это.
Однако помните, что событием по умолчанию , которое инициирует запрос элемента span
в htmx, является click
событие. Ну, это не то, чего мы хотим! Вместо этого мы хотим, чтобы этот запрос запускался немедленно при загрузке страницы.
Для этого мы можем добавить атрибут hx-trigger
для обновления триггера запросов к элементу и использовать это load
событие.
Это load
специальное событие, которое htmx запускает для всего контента при его загрузке в DOM. Установив hx-trigger
значение load
, мы заставим htmx выдавать GET
запрос при span
загрузке элемента на страницу.
Вот наш обновленный код шаблона:
Выдайте a
GET
, когда произойдет/contacts/count
событиеload
.
Обратите внимание, что файл span
начинается пустым: мы удалили из него содержимое и вместо этого разрешаем запросу /contacts/count
заполнить его.
И, проверьте, наша /contacts
страница снова работает быстро! Когда вы переходите на страницу, она кажется очень быстрой, и профилирование показывает, что да, страница действительно загружается гораздо быстрее. Почему это? Что ж, мы отложили дорогостоящие вычисления на вторичный запрос, позволив первоначальному запросу завершить загрузку быстрее.
Вы можете сказать: «Хорошо, отлично, но все равно требуется секунда или две, чтобы получить общее количество на странице». Верно, но часто пользователя может не особо интересовать общий подсчет. Они могут просто захотеть зайти на страницу и найти существующего пользователя или, возможно, захотят отредактировать или добавить пользователя. Общее количество контактов в таких случаях — это просто «приятно иметь» немного информации.
Откладывая расчет таким образом, мы позволяем пользователям продолжать использовать приложение, пока мы выполняем дорогостоящие расчеты.
Да, общее время вывода всей информации на экран занимает столько же времени. На самом деле это будет немного дольше, поскольку теперь нам нужны два HTTP-запроса, чтобы получить всю информацию о странице. Но воспринимаемая производительность для конечного пользователя будет намного лучше: они смогут делать то, что хотят, почти мгновенно, даже если некоторая информация не доступна мгновенно.
Lazy Loading — отличный инструмент, который нужно иметь под рукой при оптимизации производительности веб-приложений.
Добавление индикатора
Недостаток текущей реализации заключается в том, что в настоящее время нет никакой индикации того, что запрос на подсчет выполняется, она просто появляется в какой-то момент, когда запрос завершается.
Это не идеально. Здесь нам нужен индикатор, такой же, как мы добавили в нашем примере с активным поиском. И, по сути, мы можем просто повторно использовать то же самое изображение счетчика, скопировав и вставив его в новый HTML-код, который мы создали.
В данном случае у нас есть одноразовый запрос, и как только запрос будет завершен, счетчик нам больше не понадобится. Поэтому нет смысла использовать тот же подход, который мы использовали в примере с активным поиском. Напомним, что в этом случае мы разместили счетчик после диапазона и использовали hx-indicator
атрибут, чтобы указать на него.
В этом случае, поскольку счетчик используется только один раз, мы можем поместить его внутри содержимого диапазона. Когда запрос завершится, содержимое ответа будет помещено внутри диапазона, заменяя счетчик вычисленным количеством контактов. Оказывается, htmx позволяет размещать индикаторы с htmx-indicator
классом внутри элементов, которые отправляют запросы на основе htmx. При отсутствии атрибута hx-indicator
эти внутренние индикаторы будут отображаться во время выполнения запроса.
Итак, давайте добавим этот счетчик из примера активного поиска в качестве начального содержимого в нашем диапазоне:
Да, вот и все.
Теперь, когда пользователь загружает страницу, вместо волшебного отображения общего количества контактов появляется красивый счетчик, указывающий, что что-то происходит. Намного лучше.
Обратите внимание, что все, что нам нужно было сделать, это скопировать и вставить наш индикатор из примера активного поиска в файл span
. Мы еще раз видим, как htmx предоставляет гибкие, компонуемые функции и строительные блоки. Реализация новой функции часто заключается в простом копировании и вставке, возможно, внесении одной-двух изменений, и все готово.
Но это не лень!
Вы можете сказать: «Хорошо, но это не совсем лень. Мы по-прежнему загружаем счетчик сразу при загрузке страницы, мы просто делаем это во втором запросе. На самом деле вы не ждете, пока ценность действительно понадобится».
Отлично. Давайте сделаем это ленивым : мы будем выдавать запрос только тогда, когда span
прокрутка появится в поле зрения.
Для этого давайте вспомним, как мы настроили пример бесконечной прокрутки: мы использовали событие revealed
для нашего триггера. Это все, что нам здесь нужно, верно? Когда элемент раскрыт, мы выдаем запрос?
Да, вот и все. Опять же, мы можем смешивать и сопоставлять концепции различных UX-шаблонов, чтобы найти решения новых проблем в htmx.
Измените
hx-trigger
наrevealed
.
Теперь у нас есть действительно ленивая реализация, откладывающая дорогостоящие вычисления до тех пор, пока мы не будем абсолютно уверены, что они нам нужны. Довольно крутой трюк, и, опять же, простое изменение одного атрибута демонстрирует гибкость как htmx, так и подхода гипермедиа.
Встроенное удаление
Для нашего следующего трюка с гипермедиа мы собираемся реализовать шаблон «Встроенное удаление». Благодаря этой функции контакт можно удалить непосредственно из таблицы всех контактов, вместо того, чтобы требовать от пользователя перехода к режиму редактирования конкретного контакта, чтобы получить доступ к кнопке «Удалить контакт», которую мы добавили в прошлом глава.
Напомним, что у нас уже есть ссылки «Редактировать» и «Просмотр» для каждой строки в rows.html
шаблоне:
Теперь мы хотим также добавить ссылку «Удалить». И, поразмыслив над этим, мы хотим, чтобы эта ссылка действовала очень похоже на кнопку «Удалить контакт» на сайте edit.html
, не так ли? Мы хотим отправить HTTP-запрос DELETE
на URL-адрес данного контакта и хотим, чтобы диалоговое окно подтверждения не гарантировало, что пользователь случайно удалит контакт.
Вот HTML-код кнопки «Удалить контакт»:
Как вы уже могли догадаться, это будет еще одна работа по копированию и вставке.
Следует отметить, что в случае с кнопкой «Удалить контакт» мы хотели повторно отобразить весь экран и обновить URL-адрес, поскольку мы собираемся вернуться из представления редактирования контакта в представление списка. всех контактов. Однако в случае с этой ссылкой мы уже находимся в списке контактов, поэтому нет необходимости обновлять URL-адрес, и мы можем опустить атрибут hx-push-url
.
Вот код нашей встроенной ссылки «Удалить»:
Практически прямая копия кнопки «Удалить контакт».
Как вы можете видеть, мы добавили новый тег привязки и задали ему пустую цель (значение #
в его href
атрибуте), чтобы сохранить правильное поведение ссылки при наведении курсора мыши. Мы также скопировали hx-delete
атрибуты hx-confirm
и hx-target
из кнопки «Удалить контакт», но опустили hx-push-url
атрибуты, поскольку не хотим обновлять URL-адрес браузера.
Теперь у нас работает встроенное удаление, даже с диалоговым окном подтверждения. Пользователь может нажать ссылку «Удалить», и строка исчезнет из пользовательского интерфейса, поскольку вся страница будет перерисована.
Боковая панель стиля
Одним из побочных эффектов добавления этой ссылки удаления является то, что мы начинаем накапливать действия в строке контакта:
Было бы неплохо, если бы мы не показывали действия все подряд, и, кроме того, было бы неплохо, если бы мы показывали действия только тогда, когда пользователь проявляет интерес к данной строке. Мы вернемся к этой проблеме после того, как в следующей главе рассмотрим взаимосвязь между сценариями и приложением, управляемым гипермедиа.
А пока давайте просто мириться с этим неидеальным пользовательским интерфейсом, зная, что мы исправим его позже.
Сужение нашей цели
Однако здесь мы можем пойти еще дальше. Что, если вместо повторного рендеринга всей страницы мы просто удалим строку с контактом? Пользователь все равно просматривает строку, так действительно ли нужно перерисовывать всю страницу?
Для этого нам нужно сделать пару вещей:
Нам нужно обновить эту ссылку, чтобы она была нацелена на строку, в которой она находится.
Нам нужно изменить своп на
outerHTML
, так как мы хотим заменить (фактически удалить) всю строку.Нам нужно будет обновить серверную часть, чтобы она отображала пустой контент, когда
DELETE
запрос выдается по ссылке «Удалить», а не по кнопке «Удалить контакт» на странице редактирования контакта.
Прежде всего, обновите цель нашей ссылки «Удалить», чтобы она стала строкой, в которой находится ссылка, а не всем телом. Мы снова можем воспользоваться closest
функцией относительного позиционирования, чтобы выбрать ближайший объект tr
, как мы это делали в наших функциях «Нажмите, чтобы загрузить» и «Бесконечная прокрутка»:
Обновлено для определения ближайшего окружения
tr
(строки таблицы) ссылки.
Обновление серверной части
Теперь нам нужно обновить серверную часть. Мы хотим, чтобы кнопка «Удалить контакт» также работала, и в этом случае текущая логика верна. Поэтому нам понадобится какой-то способ различать DELETE
запросы, запускаемые кнопкой, и DELETE
запросы, поступающие от этой привязки.
Самый простой способ сделать это — добавить id
атрибут к кнопке «Удалить контакт», чтобы мы могли проверить HX-Trigger
заголовок HTTP-запроса и определить, была ли кнопка удаления причиной запроса. Это простое изменение существующего HTML:
id
К кнопке добавлен атрибут .
Присвоив этой кнопке атрибут id, мы теперь имеем механизм различия между кнопкой удаления в шаблоне edit.html
и ссылками удаления в rows.html
шаблоне. Когда эта кнопка выдает запрос, он будет выглядеть примерно так:
Вы можете видеть, что запрос теперь включает id
кнопку. Это позволяет нам писать код, очень похожий на тот, что мы делали для шаблона активного поиска, используя условие в заголовке, HX-Trigger
чтобы определить, что мы хотим сделать. Если этот заголовок имеет значение delete-btn
, то мы знаем, что запрос поступил от кнопки на странице редактирования, и мы можем сделать то, что делаем сейчас: удалить контакт и перенаправить на /contacts
страницу.
Если такого значения нет , мы можем просто удалить контакт и вернуть пустую строку. Эта пустая строка заменит цель, в данном случае строку для данного контакта, тем самым удалив строку из пользовательского интерфейса.
Давайте реорганизуем наш серверный код, чтобы сделать это:
Если кнопка удаления на странице редактирования отправила этот запрос, продолжайте выполнять предыдущую логику.
Если нет, просто верните пустую строку, которая удалит строку.
И это наша реализация на стороне сервера: когда пользователь нажимает «Удалить» в строке контакта и подтверждает удаление, строка исчезнет из пользовательского интерфейса. И снова мы имеем ситуацию, когда всего лишь изменение нескольких строк простого кода приводит к совершенно иному поведению. Гипермедиа в этом отношении сильна.
Модель подкачки Htmx
Это довольно круто, но есть еще одно улучшение, которое мы можем сделать, если потратим некоторое время на понимание модели замены содержимого htmx: было бы неплохо, если бы вместо того, чтобы просто мгновенно удалять строку, мы затемняли ее перед удалением. Затухание даст понять, что строка удаляется, давая пользователю приятную визуальную информацию об удалении.
Оказывается, мы можем сделать это довольно легко с помощью htmx, но для этого нам нужно разобраться, как именно htmx меняет содержимое.
Вы можете подумать, что htmx просто помещает новый контент в DOM, но на самом деле это работает не так. Вместо этого контент проходит ряд шагов при добавлении в DOM:
Когда контент получен и его собираются переместить в DOM,
htmx-swapping
к целевому элементу добавляется класс CSS.Затем возникает небольшая задержка (скорее мы обсудим, почему эта задержка существует).
Затем
htmx-swapping
класс удаляется из цели иhtmx-settling
добавляется.Новый контент заменяется в DOM.
Происходит еще одна небольшая задержка.
Наконец,
htmx-settling
класс удаляется из цели.
В механике обмена есть еще кое-что (например, расчет — это более сложная тема, которую мы обсудим в следующей главе), но на данный момент этого достаточно.
Здесь есть небольшие задержки в процессе, обычно порядка нескольких миллисекунд. Почему так? Оказывается, эти небольшие задержки позволяют осуществлять переходы CSS .
CSS-переходы
CSS-переходы — это технология, позволяющая анимировать переход от одного стиля к другому. Так, например, если вы изменили высоту чего-либо с 10 пикселей на 20 пикселей, с помощью перехода CSS вы можете плавно анимировать элемент до новой высоты. Подобные анимации доставляют удовольствие, часто повышают удобство использования приложения и являются отличным механизмом для улучшения вашего веб-приложения.
К сожалению, к CSS-переходам сложно получить доступ в простом HTML: обычно вам приходится использовать JavaScript и добавлять или удалять классы, чтобы они сработали. Вот почему модель подкачки htmx сложнее, чем вы думаете на первый взгляд. Заменив классы и добавив небольшие задержки, вы можете получить доступ к переходам CSS исключительно внутри HTML, без необходимости писать какой-либо JavaScript!
Использование преимущества «обмена htmx»
Хорошо, давайте вернемся и посмотрим на нашу встроенную механику удаления: мы нажимаем ссылку с расширенным htmx, которая удаляет контакт, а затем заменяет пустое содержимое строки. Мы знаем, что перед tr
удалением элемента к нему будет htmx-swapping
добавлен класс. Мы можем воспользоваться этим, чтобы написать CSS-переход, который уменьшает непрозрачность строки до 0. Вот как выглядит этот CSS:
Мы хотим, чтобы этот стиль применялся к
tr
элементам сhtmx-swapping
классом на них.Будет
opacity
0, что делает его невидимым.Используя эту функцию, он
opacity
перейдет в 0 в течение 1 секундыease-out
.
Опять же, это не книга о CSS, и мы не собираемся углубляться в детали CSS-переходов, но, надеюсь, сказанное выше имеет для вас смысл, даже если вы впервые видите CSS-переходы.
Итак, подумайте о том, что это означает с точки зрения модели подкачки htmx: когда htmx возвращает контент для замены в строке, он помещает класс htmx-swapping
в строку и немного ждет. Это позволит осуществить переход к нулевой непрозрачности, затухая строку. Затем будет заменено новое (пустое) содержимое, что фактически удалит строку.
Звучит хорошо, и мы почти у цели. Есть еще одна вещь, которую нам нужно сделать: «задержка подкачки» по умолчанию для htmx очень короткая, несколько миллисекунд. В большинстве случаев это имеет смысл: вам не нужна большая задержка перед помещением нового контента в DOM. Но в данном случае мы хотим дать CSS-анимации время для завершения, прежде чем мы произведем замену, на самом деле мы хотим дать ей секунду.
К счастью, в htmx есть опция для аннотации hx-swap
, которая позволяет вам установить задержку подкачки: после типа подкачки вы можете добавить, а затем swap:
значение времени, чтобы указать htmx подождать определенное время перед заменой. Давайте обновим наш HTML, чтобы разрешить задержку в одну секунду перед выполнением замены для действия удаления:
Задержка замены изменяет продолжительность ожидания htmx перед заменой нового контента.
Благодаря этой модификации существующая строка останется в DOM еще на секунду вместе с классом htmx-swapping
. Это даст строке время для перехода к нулевой непрозрачности, давая желаемый эффект затухания.
Теперь, когда пользователь нажимает ссылку «Удалить» и подтверждает удаление, строка будет медленно исчезать, а затем, как только ее непрозрачность станет равной 0, она будет удалена. Довольно необычно, и все сделано в декларативной, ориентированной на гипермедиа манере, без необходимости использования JavaScript. (Ну, очевидно, что htmx написан на JavaScript, но вы понимаете, что мы имеем в виду: нам не нужно было писать какой-либо JavaScript для реализации этой функции.)
Массовое удаление
Последняя функция, которую мы собираемся реализовать в этой главе, — это «Массовое удаление». Текущий механизм удаления пользователей хорош, но было бы неприятно, если бы пользователь захотел удалить пять или десять контактов за раз, не так ли? Для функции массового удаления мы хотим добавить возможность выбирать строки с помощью флажка и удалять их все за один раз, нажав кнопку «Удалить выбранные контакты».
Чтобы начать работу с этой функцией, нам нужно добавить флажок в каждую строку шаблона rows.html
. Этот вход будет иметь имя selected_contact_ids
, а его значение будет id
контактом для текущей строки.
Вот как rows.html
выглядит обновленный код:
Новая ячейка с полем для ввода флажка, значение которого равно идентификатору текущего контакта.
Нам также нужно будет добавить пустой столбец в заголовок таблицы, чтобы разместить столбец флажка. После этого мы получаем ряд флажков, по одному для каждой строки, шаблон, несомненно, знакомый вам по Интернету:
Если вы не знакомы или забыли, как работают флажки в HTML: флажок отправит свое значение, связанное с именем ввода, тогда и только тогда, когда он отмечен. Так, если, например, вы проверили контакты с идентификаторами 3, 7 и 9, то все эти три значения будут отправлены на сервер. Поскольку в этом случае все флажки имеют одно и то же имя, selected_contact_ids
все три значения будут отправлены с именем selected_contact_ids
.
Кнопка «Удалить выбранные контакты»
Следующий шаг — добавить кнопку под таблицей, которая удалит все выбранные контакты. Мы хотим, чтобы эта кнопка, как и наши ссылки удаления в каждой строке, выдавала HTTP DELETE
, а не отправляла его на URL-адрес для данного контакта, как мы делаем со встроенными ссылками удаления и с кнопкой удаления на странице редактирования, здесь мы хотим выдать URL DELETE
- /contacts
адрес.
Как и в случае с другими элементами удаления, мы хотим подтвердить, что пользователь желает удалить контакты, и в этом случае мы собираемся нацелиться на тело страницы, поскольку мы собираемся повторно отобразить всю таблицу.
Вот как выглядит код кнопки:
Выдайте
DELETE
файл/contacts
.Подтвердите, что пользователь хочет удалить выбранные контакты.
Нацельтесь на тело.
Довольно легко. Однако есть один вопрос: как мы будем включать в запрос значения всех выбранных флажков? В нынешнем виде это всего лишь отдельная кнопка, и она не содержит никакой информации, указывающей на то, что она должна включать какую-либо другую информацию в DELETE
создаваемый ею запрос.
К счастью, у htmx есть несколько разных способов включить в запрос значения входных данных.
Один из способов — использовать hx-include
атрибут, который позволяет вам использовать селектор CSS для указания элементов, которые вы хотите включить в запрос. Здесь это сработало бы нормально, но мы собираемся использовать другой подход, который в данном случае немного проще.
По умолчанию, если элемент является дочерним по отношению к form
элементу и не отправляет GET
запрос, htmx включит все значения входных данных в эту форму. В подобных ситуациях, когда над таблицей выполняется массовая операция, обычно всю таблицу заключают в тег формы, чтобы можно было легко добавлять кнопки, которые работают с выбранными элементами.
Давайте добавим этот тег формы вокруг таблицы и обязательно включим в него кнопку:
Тег формы охватывает всю таблицу.
Тег формы также включает кнопку.
Теперь, когда кнопка выдает DELETE
, она будет включать все идентификаторы контактов, которые были выбраны в качестве selected_contact_ids
переменной запроса.
Серверная часть для удаления выбранных контактов
Серверная реализация будет выглядеть как исходный серверный код для удаления контакта. Фактически, еще раз, мы можем просто скопировать и вставить и внести несколько исправлений:
Мы хотим изменить URL-адрес на
/contacts
.Мы хотим, чтобы обработчик получал все идентификаторы, представленные как,
selected_contact_ids
и перебирал каждый из них, удаляя данный контакт.
Это единственные изменения, которые нам нужно внести! Вот как выглядит серверный код:
Обрабатываем
DELETE
запрос к/contacts/
пути.Преобразуйте
selected_contact_ids
значения, отправленные на сервер, из списка строк в список целых чисел.Перебрать все идентификаторы.
Удалить данный контакт с каждым идентификатором.
Кроме того, это тот же код, что и в исходном обработчике удаления: выводит сообщение и отображает шаблон
index.html
.
Итак, мы взяли исходную логику удаления и немного изменили ее, чтобы она работала с массивом идентификаторов, а не с одним идентификатором.
Вы могли заметить еще одно небольшое изменение: мы убрали перенаправление, которое было в исходном коде удаления. Мы сделали это, потому что уже находимся на странице, которую хотим повторно отобразить, поэтому нет причин перенаправлять и обновлять URL-адрес на что-то новое. Мы можем просто перерисовать страницу, и новый список контактов (за исключением удаленных контактов) будет перерисован.
И вот, теперь у нас есть функция массового удаления для нашего приложения. Опять же, это не так уж и много кода, и мы полностью реализуем эти функции путем обмена гипермедиа с сервером традиционным, RESTful способом Интернета.
HTML-примечания: доступны по умолчанию?
Проблемы с доступностью могут возникнуть, когда мы пытаемся реализовать элементы управления, не встроенные в HTML.
Ранее, в первой главе, мы рассмотрели пример импровизированного <div>, работающего как кнопка. Давайте рассмотрим другой пример: что, если вы создадите что-то, похожее на набор вкладок, но для его создания вы используете переключатели и хаки CSS? Это изящный хак, который время от времени распространяется в сообществах веб-разработчиков.
Взаимодействие с клавиатурой
Можно ли фокусировать вкладки с помощью клавиши Tab?
Роли, состояния и свойства ARIA
«[Элемент, содержащий вкладки] имеет роль
tablist
».«Каждая [вкладка] имеет роль
tab
[...]»«Каждый элемент, содержащий панель содержимого,
tab
имеет рольtabpanel
».«Каждая [вкладка] имеет свойство,
aria-controls
ссылающееся на связанный с ней элемент панели вкладок».«Активному элементу присвоено
tab
состояние , а всем остальным элементам — ».aria-selectedtruetabfalse
«Каждый элемент с ролью
tabpanel
имеет свойствоaria-labelledby
, ссылающееся на связанный с нимtab
элемент».
Вам придется написать много кода, чтобы ваши импровизированные вкладки соответствовали всем этим требованиям. Некоторые атрибуты ARIA можно добавить непосредственно в HTML, но они повторяются, а другие (например, aria-selected
) необходимо устанавливать с помощью JavaScript, поскольку они являются динамическими. Взаимодействие с клавиатурой также может быть подвержено ошибкам.
Создать собственную реализацию набора вкладок не невозможно, даже не так сложно. Однако трудно поверить, что новая реализация будет работать для всех пользователей во всех средах, поскольку у большинства из нас ограничены ресурсы для тестирования.
Придерживайтесь установленных библиотек для взаимодействия с пользовательским интерфейсом. Если вариант использования требует индивидуального решения, тщательно протестируйте взаимодействие с клавиатурой и ее доступность. Тестируйте вручную. Тестируйте автоматически. Тестируйте с помощью программ чтения с экрана, тестируйте с помощью клавиатуры, тестируйте на разных браузерах и оборудовании и запускайте линтеры (во время написания кода и/или в CI). Тестирование имеет решающее значение для обеспечения машиночитаемости, читаемости человеком или веса страницы.
Также подумайте: нужно ли представлять информацию в виде вкладок? Иногда ответ положительный, но если нет, то последовательность подробностей и раскрытий информации преследует очень похожую цель.
Идти под угрозу UX только ради того, чтобы избежать JavaScript, — это плохая разработка. Но иногда можно добиться такого же (или лучшего!) качества UX, допуская при этом более простую и надежную реализацию.
Last updated