Переиспользуемые компоненты: Custom Elements, Shadow DOM и NPM

Скриншот с сайта Google’s material components – демонстрирует набор различных компонентов.

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

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

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

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

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

Веб-компоненты

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

Устанавливать веб-компоненты очень просто, достаточно набрать npm install component-name в окне терминала. И с помощью оператора импорта включить его в свой Javascript.

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

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

Компонентно-ориентированный подход к фронтенд разработке активно развивается и распространяется, например React в Facebook.

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

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

Результаты поиска компонентов через npmjs.com подтверждают фрагментированную экосистему Javascript.
Результаты поиска компонентов через npmjs.com подтверждают фрагментированную экосистему Javascript.
Изменчивая популярность фреймворков с течением времени.
Изменчивая популярность фреймворков с течением времени.

Когда мы говорим о веб-компоненте, мы говорим о комбинации пользовательского элемента с shadow DOM. Custom Elements и shadow DOM часть спецификации W3C DOM и WHATWG DOM Standard — это означает, что веб-компоненты являются веб-стандартом.

Custom Elements и shadow DOM наконец-то настроены на поддержку кросс-браузерности. Используя стандартную часть собственной веб-платформы, мы гарантируем, что наши компоненты смогут пережить стремительный цикл реструктуризации внешнего интерфейса и переосмысления архитектуры. Веб-компоненты можно использовать с любым языком шаблонов и любой интерфейсной средой – они действительно взаимно совместимы. Их можно использовать везде, от блога WordPress или одностраничного приложения до крупного портала.

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

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

Обычно непросто придумать качественное имя для своего тега, примерно так можно отображать его содержимое на странице.

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

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

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

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

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

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

customElements.define('whatever', Whatever); // не надо так делать
customElements.define('what-ever', Whatever); // надо делать так

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

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'

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

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

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

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

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

Атрибуты пользовательских элементов (Custom Element Attributes)

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

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

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

Атрибуты HTML могут иметь значения соответствующие их предназначению (например, href, src, alt, value и т. д.) или атрибуты-переключатели true/false (например, disabled, selected, required). Для атрибута с соответствующим значением в определение класса пользовательского элемента писать так:

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 и Custom Elements могут использоваться поврозь, пользовательские элементы могут быть полезными сами по себе. В отличие от Shadow DOM, к ним можно применять полифиллы. Тем не менее, две спецификации хорошо могут работать вместе.

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

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

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

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

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

Мы можем использовать тег style, чтобы применить 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, готовый для установки и использования в ваших собственных проектах, и доступ к нему для всего мира.

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