Глава 09. Клиентские сценарии
REST позволяет расширять функциональность клиента путем загрузки и выполнения кода в форме апплетов или сценариев. Это упрощает работу клиентов за счет уменьшения количества функций, которые необходимо реализовать заранее.
— Рой Филдинг, Архитектурные стили и проектирование сетевых архитектур программного обеспечения.
До сих пор мы (в основном) избегали написания какого-либо JavaScript (или _hyperscript) в Contact.app, главным образом потому, что реализованная нами функциональность не требовала этого. В этой главе мы рассмотрим сценарии и, в частности, сценарии, дружественные к гипермедиа, в контексте приложения, управляемого гипермедиа.
Разрешено ли создание сценариев?
Распространенной критикой Интернета является то, что им злоупотребляют. Существует версия, что WWW был создан как система доставки «документов» и стал использоваться для «приложений» только в результате несчастного случая или странных обстоятельств.
Однако концепция гипермедиа бросает вызов разделению документа и приложения. Гипермедийные системы, такие как HyperCard, предшествовавшие Интернету, обладали богатыми возможностями для активного и интерактивного взаимодействия, включая создание сценариев.
HTML в том виде, в котором он указан и реализован, не имеет возможностей, необходимых для создания высокоинтерактивных приложений. Однако это не означает, что цель гипермедиа — «документы», а не «приложения».
Скорее, хотя теоретическая основа и существует, реализация недостаточно развита. Поскольку JavaScript является единственной точкой расширения, а элементы управления гипермедиа плохо интегрированы с JavaScript (почему нельзя щелкнуть ссылку, не останавливая программу?), разработчики не внедрили гипермедиа и вместо этого использовали Интернет как тупой канал для приложений, которые подражать «родным».
Цель этой книги — показать, что можно создавать сложные веб-приложения, используя оригинальную технологию Интернета — гипермедиа, без необходимости разработчику приложений обращаться к абстракциям, предоставляемым большими популярными средами JavaScript.
Сам Htmx, конечно, написан на JavaScript, и одним из его преимуществ является то, что взаимодействия гипермедиа, которые происходят через htmx, предоставляют богатый интерфейс для кода JavaScript с поддержкой конфигурации, событий и собственных расширений htmx.
Htmx настолько расширяет выразительность HTML, что во многих ситуациях устраняется необходимость в написании сценариев. Это делает htmx привлекательным для людей, которые не хотят писать JavaScript, и есть много таких разработчиков, которые опасаются сложности фреймворков одностраничных приложений.
Однако погружение в JavaScript не является целью проекта htmx. Цель htmx — не меньше JavaScript, а меньше кода, более читабельный и дружественный к гипермедиа код.
Сценарии стали мощным мультипликатором силы для Интернета. Используя сценарии, разработчики веб-приложений могут не только улучшать свои HTML-сайты, но и создавать полноценные клиентские приложения, которые часто могут конкурировать с собственными приложениями «толстого» клиента.
Этот ориентированный на JavaScript подход к созданию веб-приложений является свидетельством мощи Интернета и, в частности, сложности веб-браузеров. Он имеет свое место в веб-разработке: бывают ситуации, когда подход гипермедиа просто не может обеспечить тот уровень взаимодействия, который может обеспечить SPA.
Однако в дополнение к этому стилю, более ориентированному на JavaScript, мы хотим разработать стиль сценариев, более совместимый и соответствующий приложениям, управляемым гипермедиа.
Скрипты для гипермедиа
Заимствуя идею Роя Филдинга об «ограничениях», определяющих REST, мы предлагаем два ограничения сценариев, дружественных к гипермедиа. Вы создаете сценарий, совместимый с HDA, если соблюдаются следующие два ограничения:
Основным форматом данных, которыми обмениваются сервер и клиент, должен быть гипермедиа, так же, как и без сценариев.
Состояние на стороне клиента, за пределами самой DOM, сведено к минимуму.
Цель этих ограничений — ограничить написание сценариев там, где они лучше всего подходят и где ничто другое не может сравниться: дизайн взаимодействия . Бизнес-логика и логика представления находятся в ведении сервера, на котором мы можем выбирать языки или инструменты, подходящие для нашей бизнес-области.
Сервер
Сохранение бизнес-логики и логики представления «на сервере» не означает, что эти две «проблемы» смешаны или связаны. Они могут быть модульными на сервере. Фактически, они должны быть модульными на сервере вместе со всеми другими задачами нашего приложения.
Также обратите внимание, что, особенно на языке веб-разработки, скромный «сервер» обычно представляет собой целый парк стоек, виртуальных машин, контейнеров и многого другого. Даже всемирная сеть центров обработки данных сводится к «серверу», когда речь идет о серверной части приложения, управляемого гипермедиа.
Удовлетворение этих двух ограничений иногда требует от нас отхода от того, что обычно считается лучшей практикой для JavaScript. Имейте в виду, что культурная мудрость JavaScript в значительной степени была развита в JavaScript-ориентированных SPA-приложениях.
Приложение, управляемое гипермедиа, не может с такой же легкостью вернуться к этой традиции. Эта глава — наш вклад в разработку нового стиля и лучших практик для того, что мы называем приложениями, управляемыми гипермедиа.
К сожалению, простое перечисление «лучших практик» редко бывает убедительным или поучительным. Честно говоря, это скучно.
Вместо этого мы продемонстрируем эти лучшие практики, реализовав функции на стороне клиента в Contact.app. Чтобы охватить различные аспекты создания сценариев, ориентированных на гипермедиа, мы реализуем три различные функции:
Переполняющее меню для выполнения действий «Редактировать» , «Просмотр» и « Удалить» , чтобы навести порядок в нашем списке контактов.
Улучшенный интерфейс массового удаления.
Сочетание клавиш для фокусировки на поле поиска.
Важным выводом при реализации каждой из этих функций является то, что, хотя они реализованы полностью на стороне клиента с использованием сценариев, они не обмениваются информацией с сервером через негипермедийный формат, такой как JSON, и что они не храните значительную часть состояния за пределами самой DOM.
Инструменты создания сценариев для Интернета
Основным языком сценариев для Интернета, конечно же, является JavaScript, который сегодня повсеместно используется в веб-разработке.
Однако в Интернете есть интересный факт: JavaScript не всегда был единственной встроенной опцией. Как намекает цитата Роя Филдинга в начале этой главы, «апплеты», написанные на других языках, таких как Java, считались частью инфраструктуры сценариев Интернета. Кроме того, был период времени, когда Internet Explorer поддерживал VBScript — язык сценариев, основанный на Visual Basic.
Сегодня у нас есть множество транскомпиляторов (часто сокращается до транспиляторов ), которые преобразуют многие языки в JavaScript, такие как TypeScript, Dart, Kotlin, ClojureScript, F# и другие. Существует также формат байт-кода WebAssembly (WASM), который поддерживается в качестве цели компиляции для C, Rust и первого языка WASM AssemblyScript.
Однако большинство этих опций не ориентированы на стиль написания сценариев, ориентированный на гипермедиа. Языки, компилируемые в JS, часто сочетаются с библиотеками, ориентированными на SPA (Dart и AngularDart, ClojureScript и Reagent, F# и Elm), а WASM в настоящее время в основном ориентирован на связывание с библиотеками C/C++ из JavaScript.
Вместо этого мы сосредоточимся на трех технологиях сценариев на стороне клиента, дружественных к гипермедиа:
VanillaJS, то есть использование JavaScript без зависимости от какого-либо фреймворка.
Alpine.js — библиотека JavaScript для добавления поведения непосредственно в HTML.
_hyperscript — язык сценариев, отличный от JavaScript, созданный вместе с htmx. Как и AlpineJS, _hyperscript обычно встроен в HTML.
Давайте кратко рассмотрим каждый из этих вариантов сценариев, чтобы мы знали, с чем имеем дело.
Обратите внимание, что, как и в случае с CSS, мы собираемся показать вам каждый из этих вариантов ровно настолько, чтобы дать представление о том, как они работают, и, мы надеемся, пробудить у вас интерес к более подробному изучению любого из них.
Ванильный JavaScript
Отсутствие кода быстрее, чем отсутствие кода.
— Мерб
Ванильный JavaScript — это просто использование в вашем приложении простого JavaScript без каких-либо промежуточных слоев. Термин «ваниль» вошел в жаргон веб-разработчиков, поскольку считалось, что любое достаточно «продвинутое» веб-приложение будет использовать некоторую библиотеку с именем, заканчивающимся на «.js». Однако по мере того, как JavaScript развивался как язык сценариев, стандартизировался во всех браузерах и предоставлял все больше и больше функциональности, эти платформы и библиотеки стали менее важными.
По иронии судьбы, поскольку JavaScript стал более мощным и устранил необходимость в первом поколении библиотек JavaScript, таких как jQuery, он также позволил людям создавать сложные библиотеки SPA. Эти библиотеки SPA часто даже более сложны, чем исходные библиотеки JavaScript первого поколения.
VanillaJS — это самый простой и комплексный фреймворк с наименьшими затратами, который я когда-либо использовал.
— http://vanilla-js.com
Поскольку JavaScript стал языком сценариев, это, безусловно, справедливо для многих приложений. Это особенно верно в случае HDA, поскольку при использовании гипермедиа вашему приложению не потребуются многие функции, обычно предоставляемые более сложными JavaScript-фреймворками для одностраничных приложений:
Маршрутизация на стороне клиента
Абстракция манипуляций с DOM (т. е. шаблонов, которые автоматически обновляются при изменении ссылочных переменных).
Добавление динамического поведения к тегам, отображаемым на сервере, при загрузке (т. е. «гидратация»)
Сетевые запросы
Если вся эта сложность не будет реализована в JavaScript, потребности вашей инфраструктуры значительно уменьшатся.
Одна из лучших особенностей VanillaJS — это способ его установки: вам не обязательно это делать!
Вы можете просто начать писать JavaScript в своем веб-приложении, и оно просто будет работать.
Это хорошие новости. Плохая новость заключается в том, что, несмотря на улучшения, произошедшие за последнее десятилетие, JavaScript имеет некоторые существенные ограничения как язык сценариев, которые могут сделать его далеко не идеальным в качестве автономной технологии сценариев для приложений, управляемых гипермедиа:
Будучи настолько устоявшимся, оно обросло множеством особенностей и бородавок.
Он имеет сложный и запутанный набор функций для работы с асинхронным кодом.
Работать с событиями на удивление сложно.
DOM API (большая часть которых изначально была разработана для Java, да, Java ) многословны и не имеют привычки упрощать использование общих функций.
Разумеется, ни одно из этих ограничений не является препятствием для сделки. Многие из них постепенно исправляются, и многие люди предпочитают «близкий к металлу» (из-за отсутствия лучшего термина) характер ванильного JavaScript более сложным подходам к написанию сценариев на стороне клиента.
Простой счетчик
Чтобы погрузиться в стандартный JavaScript в качестве варианта сценария внешнего интерфейса, давайте создадим простой виджет счетчика.
Виджеты-счетчики — это распространенный пример «Hello World» для фреймворков JavaScript, поэтому рассмотрение того, как это можно сделать в стандартном JavaScript (а также другие варианты, которые мы собираемся рассмотреть), будет поучительным.
Наш виджет счетчика будет очень простым: он будет иметь число, отображаемое в виде текста, и кнопку, которая увеличивает это число.
Одна из проблем с решением этой проблемы в стандартном JavaScript заключается в том, что ему не хватает одной вещи, которую предоставляют большинство фреймворков JavaScript: кода по умолчанию и архитектурного стиля.
С ванильным JavaScript нет никаких правил!
Это не так уж и плохо. Он предоставляет прекрасную возможность совершить небольшое путешествие по различным стилям, которые люди разработали для написания своего JavaScript.
Встроенная реализация
Для начала давайте начнем с самой простой вещи, которую только можно вообразить: весь наш JavaScript будет написан встроенно, непосредственно в HTML. При нажатии кнопки мы найдем output
элемент, содержащий число, и увеличим содержащееся в нем число.
Наш выходной элемент имеет идентификатор, который поможет нам его найти.
Мы используем
onclick
атрибут для добавления прослушивателя событий.Найдите выходные данные с помощью вызова querySelector().
JavaScript позволяет нам использовать
++
операторы для строк.
Не плохо.
Это не самый красивый код, и он может раздражать, особенно если вы не привыкли к DOM API.
Немного раздражает, что нам нужно было добавить id
к output
элементу. Эта document.querySelector()
функция немного многословна по сравнению, скажем, с $
функцией, предоставляемой jQuery.
Но это работает. Его также достаточно легко понять, и, что особенно важно, он не требует каких-либо других библиотек JavaScript.
Итак, это простой встроенный подход с VanillaJS.
Отделение наших сценариев
Хотя встроенная реализация в некотором смысле проста, более стандартным способом ее написания было бы перемещение кода в отдельный файл JavaScript. Затем этот файл JavaScript будет либо связан с помощью <script src>
тега, либо помещен во встроенный <script>
тег в процессе сборки.
Здесь мы видим HTML и JavaScript, отделенные друг от друга, в разных файлах. HTML теперь стал «чище», поскольку в нем нет JavaScript.
JavaScript немного сложнее, чем в нашей встроенной версии: нам нужно найти кнопку с помощью селектора запросов и добавить прослушиватель событий для обработки события щелчка и увеличения счетчика.
Найдите выходной элемент.
Найдите кнопку.
Мы используем
addEventListener
, что предпочтительнееonclick
по многим причинам.Логика остается прежней, меняется только структура вокруг нее.
Перенося JavaScript в другой файл, мы следуем принципу проектирования программного обеспечения, известному как разделение задач (SoC).
Разделение задач утверждает, что различные «проблемы» (или аспекты) программного проекта должны быть разделены на несколько файлов, чтобы они не «загрязняли» друг друга. JavaScript не является разметкой, поэтому его не должно быть в вашем HTML, он должен быть где-то еще . Информация о стиле также не является разметкой и поэтому также принадлежит отдельному файлу (например, файлу CSS).
В течение некоторого времени такое разделение задач считалось «ортодоксальным» способом создания веб-приложений.
Заявленная цель разделения задач состоит в том, чтобы мы могли модифицировать и развивать каждую задачу независимо, с уверенностью, что мы не нарушим ни одну из других задач.
Однако давайте посмотрим, как именно этот принцип сработал на нашем простом противоположном примере. Если вы внимательно посмотрите на новый HTML, то окажется, что нам пришлось добавить класс к кнопке. Мы добавили этот класс, чтобы можно было найти кнопку в JavaScript и добавить обработчик события «щелчок».
Теперь и в HTML, и в JavaScript это имя класса представляет собой просто строку, и не существует никакого процесса проверки того , что кнопка имеет правильные классы для нее или ее родителей, чтобы гарантировать, что обработчик событий действительно добавлен справа. элемент.
К сожалению, оказалось, что неосторожное использование селекторов CSS в JavaScript может вызвать так называемый суп jQuery . Суп jQuery — это ситуация, когда:
Трудно найти JavaScript, который придает заданное поведение данному элементу.
Повторное использование кода затруднено.
Код оказывается совершенно неорганизованным и «плоским», со множеством несвязанных между собой обработчиков событий, смешанных вместе.
Название «суп jQuery» происходит от того факта, что большинство приложений с большим количеством JavaScript раньше создавались на jQuery (многие до сих пор), что, возможно, непреднамеренно, имело тенденцию поощрять этот стиль JavaScript.
Итак, вы можете видеть, что идея разделения задач не всегда работает так хорошо, как было обещано: наши проблемы в конечном итоге переплетаются или связаны довольно глубоко, даже когда мы разделяем их по разным файлам.
Чтобы показать, что не просто именование задач может привести к неприятностям, рассмотрим еще одно небольшое изменение в нашем HTML, которое демонстрирует проблемы с нашим разделением задач: представьте, что мы решили изменить числовое поле с тега <output>
на <input type="number">
.
Это небольшое изменение в нашем HTML сломает наш JavaScript, несмотря на то, что мы «разделили» наши задачи.
Решение этой проблемы достаточно простое (нам нужно будет изменить свойство .textContent
на .value
свойство), но оно демонстрирует сложность синхронизации изменений разметки и изменений кода в нескольких файлах. По мере увеличения размера вашего приложения поддерживать все в синхронизации может стать все труднее.
Тот факт, что небольшие изменения в нашем HTML-коде могут нарушить работу наших сценариев, указывает на то, что они тесно связаны , несмотря на то, что они разбиты на несколько файлов. Эта тесная связь предполагает, что разделение HTML и JavaScript (и CSS) часто является иллюзорным разделением задач: задачи настолько связаны друг с другом, что их нелегко разделить.
В Contact.app нас не интересуют «структура», «стиль» или «поведение»; мы занимаемся сбором контактной информации и предоставлением ее пользователям. SoC, в том виде, в каком его формулируют в ортодоксальности веб-разработки, на самом деле не является незыблемым архитектурным руководством, а скорее стилистическим выбором, который, как мы видим, может даже стать помехой.
Локальность поведения
Оказывается, наблюдается растущая реакция против принципа разделения интересов. Рассмотрим следующие веб-технологии и методы:
JSX
ЛитHTML
CSS-в-JS
Однофайловые компоненты
Маршрутизация на основе файловой системы
Каждая из этих технологий размещает код на разных языках, отвечающий за одну функцию (обычно виджет пользовательского интерфейса).
Все они смешивают проблемы реализации , чтобы представить конечному пользователю единую абстракцию. Разделение технических деталей – это, кхм, не такая уж большая проблема.
Локальность поведения (LoB) — это альтернативный принцип проектирования программного обеспечения, который мы придумали в отличие от разделения ответственности. Он описывает следующие характеристики части программного обеспечения:
Поведение единицы кода должно быть максимально очевидным, если смотреть только на эту единицу кода.
— https://htmx.org/essays/locality-of-behaviour/
Проще говоря: вы должны быть в состоянии определить, что делает кнопка, просто взглянув на код или разметку, создающую эту кнопку. Это не означает, что вам нужно встроить всю реализацию, но вам не нужно искать ее или требовать предварительного знания кодовой базы, чтобы ее найти.
Мы продемонстрируем Locality of Behavior во всех наших примерах, как в демонстрациях счетчиков, так и в функциях, которые мы добавляем в Contact.app. Локальность поведения — это явная цель проектирования как _hyperscript, так и Alpine.js (о котором мы поговорим позже), а также htmx.
Все эти инструменты обеспечивают локальность поведения, позволяя встраивать атрибуты непосредственно в HTML, а не искать элементы в документе с помощью селекторов CSS, чтобы добавить к ним прослушиватели событий.
Мы считаем, что в приложении, управляемом гипермедиа, принцип проектирования локальности поведения часто более важен, чем более традиционный принцип проектирования разделения ответственности.
Что делать с нашим счетчиком?
Итак, стоит ли нам вернуться к onclick
атрибутивному способу ведения дел? Этот подход, безусловно, выигрывает в локальности поведения и имеет дополнительное преимущество, заключающееся в том, что он встроен в HTML.
Однако, к сожалению, on*
атрибуты JavaScript также имеют некоторые недостатки:
Они не поддерживают пользовательские события.
Не существует хорошего механизма для связывания долговременных переменных с элементом — все переменные отбрасываются, когда прослушиватель событий завершает выполнение.
Если у вас несколько экземпляров элемента, вам нужно будет повторить код прослушивателя для каждого или использовать что-то более умное, например делегирование событий.
Код JavaScript, который напрямую манипулирует DOM, становится многословным и загромождает разметку.
Элемент не может прослушивать события на другом элементе.
Рассмотрим распространенную ситуацию: у вас есть всплывающее окно, и вы хотите, чтобы оно закрывалось, когда пользователь щелкает за его пределами. В этой ситуации слушатель должен находиться в элементе body, вдали от фактической разметки всплывающего окна. Это означает, что к элементу body должны быть прикреплены слушатели, которые работают со многими несвязанными компонентами. Некоторые из этих компонентов могут даже отсутствовать на странице при ее первом отображении, если они добавляются динамически после отображения исходной HTML-страницы.
Таким образом, ванильный JavaScript и Locality of Behavior, похоже, не так хорошо взаимодействуют, как нам хотелось бы.
Однако ситуация не безнадежна: важно понимать, что LoB не требует, чтобы поведение было реализовано на месте использования, а просто вызывалось там. То есть нам не нужно писать весь наш код для данного элемента, нам просто нужно дать понять, что данный элемент вызывает некоторый код, который может находиться в другом месте.
Учитывая это, можно улучшить LoB, записывая JavaScript в отдельный файл, при условии, что у нас есть разумная система структурирования нашего JavaScript.
РСДЖС
Вот рекомендации RSJS, наиболее подходящие для нашего виджета счетчика:
«Использовать
data-
атрибуты» в HTML: вызов поведения посредством добавления атрибутов данных делает очевидным, что происходит JavaScript, в отличие от использования случайных классов или идентификаторов, которые могут быть ошибочно удалены или изменены.«Один компонент на файл»: имя файла должно соответствовать атрибуту данных, чтобы его можно было легко найти, что является выигрышем для LoB.
Чтобы следовать рекомендациям RSJS, давайте реструктурируем наши текущие файлы HTML и JavaScript. Во-первых, мы будем использовать атрибуты данных , то есть атрибуты HTML, начинающиеся с data-
, стандартной функции HTML, чтобы указать, что наш HTML является компонентом счетчика. Затем мы обновим наш JavaScript, чтобы использовать селектор атрибутов, который ищет data-counter
атрибут в качестве корневого элемента в нашем компоненте-счетчике и подключает соответствующие обработчики событий и логику. Кроме того, давайте переработаем код querySelectorAll()
и добавим функции счетчика ко всем компонентам счетчика, найденным на странице. (Никогда не знаешь, сколько жетонов вам понадобится!)
Вот как сейчас выглядит наш код:
Счетчик на ванильном JavaScript с RSJS
Вызов поведения JavaScript с атрибутом данных.
Отметьте соответствующие элементы-потомки.
Файл должен иметь то же имя, что и атрибут данных, чтобы мы могли легко его найти.
Получите все элементы, вызывающие это поведение.
Получите любые дочерние элементы, которые нам нужны.
Зарегистрируйте обработчики событий.
Использование RSJS решает или, по крайней мере, облегчает многие проблемы, на которые мы указали в нашем первом неструктурированном примере разделения VanillaJS на отдельный файл:
JS, который привязывает поведение к данному элементу, ясен (хотя и только посредством соглашений об именах).
Повторное использование легко — вы можете создать на странице еще один компонент счетчика, и он будет работать.
Код хорошо организован — одно поведение на файл.
В общем, RSJS — это хороший способ структурировать ваш стандартный JavaScript в приложении, управляемом гипермедиа. Пока JavaScript не взаимодействует с сервером через простой API JSON данных и не хранит кучу внутреннего состояния за пределами DOM, это полностью совместимо с подходом HDA.
Давайте реализуем функцию в Contact.app, используя подход RSJS/ванильный JavaScript.
VanillaJS в действии: дополнительное меню
На нашей домашней странице есть ссылки «Редактировать», «Просмотреть» и «Удалить» для каждого контакта в нашей таблице. Это занимает много места и создает визуальный беспорядок. Давайте исправим это, поместив эти действия в раскрывающееся меню с кнопкой для его открытия.
Если вы плохо знакомы с JavaScript и код здесь кажется вам слишком сложным, не волнуйтесь; примеры Alpine.js и _hyperscript, которые мы рассмотрим далее, легче понять.
Давайте начнем с наброска разметки, которую мы хотим использовать для нашего раскрывающегося меню. Во-первых, нам нужен элемент, мы будем использовать его <div>
, чтобы охватить весь виджет и пометить его как компонент меню. Внутри этого div у нас будет стандарт <button>
, который будет функционировать как механизм, показывающий и скрывающий пункты меню. Наконец, у нас будет другой <div>
, в котором будут храниться пункты меню, которые мы собираемся показать.
Эти пункты меню будут простыми тегами привязки, как и в текущей таблице контактов.
Вот как выглядит наш обновленный HTML-код со структурой RSJS:
Отметьте корневой элемент компонента меню
Эта кнопка будет открывать и закрывать наше меню.
Контейнер для позиций нашего меню
Пункты меню
Роли и атрибуты ARIA основаны на шаблонах меню и кнопок меню из ARIA Authoring Practices Guide.
Что такое АРИЯ?
Поскольку мы, веб-разработчики, создаем более интерактивные веб-сайты, похожие на приложения, в репертуаре элементов HTML не будет всего, что нам нужно. Как мы видели, используя CSS и JavaScript, мы можем наделить существующие элементы расширенным поведением и внешним видом, конкурирующим с собственными элементами управления.
Однако была одна вещь, которую веб-приложения не могли воспроизвести. Хотя эти виджеты могут выглядеть достаточно похоже на реальные, вспомогательные технологии (например, программы чтения с экрана) могут работать только с базовыми элементами HTML.
Даже если вы потратите время на то, чтобы правильно настроить все взаимодействия с клавиатурой, некоторые пользователи часто не смогут легко работать с этими настраиваемыми элементами.
ARIA была создана Инициативой веб-доступности W3C (WAI) в 2008 году для решения этой проблемы. На поверхностном уровне это набор атрибутов, которые вы можете добавить в HTML, чтобы сделать его значимым для вспомогательного программного обеспечения, такого как программа чтения с экрана.
ARIA состоит из двух основных компонентов, которые взаимодействуют друг с другом:
Во-первых, это role
атрибут. Этот атрибут имеет предопределенный набор возможных значений: menu, dialog, radiogroup
и т. д. role
Атрибут не добавляет никакого поведения к элементам HTML. Скорее, это обещание, которое вы даете пользователю. Когда вы аннотируете элемент как role='menu'
, вы говорите: « Я заставлю этот элемент работать как меню».
Если вы добавите элемент role
к элементу, но не выполните обещание, впечатление для многих пользователей будет хуже , чем если бы элемент role
вообще не имелся. Так написано:
Никакая АРИЯ лучше, чем Плохая АРИЯ.
— W3C, прочтите меня первым | APG https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/
Второй компонент ARIA — это состояния и свойства , имеющие общий aria-
префикс: aria-expanded, aria-controls, aria-label
и т. д. Эти атрибуты могут определять различные вещи, такие как состояние виджета, отношения между компонентами или дополнительную семантику. Еще раз: эти атрибуты являются обещаниями , а не реализацией.
Вместо того, чтобы изучать все роли и атрибуты и пытаться объединить их в удобный виджет, для большинства разработчиков лучше всего полагаться на ARIA Authoring Practices Guide (APG), веб-ресурс с практической информацией, предназначенный непосредственно для веб-разработчиков. .
Если вы новичок в ARIA, посетите следующие ресурсы W3C:
Всегда не забывайте тестировать свой веб-сайт на доступность, чтобы все пользователи могли легко и эффективно взаимодействовать с ним.
После этого краткого введения в ARIA давайте вернемся к раскрывающемуся меню VanillaJS. Мы начнем с шаблона RSJS: запросим все элементы с некоторым атрибутом данных, пройдемся по ним, получим всех соответствующих потомков.
Обратите внимание, что ниже мы немного модифицировали шаблон RSJS для интеграции с htmx; мы загружаем меню переполнения, когда htmx загружает новый контент.
С RSJS вы будете
document.querySelectorAll(…).forEach
много писать.Чтобы сохранить чистоту HTML, мы используем здесь атрибуты ARIA, а не пользовательские атрибуты данных.
Используйте оператор распространения, чтобы преобразовать a
NodeList
в обычныйArray
.Инициализируйте все дополнительные меню при загрузке страницы или при вставке содержимого с помощью htmx.
Традиционно мы отслеживали бы, открыто ли меню, используя переменную JavaScript или свойство в объекте состояния JavaScript. Этот подход распространен в больших веб-приложениях с большим количеством JavaScript.
Однако у этого подхода есть один недостаток:
Нам нужно будет синхронизировать DOM с состоянием (сложнее без фреймворка).
Мы потеряем возможность сериализации HTML (поскольку это открытое состояние сохраняется не в DOM, а в JavaScript).
Вместо этого подхода мы будем использовать DOM для хранения нашего состояния. Мы воспользуемся атрибутом hidden
элемента меню, чтобы сообщить нам, что он закрыт. Если HTML-код страницы сделан и восстановлен, меню также можно восстановить, просто перезапустив JS.
Атрибут
hidden
отображается какhidden
свойство , поэтому нам не нужно его использоватьgetAttribute
.
Мы также сделаем пункты меню недоступными для вкладок, чтобы мы могли самостоятельно управлять их фокусом.
Теперь давайте реализуем переключение меню на JavaScript:
Необязательный параметр для указания желаемого состояния. Это позволяет нам использовать одну функцию для открытия, закрытия или переключения меню.
Фокус на первый пункт меню при открытии.
Вызов
toggleMenu
с текущим состоянием для инициализации атрибутов элемента.Переключение меню при нажатии кнопки.
Закрыть меню, когда фокус переместится.
Давайте также сделаем так, чтобы меню закрывалось, когда мы щелкаем за его пределами. Это хорошее поведение, имитирующее работу встроенных раскрывающихся меню. Для этого потребуется прослушиватель событий во всем окне.
Обратите внимание, что нам нужно быть осторожными с этим типом прослушивателей: вы можете обнаружить, что прослушиватели накапливаются по мере того, как компоненты добавляют прослушиватели, и не могут их удалить, когда компонент удаляется из DOM. Это, к сожалению, приводит к трудно отслеживаемым утечкам памяти.
В JavaScript нет простого способа выполнения логики при удалении элемента. Лучшим вариантом является так называемый MutationObserver
API. A MutationObserver
очень полезен, но API довольно тяжелый и немного непонятный, поэтому мы не будем использовать его в нашем примере.
Вместо этого мы будем использовать простой шаблон, чтобы избежать утечки прослушивателей событий: когда наш прослушиватель событий запускается, мы проверяем, находится ли присоединяемый компонент все еще в DOM, и, если элемент больше не находится в DOM, мы удалим прослушиватель. и выйти.
Это несколько хакерская ручная форма сборки мусора . Как (обычно) в случае с другими алгоритмами сборки мусора, наша стратегия удаляет прослушиватели через недетерминированный промежуток времени после того, как они больше не нужны. К счастью для нас, поскольку такое частое событие, как «пользователь нажимает в любом месте страницы», приводит к сбору данных, оно должно работать достаточно хорошо для нашей системы.
Эта строка — сбор мусора.
Если щелчок находится за пределами меню, закройте меню.
Теперь давайте перейдем к взаимодействию с клавиатурой для нашего раскрывающегося меню. Все обработчики клавиатуры очень похожи друг на друга и не особенно сложны, поэтому давайте разберем их все за один раз:
Помощник: получить индекс в массиве элементов текущего пункта меню (0, если его нет).
Переместите фокус на предыдущий пункт меню при нажатии клавиши со стрелкой вверх.
Переместите фокус на следующий пункт меню при нажатии клавиши со стрелкой вниз.
Активируйте текущий элемент, находящийся в фокусе, при нажатии клавиши пробела.
Переместите фокус на первый пункт меню при нажатии кнопки «Домой».
Переместите фокус на последний пункт меню при нажатии кнопки «Конец».
Закрыть меню при нажатии Escape.
Вернуть фокус на кнопку меню при закрытии меню.
Это должно охватывать все наши основы, и мы признаем, что это очень много кода. Но, честно говоря, этот код кодирует большую часть поведения.
Но для нашего относительно простого варианта использования ванильный JavaScript отлично справляется, и при его реализации нам пришлось изучить ARIA и RSJS.
Альпийский.js
Alpine — это относительно новая библиотека JavaScript, которая позволяет разработчикам встраивать код JavaScript непосредственно в HTML, аналогично атрибутам, on*
доступным в простом HTML и JavaScript. Однако Alpine развивает концепцию встроенных сценариев гораздо дальше, чем on*
атрибуты.
Alpine позиционирует себя как современную замену jQuery, широко используемой старой библиотеки JavaScript. Как вы увидите, он определенно оправдывает это обещание.
Установить Alpine очень просто: это один файл, не зависящий от зависимостей, поэтому вы можете просто подключить его через CDN:
Вы также можете установить его через менеджер пакетов, такой как NPM, или продать его со своего собственного сервера.
Alpine предоставляет набор атрибутов HTML, все из которых начинаются с префикса x-
, основной из которых — x-data
. Содержимое x-data
— это выражение JavaScript, результатом которого является объект. Таким образом, к свойствам этого объекта можно получить доступ внутри элемента, в котором x-data
расположен атрибут.
Чтобы получить представление о AlpineJS, давайте посмотрим, как реализовать с его помощью наш встречный пример.
Для счетчика единственное состояние, которое нам нужно отслеживать, — это текущее число, поэтому давайте объявим объект JavaScript с одним свойством , count
в x-data
атрибуте div для нашего счетчика:
Это определяет наше состояние, то есть данные, которые мы будем использовать для динамических обновлений DOM. Объявив такое состояние, мы теперь можем использовать его внутри элемента div, для которого оно объявлено. Добавим output
элемент с x-text
атрибутом.
Далее мы свяжем атрибут x-text
с count
атрибутом, который мы объявили в x-data
атрибуте родительского div
элемента. Это приведет к установке текста элемента output
в любое значение count
: если count
будет обновлено, то же самое произойдет и с текстом output
. Это «реактивное» программирование, при котором DOM «реагирует» на изменения резервных данных.
Атрибут
x-text
.
Далее нам нужно обновить счетчик с помощью кнопки. Alpine позволяет прикреплять прослушиватели событий к x-on
атрибуту.
Чтобы указать событие для прослушивания, вы добавляете двоеточие, а затем имя события после x-on
имени атрибута. Тогда значением атрибута будет код JavaScript, который вы хотите выполнить. Это похоже на простые on*
атрибуты, которые мы обсуждали ранее, но оказывается гораздо более гибким.
Мы хотим прослушивать click
событие и хотим увеличивать его count
при щелчке, поэтому вот как будет выглядеть код Alpine:
С помощью
x-on
мы указываем атрибут в имени атрибута .
И это все, что нужно. Простой компонент, такой как счетчик, должен быть простым в кодировании, и Alpine это обеспечивает.
«x-on:click» против «onclick»
Как мы уже говорили, атрибут Alpine x-on:click
(или его сокращение — @click
атрибут) аналогичен встроенному onclick
атрибуту. Однако у него есть дополнительные функции, которые делают его значительно более полезным:
Вы можете прослушивать события от других элементов. Например,
.outside
модификатор позволяет прослушивать любое событие щелчка, которое не находится внутри элемента.Вы можете использовать другие модификаторы, чтобы:
прослушиватели событий дросселирования или устранения дребезга
игнорировать события, которые всплывают из элементов-потомков
подключить пассивных слушателей
Вы можете слушать пользовательские события. Например, если вы хотите прослушать событие,
htmx:after-request
вы можете написатьx-on:htmx:after-request="doSomething()"
.
Реактивность и шаблонизация
Мы надеемся, что вы согласитесь, что версия виджета счетчика AlpineJS в целом лучше, чем реализация VanillaJS, которая была либо несколько хакерской, либо разбросана по нескольким файлам.
Большая часть возможностей AlpineJS заключается в том, что он поддерживает понятие «реактивных» переменных, позволяя вам привязать счетчик элемента div
к переменной, на которую могут ссылаться как the, output
так и the button
, и правильно обновлять все зависимости при возникновении мутации. . Alpine позволяет выполнять гораздо более сложные привязки данных, чем мы продемонстрировали здесь, и это отличная библиотека сценариев на стороне клиента общего назначения.
Alpine.js в действии: панель инструментов массовых действий
Давайте реализуем функцию в Contact.app с помощью Alpine. В настоящее время в Contact.app есть кнопка «Удалить выбранные контакты» в самом низу страницы. У этой кнопки длинное название, ее нелегко найти, и она занимает много места. Если бы мы хотели добавить дополнительные «массовые» действия, это не было бы хорошо масштабируемо визуально.
В этом разделе мы заменим эту единственную кнопку панелью инструментов. Кроме того, панель инструментов появится только тогда, когда пользователь начнет выбирать контакты. Наконец, он покажет, сколько контактов выбрано, и позволит вам выбрать все контакты за один раз.
Первое, что нам нужно будет добавить, — это x-data
атрибут, сохраняющий состояние, которое мы будем использовать для определения, видна ли панель инструментов или нет. Нам нужно будет разместить это в родительском элементе как панели инструментов, которую мы собираемся добавить, так и флажков, которые будут обновлять состояние, когда они отмечены и сняты. Лучший вариант, учитывая наш текущий HTML, — поместить атрибут в form
элемент, окружающий таблицу контактов. Мы объявим свойство, selected
которое будет представлять собой массив, содержащий выбранные идентификаторы контактов на основе установленных флажков.
Вот как будет выглядеть наш тег формы:
Эта форма располагается вокруг таблицы контактов.
Далее вверху таблицы контактов мы добавим тег template
. Тег шаблона по умолчанию не отображается браузером, поэтому вы можете быть удивлены тем, что мы его используем. Однако, добавив x-if
атрибут Alpine, мы можем сказать Alpine: если условие истинно, отобразить HTML в этом шаблоне.
Напомним, что мы хотим показывать панель инструментов тогда и только тогда, когда выбран один или несколько контактов. Но мы знаем, что в свойстве у нас будут идентификаторы выбранных контактов selected
. Таким образом, мы можем довольно легко проверить длину этого массива, чтобы увидеть, есть ли какие-либо выбранные контакты:
Показывать этот HTML-код, если выбран один или несколько контактов.
Мы реализуем эти кнопки буквально через мгновение.
Следующий шаг — убедиться, что переключение флажка для данного контакта добавляет (или удаляет) идентификатор данного контакта из свойства selected
. Для этого нам нужно будет использовать новый атрибут Alpine x-model
. Атрибут x-model
позволяет вам привязать данный элемент к некоторым базовым данным или его «модели».
В этом случае мы хотим привязать значение входных данных флажка к свойству selected
. Вот как мы это делаем:
Атрибут
x-model
привязываетvalue
этот вход кselected
свойству
Теперь, когда флажок установлен или снят, selected
массив будет обновлен с использованием идентификатора контакта данной строки. Более того, изменения, которые мы вносим в selected
массив, будут аналогичным образом отражаться на состоянии флажков. Это известно как двусторонняя привязка.
Написав этот код, мы можем заставить панель инструментов появляться и исчезать в зависимости от того, установлены ли флажки контактов.
Очень ловко.
Прежде чем мы продолжим, вы, возможно, заметили, что наш код включает в себя несколько ссылок «class=». Они предназначены для стилей CSS и не являются частью Alpine.js. Мы включили их только как напоминание о том, что для хорошей работы строки меню, которую мы создаем, потребуется CSS. Классы в приведенном выше коде относятся к минимальной библиотеке CSS под названием Missing.css. Если вы используете другие библиотеки CSS, такие как Bootstrap, Tailwind, Bulma, Pico.css и т. д., ваш код стилей будет другим.
Реализация действий
Теперь, когда у нас есть механизм отображения и скрытия панели инструментов, давайте посмотрим, как реализовать кнопки на панели инструментов.
Давайте сначала реализуем кнопку «Очистить», потому что это довольно просто. Все, что нам нужно сделать, это при нажатии кнопки очистить selected
массив. Из-за двусторонней привязки, предоставляемой Alpine, при этом отметки со всех выбранных контактов будут сняты (а затем скроется панель инструментов)!
Для кнопки «Отмена» наша работа проста:
Сбросьте
selected
массив.
И снова AlpineJS делает это очень легко.
Однако кнопка «Удалить» будет немного сложнее. Ему нужно будет сделать две вещи: во-первых, он подтвердит, действительно ли пользователь намерен удалить выбранные контакты. Затем, если пользователь подтвердит действие, он будет использовать API JavaScript htmx для отправки запроса DELETE
.
Подтвердите, что пользователь желает удалить выбранное количество контактов.
Выполните ошибку
DELETE
с использованием API JavaScript htmx.
Обратите внимание, что мы используем укороченное поведение оператора &&
в JavaScript, чтобы избежать вызова, htmx.ajax()
если confirm()
вызов возвращает false.
Эта htmx.ajax()
функция — это всего лишь способ доступа к обычному обмену гипермедиа на основе HTML, который HTML-атрибуты htmx предоставляют вам непосредственно из JavaScript.
Глядя на то, как мы вызываем htmx.ajax
, мы сначала сообщаем, что хотим выдать DELETE
to /contacts
. Затем мы передаем две дополнительные части информации: source
и target
. Свойство source
— это элемент, из которого htmx будет собирать данные для включения в запрос. Мы устанавливаем для него значение $root
, которое является специальным символом в Alpine и будет элементом, для которого x-data
объявлен атрибут. В данном случае это будет форма, содержащая все наши контакты. HTML- код target
или место, где будет размещен ответный HTML, представляет собой тело всего документа, поскольку DELETE
по завершении обработчик возвращает целую страницу.
Обратите внимание, что здесь мы используем Alpine для совместимости с приложениями, управляемыми гипермедиа. Мы могли бы отправить запрос AJAX непосредственно из Alpine и, возможно, обновить x-data
свойство в зависимости от результатов этого запроса. Но вместо этого мы делегировали JavaScript API htmx, который осуществил обмен гипермедиа с сервером.
Это ключ к написанию сценариев, дружественных к гипермедиа, в приложении, управляемом гипермедиа.
Итак, теперь, когда все это есть, у нас теперь есть значительно улучшенный опыт выполнения массовых действий с контактами: меньше визуального беспорядка, а панель инструментов можно расширить за счет большего количества опций, не создавая раздутия в основном интерфейсе нашего приложения.
_гиперскрипт
Хотя предыдущие два примера ориентированы на JavaScript, _hyperscript имеет совершенно другой синтаксис, чем JavaScript, и основан на более старом языке HyperTalk. HyperTalk был языком сценариев для технологии HyperCard, старой гипермедийной системы, доступной на ранних компьютерах Macintosh.
Самое примечательное в _hyperscript то, что он больше напоминает английскую прозу, чем другие языки программирования.
Как и Alpine, _hyperscript — это современная замена jQuery. Также, как и Alpine, _hyperscript позволяет вам писать сценарии в HTML.
Однако, в отличие от Alpine, _hyperscript не является реактивным. Вместо этого он фокусируется на упрощении записи и чтения манипуляций с DOM в ответ на события. Он имеет встроенные языковые конструкции для многих операций DOM, что избавляет вас от необходимости перемещаться по иногда многословным API-интерфейсам JavaScript DOM.
Мы дадим небольшое представление о том, что представляет собой создание сценариев на языке _hyperscript, чтобы вы могли изучить этот язык более подробно позже, если он покажется вам интересным.
Как и htmx и AlpineJS, _hyperscript можно установить через CDN или из npm (имя пакета hyperscript.org
):
_hyperscript использует \_
атрибут (подчеркивание) для размещения сценариев в элементах DOM. Вы также можете использовать атрибуты script
или data-script
в зависимости от ваших потребностей в проверке HTML.
Давайте посмотрим, как реализовать простой компонент счетчика, который мы рассматривали, с помощью _hyperscript. Мы поместим output
элемент и button
внутреннюю часть файла div
. Чтобы реализовать счетчик, нам нужно добавить к кнопке небольшой фрагмент _hyperscript. При нажатии кнопка должна увеличивать текст предыдущего output
тега.
Как вы увидите, последнее предложение близко к реальному коду _hyperscript:
К кнопке добавлен код _hyperscript.
Давайте пройдемся по каждому компоненту этого скрипта:
on click
— это прослушиватель событий, сообщающий кнопке прослушивание события,click
а затем выполняющий оставшийся код.increment
— это «команда» в _hyperscript, которая «увеличивает» элементы, аналогично++
оператору в JavaScript.the
не имеет никакого семантического значения в _hyperscript, но может использоваться для повышения читабельности сценариев.textContent of
— это одна из форм доступа к свойствам в _hyperscript. Вероятно, вы знакомы с синтаксисом JavaScripta.b
, который означает «Получить свойствоb
объекта и» . Какой из них вы используете, зависит от того, какой из них наиболее читабельен.a.`" _hyperscript supports this syntax, but
also
supports the forms `b of aa’s b
previous
— это выражение в _hyperscript, которое находит предыдущий элемент в DOM, соответствующий некоторому условию.<output />
— это литерал запроса , представляющий собой селектор CSS, заключенный между<
и/>
.
В этом коде previous
ключевое слово (и сопровождающее его next
ключевое слово) является примером того, как _hyperscript упрощает операции с DOM: в стандартном API DOM нет такой встроенной функциональности, и реализовать ее в VanillaJS сложнее, чем вы думаете!
Итак, как видите, _hyperscript очень выразителен, особенно когда дело касается манипуляций с DOM. Это упрощает встраивание сценариев непосредственно в HTML: поскольку язык сценариев более мощный, сценарии, написанные на нем, обычно короче и их легче читать.
Программирование на естественном языке?
Опытные программисты могут с подозрением относиться к _hyperscript: существует множество проектов «программирования на естественном языке» (НЛП), ориентированных на непрограммистов и начинающих программистов, предполагающих, что способность читать код на своем «естественном языке» даст им возможность писать. это тоже. Это привело к появлению плохо написанного и структурированного кода, который не оправдал (часто чрезмерной) шумихи.
_hyperscript не является языком программирования НЛП. Да, его синтаксис во многих местах вдохновлен речевыми шаблонами веб-разработчиков. Но читаемость _hyperscript достигается не за счет сложной эвристики или нечеткой обработки НЛП, а, скорее, за счет разумного использования общих приемов синтаксического анализа в сочетании с культурой читабельности.
Как вы можете видеть в приведенном выше примере , при использовании ссылки на запрос<output/>
_hyperscript не уклоняется от использования неестественного языка, специфичного для DOM, когда это необходимо.
_hyperscript в действии: сочетание клавиш
Хотя демонстрация счетчика — хороший способ сравнить различные подходы к написанию сценариев, резина встречается с дорогой, когда вы пытаетесь реально реализовать полезную функцию с помощью подхода. Для _hyperscript давайте добавим сочетание клавиш в Contact.app: когда пользователь нажимает Alt+S в нашем приложении, мы фокусируем поле поиска.
Поскольку наше сочетание клавиш фокусируется на вводе поиска, давайте поместим для него код на этот ввод поиска, удовлетворяющий локальности.
Вот исходный HTML-код для ввода поиска:
Мы добавим прослушиватель событий, используя on keydown
синтаксис, который будет срабатывать при каждом нажатии клавиши. Кроме того, мы можем использовать синтаксис фильтра _event в _hyperscript, используя квадратные скобки после события. В квадратных скобках мы можем поместить выражение фильтра , которое будет отфильтровывать keydown
события, которые нас не интересуют. В нашем случае мы хотим учитывать только события, в которых удерживается клавиша Alt и нажимается клавиша «S». Для достижения этой цели мы можем создать логическое выражение, которое проверяет altKey
свойство (на предмет того, является ли оно true
) и code
свойство (на предмет того, является ли оно "KeyS"
) события.
Пока что наш _hyperscript выглядит так:
Теперь по умолчанию _hyperscript будет прослушивать данное событие _в элементе, в котором оно объявлено. Итак, с помощью имеющегося у нас сценария мы будем получать keydown
события только в том случае, если поле поиска уже находится в фокусе. Это не то, чего мы хотим! Мы хотим, чтобы этот ключ работал глобально , независимо от того, какой элемент находится в фокусе.
Не проблема! Мы можем прослушивать событие keyDown
в другом месте, используя from
предложение в нашем обработчике событий. В этом случае мы хотим прослушивать команду keyDown
из окна, и наш код, естественно, выглядит следующим образом:
Используя это from
предложение, мы можем прикрепить прослушиватель к окну, в то же время сохраняя код в элементе, к которому оно логически относится.
Теперь, когда мы выбрали событие, которое хотим использовать для фокусировки поля поиска, давайте реализуем фактическую фокусировку, вызвав стандартный .focus()
метод.
Вот весь скрипт, встроенный в HTML:
«Я» относится к элементу, на котором написан сценарий.
Учитывая всю функциональность, он на удивление краток и, как английский язык программирования, довольно легко читается.
Почему новый язык программирования?
Это все хорошо, но вы можете подумать: «Совершенно новый язык сценариев? Это кажется чрезмерным». И в каком-то смысле вы правы: JavaScript — достойный язык сценариев, он очень хорошо оптимизирован и широко используется в веб-разработке. С другой стороны, создав совершенно новый интерфейсный язык сценариев, мы получили свободу решать некоторые проблемы, с которыми мы столкнулись при создании уродливого и многословного кода в JavaScript:
Асинхронная прозрачность
В _hyperscript асинхронные функции (т. е. функции, возвращающие Promise
экземпляры) могут вызываться _ так, как если бы они были синхронными. Изменение функции с синхронной на асинхронную не нарушает код _hyperscript, который ее вызывает. Это достигается путем проверки промиса при оценке любого выражения и приостановки работающего сценария, если он существует (приостанавливается только текущий обработчик событий, а основной поток не блокируется). Вместо этого JavaScript требует либо явного использования обратных вызовов , либо явных async
аннотаций (которые нельзя смешивать с синхронным кодом).
Доступ к свойству массива
В _hyperscript доступ к свойству массива (отличному от length
числа или числа) вернет массив значений свойств каждого члена этого массива, благодаря чему доступ к свойству массива действует как операция с плоской картой. jQuery имеет аналогичную функцию, но только для собственной структуры данных.
Собственный синтаксис CSS
В _hyperscript вы можете использовать такие вещи, как литералы классов и идентификаторов CSS или литералы запросов CSS, непосредственно в языке, вместо необходимости обращаться к многословному API DOM, как вы это делаете в JavaScript.
Глубокая поддержка событий
Работать с событиями в _hyperscript гораздо приятнее, чем работать с ними в JavaScript, поскольку имеется встроенная поддержка реагирования и отправки событий, а также общих шаблонов обработки событий, таких как «устранение дребезга» или ограничение скорости событий. _hyperscript также предоставляет декларативные механизмы для синхронизации событий внутри данного элемента и между несколькими элементами.
Мы еще раз хотим подчеркнуть, что в этом примере мы не выходим за рамки приложения, управляемого гипермедиа: мы только добавляем клиентскую функциональность с помощью наших сценариев. Мы не создаем и не управляем большим объемом состояния за пределами самого DOM и не общаемся с сервером посредством обмена без гипермедиа.
Кроме того, поскольку _hyperscript так хорошо встраивается в HTML, он фокусируется на гипермедиа , а не на логике сценариев.
Возможно, он не соответствует всем стилям и потребностям сценариев, но _hyperscript может обеспечить превосходные возможности создания сценариев для приложений, управляемых гипермедиа. Это небольшой и малоизвестный язык программирования, на который стоит обратить внимание, чтобы понять, чего он пытается достичь.
Использование готовых компонентов
На этом мы завершаем рассмотрение трех различных вариантов вашей инфраструктуры сценариев, то есть кода, который вы пишете для улучшения своего приложения, управляемого гипермедиа. Однако есть еще одна важная область, которую следует учитывать при обсуждении сценариев на стороне клиента: «готовые» компоненты. То есть библиотеки JavaScript, созданные другими людьми и предлагающие какую-то функциональность, например показ модальных диалогов.
Веб-компоненты/Пользовательские элементы
Веб-компоненты — это собирательное название нескольких стандартов; Пользовательские элементы и Shadow DOM, а <template>
также <slot>
.
Все эти стандарты предоставляют полезные возможности. <template>
элементы удаляют свое содержимое из документа, при этом анализируя его как HTML (в отличие от комментариев) и делая его доступным для JavaScript. Пользовательские элементы позволяют нам инициализировать и удалять поведение при добавлении или удалении элементов, что раньше требовало ручной работы или MutationObservers. Теневой DOM позволяет нам инкапсулировать элементы, оставляя «легкий» (не теневой) DOM чистым.
Однако попытки воспользоваться этими преимуществами часто разочаровывают. Некоторые трудности — это просто проблемы роста новых стандартов (например, проблемы доступности Shadow DOM), над которыми активно работают. Другие являются результатом того, что веб-компоненты пытаются одновременно быть слишком многими вещами:
Механизм расширения для HTML. С этой целью каждый пользовательский элемент представляет собой тег, который мы добавляем в язык.
Механизм жизненного цикла поведения. Такие методы
createdCallback
, какconnectedCallback
, и т. д., позволяют добавлять поведение к элементам без необходимости вызывать их вручную при добавлении этих элементов.Единица инкапсуляции. Shadow DOM изолирует элементы от их окружения.
В результате, если вам нужна одна из этих вещей, другие тоже придут с вами. Если вы хотите прикрепить какое-либо поведение к некоторым элементам с помощью обратных вызовов жизненного цикла, вам необходимо создать новый тег, что означает, что вы не можете иметь несколько вариантов поведения для одного элемента, и вы изолируете добавляемые элементы от элементов, уже находящихся на странице, что проблема, если им нужны отношения ARIA.
Варианты интеграции
Лучшие библиотеки JavaScript для работы при создании приложения, управляемого гипермедиа, — это те, которые:
Измените DOM, но не связывайтесь с сервером через JSON.
Соблюдайте нормы HTML (например, использование
input
элементов для хранения значений).Запускайте множество пользовательских событий по мере обновления библиотеки.
Последний момент — запуск множества пользовательских событий (вместо использования множества методов и обратных вызовов) — особенно важен, поскольку эти пользовательские события можно отправлять или прослушивать без дополнительного связующего кода, написанного на языке сценариев.
Давайте рассмотрим два разных подхода к написанию сценариев: один с использованием обратных вызовов JavaScript, а другой с использованием событий.
Чтобы конкретизировать ситуацию, давайте реализуем улучшенное диалоговое окно подтверждения для DELETE
кнопки, которую мы создали в Alpine в предыдущем разделе. В исходном примере мы использовали confirm()
функцию, встроенную в JavaScript, которая показывает довольно простой диалог подтверждения системы. Мы заменим эту функцию популярной библиотекой JavaScript SweetAlert2, которая отображает гораздо более красивый диалог подтверждения. В отличие от confirm()
функции, которая блокирует и возвращает логическое значение ( true
если пользователь подтвердил, false
в противном случае), SweetAlert2 возвращает объект Promise
, который представляет собой механизм JavaScript для подключения обратного вызова после асинхронного действия (например, ожидания подтверждения или отклонения пользователем действие) завершается.
Интеграция с использованием обратных вызовов
Если SweetAlert2 установлен в виде библиотеки, у вас есть доступ к объекту Swal
, fire()
на котором есть функция, запускающая показ оповещения. Вы можете передать аргументы методу, fire()
чтобы точно настроить внешний вид кнопок в диалоговом окне подтверждения, заголовок диалогового окна и т. д. Мы не будем слишком углубляться в эти детали, но вы вскоре увидите, как выглядит диалог.
Итак, поскольку мы установили библиотеку SweetAlert2, мы можем заменить ее вместо confirm()
вызова функции. Затем нам нужно реструктурировать код, чтобы передать обратный вызовthen()
возвращаемому методу . Глубокое погружение в промисы выходит за рамки этой главы, но достаточно сказать, что этот обратный вызов будет вызываться, когда пользователь подтверждает или отклоняет действие. Если пользователь подтвердил действие, то свойство будет .PromiseSwal.fire()result.isConfirmedtrue
Учитывая все это, наш обновленный код будет выглядеть так:
Вызов
Swal.fire()
функцииНастройте диалог
Обработка результата выбора пользователя
И теперь, когда нажимаем эту кнопку, мы получаем красивый диалог в нашем веб-приложении:
Гораздо приятнее, чем диалог подтверждения системы. Тем не менее, это кажется немного неправильным. Нужно написать много кода только для того, чтобы вызвать немного более приятный результат confirm()
, не так ли? И код JavaScript htmx, который мы здесь используем, кажется неуклюжим. Было бы более естественно перенести htmx в атрибуты кнопки, как мы это делали, а затем инициировать запрос через события.
Итак, давайте воспользуемся другим подходом и посмотрим, как это выглядит.
Интеграция с использованием событий
Чтобы очистить этот код, мы перенесем его Swal.fire()
в созданную нами пользовательскую функцию JavaScript под названием sweetConfirm()
. sweetConfirm()
примет параметры диалога, переданные в fire()
метод, а также элемент, подтверждающий действие. Большая разница здесь в том, что новая sweetConfirm()
функция вместо прямого вызова некоторого htmx вместо этого запускает событие confirmed
на кнопке, когда пользователь подтверждает, что хочет удалить.
Вот как выглядит наша функция JavaScript:
Передайте конфигурацию в
fire()
функцию.Если пользователь подтвердил действие, инициируйте
confirmed
событие.
Благодаря этому методу мы теперь можем немного ужесточить нашу кнопку удаления. Мы можем удалить весь код SweetAlert2, который у нас был в @click
атрибуте Alpine, и просто вызвать этот новый sweetConfirm()
метод, передав аргументы $el
, которые представляют собой синтаксис Alpine для получения «текущего элемента», на котором находится скрипт, а затем точную конфигурацию, которую мы хотим для нашего диалога.
Если пользователь подтвердит действие, confirmed
на кнопке сработает событие. Это означает, что мы можем вернуться к использованию наших проверенных атрибутов htmx! А именно, мы можем перейти DELETE
к hx-delete
атрибуту и использовать его hx-target
для нацеливания на тело. И затем, и это решающий шаг, мы можем использовать событие confirmed
, которое срабатывает в sweetConfirm()
функции, чтобы инициировать запрос, но добавив hx-trigger
для него .
Вот как выглядит наш код:
Наши атрибуты htmx вернулись.
Мы передаем кнопку в функцию, чтобы на ней можно было вызвать событие.
Мы передаем информацию о конфигурации SweetAlert2.
Как видите, этот код, основанный на событиях, намного чище и определенно более «HTML-подобен». Ключом к этой более чистой реализации является то, что наша новая sweetConfirm()
функция запускает событие, которое может прослушивать htmx.
Вот почему при выборе библиотеки для работы важно учитывать богатую модель событий, как с htmx, так и с приложениями, управляемыми гипермедиа, в целом.
К сожалению, из-за распространенности и доминирования сегодня подхода, ориентированного на JavaScript, многие библиотеки похожи на SweetAlert2: они ожидают, что вы передадите обратный вызов в первом стиле. В этих случаях вы можете использовать метод, который мы продемонстрировали здесь, заключая библиотеку в функцию, которая запускает события в обратном вызове, чтобы сделать библиотеку более гипермедиа и htmx-дружественной.
Прагматическое написание сценариев
В случае конфликта отдавайте предпочтение пользователям, а не авторам, а не разработчикам, а не спецификаторам, а не теоретической чистоте.
- W3C, Принципы проектирования HTML § 3.2 Приоритет групп
Мы рассмотрели несколько инструментов и методов создания сценариев в приложении, управляемом гипермедиа. Как следует выбирать между ними? Печальная истина заключается в том, что на этот вопрос никогда не будет единственного и всегда правильного ответа.
Вы привержены использованию только ванильного JavaScript, возможно, из-за политики компании? Что ж, вы можете эффективно использовать ванильный JavaScript для создания сценариев вашего приложения, управляемого гипермедиа.
У вас больше свободы действий и вам нравится внешний вид Alpine.js? Это отличный способ добавить в ваше приложение более структурированный, локализованный JavaScript, а также предлагает несколько приятных реактивных функций.
Вы немного смелее в своем техническом выборе? Возможно, стоит обратить внимание на _hyperscript. (Мы, конечно, так думаем.)
Иногда вы можете даже подумать о выборе двух (или более) из этих подходов в приложении. У каждого есть свои сильные и слабые стороны, и все они относительно небольшие и автономные, поэтому лучшим подходом может быть выбор правильного инструмента для конкретной работы.
В общем, мы поощряем прагматичный подход к написанию сценариев: все, что кажется правильным, вероятно, правильно (или, по крайней мере, достаточно правильно ) для вас. Вместо того, чтобы беспокоиться о том, какой конкретный подход используется для вашего сценария, мы бы сосредоточились на этих более общих проблемах:
Избегайте связи с сервером через API данных JSON.
Избегайте хранения больших объемов состояния за пределами DOM.
Отдавайте предпочтение использованию событий, а не жестко запрограммированных обратных вызовов или вызовов методов.
И даже по этим темам иногда веб-разработчику приходится делать то, что должен делать веб-разработчик. Если идеальный виджет для вашего приложения существует, но использует API данных JSON? Это нормально.
Только не делайте это привычкой.
Примечания к HTML: HTML предназначен для приложений.
Среди разработчиков распространен мем, предполагающий, что HTML был разработан для «документов» и не подходит для «приложений». На самом деле гипермедиа — это не только сложная современная архитектура приложений, но она может позволить нам навсегда покончить с этим искусственным разделением приложений и документов.
Когда я говорю «гипертекст», я имею в виду одновременное представление информации и элементов управления, так что информация становится возможностью, посредством которой пользователь получает возможность выбора и выбирает действия.
HTML позволяет документам содержать богатые мультимедиа, включая изображения, аудио, видео, программы JavaScript, векторную графику и (с некоторой помощью) трехмерную среду. Однако, что еще более важно, он позволяет встраивать интерактивные элементы управления в эти документы, позволяя самой информации быть приложением, через которое к ней осуществляется доступ.
Подумайте: разве не ошеломляет тот факт, что одно приложение, работающее на всех типах компьютеров и операционных систем, может позволить вам читать новости, совершать видеозвонки, составлять документы, входить в виртуальные миры и выполнять практически любые другие повседневные вычислительные задачи?
К сожалению, именно интерактивные возможности HTML являются его наименее развитым аспектом. По неизвестным нам причинам, несмотря на то, что HTML дошел до версии 5 и стал стандартом жизни, прибавив по пути множество революционных функций, взаимодействие с данными в нем по-прежнему в основном ограничивается ссылками и формами. Разработчики должны расширять HTML, и мы хотим сделать это таким образом, чтобы не абстрагироваться от его простоты и не подражать классическим «родным» инструментариям.
ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ НЕ ДОЛЖНО ИСПОЛЬЗОВАТЬ НАТИВНЫЕ ИНСТРУМЕНТАРИИ
ГОДЫ БИБЛИОТЕК WINDOWS UI, но НЕ НАЙДЕНО РЕАЛЬНОГО ПРИМЕНЕНИЯ для перехода на более низкий уровень, чем ИНТЕРНЕТ.
Все равно хотели окно, чтобы посмеяться? Для этого у нас был инструмент: он назывался «ЭЛЕКТРОН».
«да, я бы хотел написать 4 РАЗНЫЕ копии одного и того же пользовательского интерфейса» — утверждения, придуманные Совершенно невменяемым.
— Лия Кларк, @leah@tilde.zone
Last updated