[Перевод] Как работает JS: технология Shadow DOM и веб-компоненты

[Советуем почитать] Предыдущие 16 частей цикла


Сегодня, в переводе 17 части материалов, посвящённых особенностям всего, что так или иначе связано с JavaScript, речь пойдёт о веб-компонентах и о различных стандартах, которые направлены на работу с ними. Особое внимание здесь будет уделено технологии Shadow DOM.

8kliz5q5jcynt6ggr2rqa-fancc.png

Обзор


Веб-компоненты — это семейство API, предназначенных для описания новых элементов DOM, подходящих для повторного использования. Функционал таких элементов отделён от остального кода, их можно применять в веб-приложениях собственной разработки.

Существует четыре технологии, относящиеся к веб-компонентам:

  • Shadow DOM (теневой DOM)
  • HTML Templates (HTML-шаблоны)
  • Custom Elements (пользовательские элементы)
  • HTML Imports (HTML-импорт)


В этом материале мы поговорим о технологии Shadow DOM, которая разработана для создания приложений, основанных на компонентах. Она предлагает способы решения распространённых проблем веб-разработки, с которыми вы, возможно, уже сталкивались:

  • Изоляция DOM: компонент обладает изолированным деревом DOM (это означает, что команда document.querySelector() не позволит обратиться к узлу в теневом DOM компонента). Кроме того, это упрощает систему CSS-селекторов в веб-приложениях, так как компоненты DOM изолированы, что даёт разработчику возможность использовать одни и те же универсальные идентификаторы и имена классов в различных компонентах, не беспокоясь о возможных конфликтах имён.
  • Изоляция CSS: CSS-правила, описанные внутри теневого DOM, ограничены им. Эти стили не покидают пределов элемента, они не смешиваются с другими стилями страницы.
  • Композиция: разработка декларативного API для компонентов, основанного на разметке.


Технология Shadow DOM


Тут предполагается, что вы уже знакомы с концепцией DOM и с соответствующими API. Если это не так — можете почитать этот материал.

Shadow DOM — это, в целом, то же самое, что и обычный DOM, но с двумя отличиями:

  • Первое заключается в том, как Shadow DOM создают и используют, в частности, речь идёт об отношении Shadow DOM к остальным частям страницы.
  • Второе заключается в поведении Shadow DOM по отношению к странице.


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

Это изолированное поддерево называют shadow tree (теневое дерево). Элемент, к которому присоединено такое дерево, называется shadow host (теневой хост-элемент). Всё, что добавляется в теневое поддерево DOM, оказывается локальным для элемента, к которому оно присоединено, в том числе — стили, описываемые с помощью тегов Launch


Шаблоны


Если вам приходится постоянно применять одни и те же структуры в HTML-разметке веб-страниц, полезно будет воспользоваться неким шаблоном вместо того, чтобы снова и снова писать один и тот же код. Подобное было возможно и раньше, но теперь всё значительно упростилось благодаря появлению HTML-тега


Теперь описанный нами пользовательский элемент можно использовать на обычных веб-страницах следующим образом:



Слоты


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

Слоты можно воспринимать как местозаполнители, которые позволяют включать в шаблон собственный HTML-код. Это позволяет создавать универсальные HTML-шаблоны, а затем делать их настраиваемыми, добавляя в них слоты.

Взглянем на то, как будет выглядеть вышеописанный шаблон с использованием тега :



Если содержимое слота не задано когда элемент включается в разметку, или если браузер не поддерживает работу со слотами, элемент будет включать в себя лишь стандартное содержимое Default text.

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

Как и ранее, тут может быть всё, что угодно. Например:


 Let's have some different text!


Элементы, которые можно помещать в слоты, называются Slotable-элементами.

Обратите внимание на то, что в предыдущем примере мы добавили в слот элемент , он является так называемым slotted-элементом. У него есть атрибут slot, которому присвоено значение my-text, то есть — то же самое значение, которое использовано в атрибуте name слота, описанного в шаблоне.

После обработки вышеописанной разметки браузером будет создано следующее дерево Flattened DOM:


  #shadow-root
  

Let's have some different text!


Обратите внимание на элемент #shadow-root. Это — всего лишь индикатор существования Shadow DOM.

Стилизация


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

▍Стили, описываемые в компонентах


Изоляция CSS — это одно из самых замечательных свойств технологии Shadow DOM. А именно, речь идёт о следующем:

  • CSS-селекторы страницы, на которой размещён соответствующий компонент, не влияют на то, что имеется у него внутри.
  • Стили, описанные внутри компонента, не оказывают воздействия на страницу. Они изолированы в хост-элементе.


CSS-селекторы, использованные внутри теневого DOM, применяются к содержимому компонента локально. На практике это означает возможность многократного использования одних и тех же идентификаторов и имён классов в разных компонентах и отсутствие необходимости беспокоиться о конфликтах имён. Простые CSS-селекторы означают и более высокую производительность решений, в которых они используются.

Взглянем на элемент #shadow-root, который определяет некоторые стили:

#shadow-root



Все вышеописанные стили являются локальными для #shadow-root.

Кроме того, для включения в #shadow-root внешних таблиц стилей можно использовать тег . Такие стили тоже будут локальными.

▍Псевдокласс : host


Псевдокласс :host позволяет обращаться к элементу, содержащему теневое дерево DOM и стилизовать этот элемент:



Пользуясь псевдоклассом :host следует помнить о том, что правила родительской страницы имеют более высокий приоритет, чем те, которые заданы в элементе с использованием этого псевдокласса. Это позволяет пользователям переопределять стили хост-компонента, заданные в нём, извне. Кроме того, псевдокласс :host работает лишь в контексте теневого корневого элемента, за пределами теневого дерева DOM пользоваться им нельзя.

Функциональная форма псевдокласса, :host(), позволяет обращаться к хост-элементу, если он соответствует заданному элементу . Это — отличный способ, позволяющий компонентам инкапсулировать поведение, которое реагирует на действия пользователя или на изменение состояния компонента, и позволяет стилизовать внутренние узлы, основываясь на хост-компоненте:



▍Темы и элементы с псевдоклассом : host-context ()


Псевдокласс :host-context() соответствует хост-элементу, если он или любые его предки соответствуют заданному элементу .

Обычный вариант использования этой возможности заключается в стилизации элементов с помощью тем. Например, часто темы применяют, назначая соответствующий класс тегам или :


  













Псевдокласс :host-context(.lightheme) будет применяться к  в том случае, если этот элемент является потомком .lightteme:

:host-context(.lightheme) {
  color: black;
  background: white;
}


Конструкция :host-context() может быть полезной для применения тем, но для этой цели лучше использовать хуки с применением пользовательских свойств CSS.

▍Стилизация хост-элемента компонента извне


Хост-элемент компонента можно стилизовать извне, используя имя его тега в качестве селектора:

custom-container {
  color: red;
}


Внешние стили имеют более высокий приоритет, чем стили, определённые в теневом DOM.
Предположим, пользователь создал следующий селектор:

custom-container {
  width: 500px;
}


Он переопределит правило, заданное в самом компоненте:

:host {
  width: 300px;
}


Используя этот подход можно стилизовать лишь сам компонент. Как стилизовать внутренние структуры компонента? Для этой цели используются пользовательские свойства CSS.

▍Создание хуков стилей с использованием пользовательских свойств CSS


Пользователи могут настраивать стили внутренних структур компонентов если автор компонента предоставляет им хуки стилей, применяя пользовательские свойства CSS.

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

Рассмотрим пример:






Вот что находится внутри теневого дерева DOM:

:host([background]) {
  background: var( - custom-container-bg, #CECECE);
  border-radius: 10px;
  padding: 10px;
}


В данном случае компонент, в качестве цвета фона, использует чёрный, так как именно его задал пользователь. В противном случае цветом фона будет #CECECE.

В роли автора компонента вы ответственны за то, чтобы сообщить его пользователям о том, какие именно пользовательские CSS-свойства они могут использовать. Считайте это частью открытого интерфейса вашего компонента.

API JavaScript для работы со слотами


API Shadow DOM предоставляет возможности работы со слотами.

▍Событие slotchange


Событие slotchange вызывается при изменении узлов, помещённых в слот. Например, если пользователь добавляет дочерние узлы в Light DOM или удаляет их из него:

var slot = this.shadowRoot.querySelector('#some_slot');
slot.addEventListener('slotchange', function(e) {
  console.log('Light DOM change');
});


Для отслеживания других типов изменений в Light DOM, можно, в конструкторе элемента, использовать MutationObserver. Подробнее об этом читайте здесь.

▍Метод assignedNodes ()


Метод assignedNodes() может оказаться полезным в том случае, если нужно узнать о том, какие элементы связаны со слотом. Вызов метода slot.assignedNodes() позволяет узнать о том, какие именно элементы выводятся средствами слота. Использование опции {flatten: true} позволяет получить стандартное содержимое слота (выводимое в том случае, если к нему не было присоединено никаких узлов).

Рассмотрим пример:

Default content


Представим, что этот слот размещён в компоненте .

Взглянем на различные варианты использования этого компонента, и на то, что будет выдано при вызове метода assignedNodes().

В первом случае мы добавляем в слот собственное содержимое:


   container text 


В данном случае вызов assignedNodes() вернёт [ container text ]. Обратите внимание на то, что это значение является массивом узлов.

Во втором случае мы не заполняем слот собственным содержимым:

 


Вызов assignedNodes() вернёт пустой массив — [].

Если, однако, передать этому методу параметр {flatten: true}, то его вызов для того же самого элемента выдаст его содержимое, выводимое по умолчанию: [

Default content

].

Кроме того, для того, чтобы получить доступ к элементу внутри слота, вы можете вызвать assignedNodes(), что позволить узнать о том, какому из слотов компонента назначен ваш элемент.

Модель событий


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

Вот список событий, которые передаются из теневого дерева DOM (некоторым событиям такое поведение не свойственно):

  • События фокуса (Focus Events): blur, focus, focusin, focusout.
  • События мыши (Mouse Event)s: click, dblclick, mousedown, mouseenter, mousemove и другие.
  • События колеса мыши (Wheel Events): wheel.
  • События ввода (Input Events): beforeinput, input.
  • События клавиатуры (Keyboard Events): keydown, keyup.
  • События композиции (Composition Events): compositionstart, compositionupdate, compositionend.
  • События перетаскивания (Drag Events): dragstart, drag, dragend, drop, и так далее.


Пользовательские события


Пользовательские события по умолчанию не покидают пределов теневого дерева DOM. Если вы хотите вызвать событие, и требуется, чтобы оно покинуло пределы Shadow DOM, нужно снабдить его параметрами bubbles: true и composed: true. Вот как выглядит вызов подобного события:

var container = this.shadowRoot.querySelector('#container');
container.dispatchEvent(new Event('containerchanged', {bubbles: true, composed: true}));


Поддержка Shadow DOM браузерами


Для того чтобы узнать, поддерживает ли браузер технологию Shadow DOM, можно проверить наличие attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;


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

bead78c47c464b5059576df077613803.png


Поддержка технологии Shadow DOM в браузерах

Итоги


Теневое дерево DOM ведёт себя не так, как обычное дерево DOM. В частности, по словам автора данного материала, в библиотеке SessionStack это выражается в усложнении процедуры отслеживания изменений DOM, сведения о которых нужны для воспроизведения того, что происходило со страницей. А именно, для отслеживания изменений используется MutationObserver. При этом теневое дерево DOM не вызывает события MutationObserver в глобальной области видимости, что приводит к необходимости использования особых подходов для работы с компонентами, использующими Shadow DOM.

Практика показывает, что всё больше современных веб-приложений используют Shadow DOM, что позволяет говорить о том, что эту технологию, вероятно, ждёт дальнейшее развитие и распространение.

Уважаемые читатели! Пользуетесь ли вы веб-компонентами, построенными на основе технологии Shadow DOM?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru