Святой Грааль повторно используемых компонентов: пользовательские элементы, Shadow DOM и NPM

Опубликовано: 2022-03-10
Краткое резюме ↬ В этой статье рассматривается расширение HTML с помощью компонентов со встроенными функциями и стилями. Мы также узнаем, как сделать эти пользовательские элементы повторно используемыми в проектах с помощью NPM.

Даже для самых простых компонентов стоимость человеческого труда могла быть значительной. UX-команды проводят юзабилити-тестирование. Массив заинтересованных сторон должен подписать дизайн.

Разработчики проводят AB-тесты, аудиты доступности, модульные тесты и кроссбраузерные проверки. Как только вы решили проблему, вы не хотите повторять эти усилия . Создавая повторно используемую библиотеку компонентов (вместо создания всего с нуля), мы можем постоянно использовать прошлые усилия и избегать повторного обращения к уже решенным задачам проектирования и разработки.

Скриншот веб-сайта компонентов материалов Google, показывающий различные компоненты.
Большой превью

Создание арсенала компонентов особенно полезно для таких компаний, как Google, которые владеют значительным портфелем веб-сайтов, объединенных общим брендом. Преобразовывая свой пользовательский интерфейс в компонуемые виджеты, крупные компании могут сократить время разработки и добиться согласованности визуального дизайна и дизайна взаимодействия с пользователем в разных проектах. За последние несколько лет наблюдается рост интереса к руководствам по стилю и библиотекам шаблонов. Учитывая, что несколько разработчиков и дизайнеров распределены по нескольким командам, крупные компании стремятся достичь согласованности. Мы можем сделать лучше, чем простые образцы цветов. Нам нужен легко распространяемый код .

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

Копировать и вставлять код вручную не составляет труда. Однако поддержание этого кода в актуальном состоянии — кошмар обслуживания. Поэтому многие разработчики полагаются на диспетчер пакетов для повторного использования кода в проектах. Несмотря на свое название, Node Package Manager стал непревзойденной платформой для внешнего управления пакетами. В настоящее время в реестре NPM содержится более 700 000 пакетов, и каждый месяц загружаются миллиарды пакетов. Любую папку с файлом package.json можно загрузить в NPM как общий пакет. Хотя NPM в основном связан с JavaScript, пакет может включать CSS и разметку. NPM упрощает повторное использование и, что важно, обновление кода. Вместо того, чтобы изменять код во множестве мест, вы меняете код только в пакете.

Еще после прыжка! Продолжить чтение ниже ↓

Проблема разметки

Sass и Javascript легко переносятся с помощью операторов импорта. Языки шаблонов предоставляют HTML такую ​​же возможность — шаблоны могут импортировать другие фрагменты HTML в виде частей. Например, вы можете написать разметку для нижнего колонтитула один раз, а затем включить ее в другие шаблоны. Сказать, что существует множество языков шаблонов, было бы преуменьшением. Привязка себя только к одному серьезно ограничивает потенциальное повторное использование вашего кода. Альтернативой является копирование и вставка разметки и использование NPM только для стилей и javascript.

Это подход, принятый Financial Times с их библиотекой компонентов Origami . В своем выступлении «Разве вы не можете просто сделать это более похожим на Bootstrap?» Элис Бартлетт пришла к выводу, что «нет хорошего способа позволить людям включать шаблоны в свои проекты». Рассказывая о своем опыте обслуживания библиотеки компонентов в Lonely Planet, Ян Фезер вновь упомянул о проблемах, связанных с этим подходом:

«Как только они копируют этот код, они, по сути, вырезают версию, которую нужно поддерживать неопределенно долго. Когда они копировали разметку для рабочего компонента, у нее была неявная ссылка на моментальный снимок CSS на тот момент. Если вы затем обновите шаблон или выполните рефакторинг CSS, вам потребуется обновить все версии шаблона, разбросанные по вашему сайту».

Решение: веб-компоненты

Веб-компоненты решают эту проблему, определяя разметку в JavaScript. Автор компонента может изменять разметку, CSS и Javascript. Потребитель компонента может извлечь выгоду из этих обновлений без необходимости вручную просматривать проект, изменяя код. Синхронизация с последними изменениями в рамках всего проекта может быть достигнута с помощью npm update через терминал. Только имя компонента и его API должны оставаться согласованными.

Установить веб-компонент так же просто, как npm install component-name в терминал. Javascript может быть включен в оператор импорта:

 <script type="module"> import './node_modules/component-name/index.js'; </script>

Затем вы можете использовать компонент в любом месте вашей разметки. Вот простой пример компонента, который копирует текст в буфер обмена.

См. демонстрацию веб-компонента Pen Simple от CSS GRID (@cssgrid) на CodePen.

См. демонстрацию веб-компонента Pen Simple от CSS GRID (@cssgrid) на CodePen.

Компонентно-ориентированный подход к фронтенд-разработке стал повсеместным благодаря платформе React от Facebook. Неизбежно, учитывая широкое распространение фреймворков в современных интерфейсных рабочих процессах, ряд компаний создали библиотеки компонентов, используя предпочитаемые ими фреймворки. Эти компоненты можно повторно использовать только в рамках этой конкретной структуры.

Компонент из IBM Carbon Design System
Компонент из IBM Carbon Design System. Только для использования в приложениях React. Другие важные примеры библиотек компонентов, созданных в React, включают Atlaskit от Atlassian и Polaris от Shopify. (Большой превью)

У крупной компании редко бывает единый интерфейс, и переход с одного фреймворка на другой не является чем-то необычным. Фреймворки приходят и уходят. Чтобы обеспечить максимально возможное повторное использование в проектах, нам нужны компоненты, не зависящие от фреймворка .

Скриншот с npmjs.com, показывающий компоненты, которые делают то же самое, созданные исключительно для определенных фреймворков javascript.
Поиск компонентов через npmjs.com показывает фрагментированную экосистему Javascript. (Большой превью)
График, показывающий популярность фреймворков с течением времени. Популярность Ember, Knockout и Backbone резко упала, их заменили более новые предложения.
Постоянно меняющаяся популярность фреймворков с течением времени. (Большой превью)
«Я создавал веб-приложения, используя: Dojo, Mootools, Prototype, jQuery, Backbone, Thorax и React на протяжении многих лет… Мне бы очень хотелось иметь возможность перенести этот убойный компонент Dojo, над которым я трудился вместе со мной, в свой React. приложение сегодняшнего дня».

— Дион Алмаер, технический директор, Google.

Когда мы говорим о веб-компоненте, мы говорим о сочетании пользовательского элемента с теневым DOM. Пользовательские элементы и теневой DOM являются частью как спецификации W3C DOM, так и стандарта WHATWG DOM , что означает, что веб-компоненты являются веб-стандартом . Пользовательские элементы и теневой DOM наконец -то настроены на поддержку кроссбраузерности в этом году. Используя стандартную часть собственной веб-платформы, мы гарантируем, что наши компоненты смогут выжить в быстро меняющемся цикле реструктуризации внешнего интерфейса и переосмысления архитектуры. Веб-компоненты можно использовать с любым языком шаблонов и любой интерфейсной структурой — они действительно кросс-совместимы и интероперабельны. Их можно использовать везде, от блога Wordpress до одностраничного приложения.

Проект Custom Elements Everywhere Роба Додсона документирует совместимость веб-компонентов с различными средами Javascript на стороне клиента.
Проект Custom Elements Everywhere Роба Додсона документирует совместимость веб-компонентов с различными средами Javascript на стороне клиента. Мы надеемся, что React, исключение из этого списка, решит эти проблемы с помощью React 17. (Большой предварительный просмотр)

Создание веб-компонента

Определение пользовательского элемента

Всегда можно было придумывать имена тегов и отображать их содержимое на странице.

 <made-up-tag>Hello World!</made-up-tag>

HTML спроектирован так, чтобы быть отказоустойчивым. Приведенное выше будет отображаться, даже если это недопустимый элемент HTML. Для этого никогда не было веской причины — отклонение от стандартизированных тегов традиционно было плохой практикой. Однако, определив новый тег с помощью API пользовательского элемента, мы можем дополнить HTML повторно используемыми элементами со встроенной функциональностью. Создание пользовательского элемента очень похоже на создание компонента в React, но здесь речь идет о расширении HTMLElement .

 class ExpandableBox extends HTMLElement { constructor() { super() } }

Вызов super() без параметров должен быть первым оператором в конструкторе. Конструктор следует использовать для установки начального состояния и значений по умолчанию, а также для настройки любых прослушивателей событий. Новый пользовательский элемент должен быть определен с именем для его тега HTML и соответствующего класса элементов:

 customElements.define('expandable-box', ExpandableBox)

Это соглашение использовать заглавные буквы в именах классов. Однако синтаксис тега HTML — это больше, чем соглашение. Что, если браузеры захотят реализовать новый HTML-элемент и назовут его расширяемым полем? Чтобы предотвратить конфликты имен, никакие новые стандартизированные HTML-теги не будут содержать тире. Напротив, имена пользовательских элементов должны включать тире.

 customElements.define('whatever', Whatever) // invalid customElements.define('what-ever', Whatever) // valid

Жизненный цикл пользовательского элемента

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

connectedCallback запускается, когда пользовательский элемент добавляется в DOM.

 connectedCallback() { console.log("custom element is on the page!") }

Это включает в себя добавление элемента с помощью Javascript:

 document.body.appendChild(document.createElement("expandable-box")) //“custom element is on the page”

а также просто включение элемента на страницу с тегом HTML:

 <expandable-box></expandable-box> // "custom element is on the page"

Любая работа, связанная с получением ресурсов или рендерингом, должна быть здесь.

disabledCallback запускается, когда пользовательский элемент удаляется из DOM.

 disconnectedCallback() { console.log("element has been removed") } document.querySelector("expandable-box").remove() //"element has been removed"

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

attributeChangedCallback запускается при добавлении, изменении или удалении атрибута. Его можно использовать для прослушивания изменений как стандартизированных нативных атрибутов, таких как disabled или src , так и любых настраиваемых нами атрибутов. Это один из самых мощных аспектов пользовательских элементов, поскольку он позволяет создавать удобный API.

Пользовательские атрибуты элемента

Существует великое множество HTML-атрибутов. Чтобы браузер не тратил время на вызов нашего attributeChangedCallback при изменении любого атрибута, нам нужно предоставить список изменений атрибутов, которые мы хотим прослушивать. В этом примере нас интересует только один.

 static get observedAttributes() { return ['expanded'] }

Так что теперь наш attributeChangedCallback будет вызываться только тогда, когда мы изменим значение расширенного атрибута в пользовательском элементе, поскольку это единственный атрибут, который мы указали.

Атрибуты HTML могут иметь соответствующие значения (например, href, src, alt, value и т. д.), в то время как другие могут быть либо истинными, либо ложными (например , отключенными, выбранными, обязательными ). Для атрибута с соответствующим значением мы бы включили следующее в определение класса пользовательского элемента.

 get yourCustomAttributeName() { return this.getAttribute('yourCustomAttributeName'); } set yourCustomAttributeName(newValue) { this.setAttribute('yourCustomAttributeName', newValue); }

Для элемента из нашего примера атрибут будет либо истинным, либо ложным, поэтому определение геттера и сеттера немного отличается.

 get expanded() { return this.hasAttribute('expanded') } // the second argument for setAttribute is mandatory, so we'll use an empty string set expanded(val) { if (val) { this.setAttribute('expanded', ''); } else { this.removeAttribute('expanded') } }

Теперь, когда с шаблоном разобрались, мы можем использовать attributeChangedCallback .

 attributeChangedCallback(name, oldval, newval) { console.log(`the ${name} attribute has changed from ${oldval} to ${newval}!!`); // do something every time the attribute changes }

Традиционно настройка компонента Javascript включала бы передачу аргументов в функцию init . Используя attributeChangedCallback , можно создать настраиваемый элемент, который можно настроить только с помощью разметки.

Shadow DOM и пользовательские элементы можно использовать отдельно, и вы можете найти пользовательские элементы полезными сами по себе. В отличие от теневого DOM, они могут быть полифилами. Тем не менее, эти две спецификации хорошо работают вместе.

Прикрепление разметки и стилей с помощью Shadow DOM

До сих пор мы обрабатывали поведение пользовательского элемента. Однако с точки зрения разметки и стилей наш пользовательский элемент эквивалентен пустому элементу <span> без стилей. Чтобы инкапсулировать HTML и CSS как часть компонента, нам нужно прикрепить теневой DOM. Лучше всего сделать это в функции-конструкторе.

 class FancyComponent extends HTMLElement { constructor() { super() var shadowRoot = this.attachShadow({mode: 'open'}) shadowRoot.innerHTML = `<h2>hello world!</h2>` }

Не беспокойтесь о понимании того, что означает режим — его шаблон вы должны включить, но вы почти всегда будете хотеть open . Этот простой пример компонента просто отобразит текст «hello world». Как и у большинства других элементов HTML, у пользовательского элемента могут быть дочерние элементы, но не по умолчанию. Пока что указанный выше пользовательский элемент, который мы определили, не будет отображать дочерние элементы на экране. Чтобы отобразить любой контент между тегами, нам нужно использовать элемент slot .

 shadowRoot.innerHTML = ` <h2>hello world!</h2> <slot></slot> `

Мы можем использовать тег стиля, чтобы применить CSS к компоненту.

 shadowRoot.innerHTML = `<style> p { color: red; } </style> <h2>hello world!</h2> <slot>some default content</slot>`

Эти стили будут применяться только к компоненту, поэтому мы можем использовать селекторы элементов без влияния стилей на что-либо еще на странице. Это упрощает написание CSS, делая ненужными соглашения об именах, такие как BEM.

Публикация компонента в NPM

Пакеты NPM публикуются через командную строку. Откройте окно терминала и перейдите в каталог, который вы хотите превратить в многоразовый пакет. Затем введите в терминал следующие команды:

  1. Если в вашем проекте еще нет файла package.json, npm init поможет вам создать его.
  2. npm adduser связывает вашу машину с вашей учетной записью NPM. Если у вас нет существующей учетной записи, она создаст новую для вас.
  3. npm publish
Пакеты NPM публикуются через командную строку
Большой превью

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

Пример компонента в реестре NPM, готового к установке и использованию в ваших собственных проектах.
Большой превью

API веб-компонентов не идеален. Пользовательские элементы в настоящее время не могут включать данные в формы. История с прогрессивным улучшением не очень хороша. Работа с доступностью не так проста, как должна быть.

Хотя первоначально было объявлено в 2011 году, поддержка браузеров по-прежнему не универсальна. Поддержка Firefox должна появиться позже в этом году. Тем не менее, некоторые известные веб-сайты (например, Youtube) уже используют их. Несмотря на их нынешние недостатки, для общедоступных компонентов они являются единственным вариантом, и в будущем мы можем ожидать интересных дополнений к тому, что они могут предложить.