- PVSM.RU - https://www.pvsm.ru -
Сегодня, в переводе 17 части материалов, посвящённых особенностям всего, что так или иначе связано с JavaScript, речь пойдёт о веб-компонентах и о различных стандартах, которые направлены на работу с ними. Особое внимание здесь будет уделено технологии Shadow DOM.
Веб-компоненты — это семейство API, предназначенных для описания новых элементов DOM, подходящих для повторного использования. Функционал таких элементов отделён от остального кода, их можно применять в веб-приложениях собственной разработки.
Существует четыре технологии, относящиеся к веб-компонентам:
В этом материале мы поговорим о технологии Shadow DOM, которая разработана для создания приложений, основанных на компонентах. Она предлагает способы решения распространённых проблем веб-разработки, с которыми вы, возможно, уже сталкивались:
document.querySelector() не позволит обратиться к узлу в теневом DOM компонента). Кроме того, это упрощает систему CSS-селекторов в веб-приложениях, так как компоненты DOM изолированы, что даёт разработчику возможность использовать одни и те же универсальные идентификаторы и имена классов в различных компонентах, не беспокоясь о возможных конфликтах имён.Тут предполагается, что вы уже знакомы с концепцией DOM и с соответствующими API. Если это не так — можете почитать этот [18] материал.
Shadow DOM — это, в целом, то же самое, что и обычный DOM, но с двумя отличиями:
При работе с DOM создаются узлы DOM, которые присоединяются, в качестве дочерних элементов, к другим элементам страницы. В случае с технологией Shadow DOM создают изолированное дерево DOM, которое присоединяется к элементу, но оно отделено от его обычных дочерних элементов.
Это изолированное поддерево называют shadow tree (теневое дерево). Элемент, к которому присоединено такое дерево, называется shadow host (теневой хост-элемент). Всё, что добавляется в теневое поддерево DOM, оказывается локальным для элемента, к которому оно присоединено, в том числе — стили, описываемые с помощью тегов <style>. Именно так в рамках технологии Shadow DOM обеспечивается изоляция CSS.
Shadow root (теневой корневой элемент) — это фрагмент документа, который присоединяется к хост-элементу. Элемент обзаводится теневым DOM тогда, когда к нему присоединяют теневой корневой элемент. Для того, чтобы создать для некоего элемента теневой DOM, нужно воспользоваться командой вида element.attachShadow():
var header = document.createElement('header');
var shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.appendChild(document.createElement('<p> Shadow DOM </p>');
Надо отметить, что в спецификации [19] Shadow DOM имеется список элементов, к которым нельзя подключать теневые поддеревья DOM.
Композиция — это одна из важнейших возможностей Shadow DOM, это способ создания веб-приложений, который применяется в процессе написания HTML-кода. В ходе этого процесса программист комбинирует различные строительные блоки (элементы), из которых состоит страница, вкладывая их, при необходимости, друг в друга. Например, это такие элементы, как <div>, <header>, <form>, и другие, используемые для создания интерфейсов веб-приложений, в том числе, выступающие в роли контейнеров для других элементов.
Композиция определяет возможности элементов, таких, как <select>, <form>, <video>, по включению в их состав других HTML-элементов в качестве дочерних, и возможности организации особого поведения таких конструкций, состоящих из разных элементов.
Например, элемент <select> имеет средства для рендеринга элементов <option> в виде выпадающего списка с заранее заданным содержимым элементов такого списка.
Рассмотрим некоторые возможности Shadow DOM применяемые при композиции элементов.
Light DOM — это разметка, создаваемая пользователем вашего компонента. Этот DOM находится за пределами теневого DOM компонента и представляет собой дочерний элемент компонента. Представьте себе, что вы создали пользовательский компонент, называемый <better-button>, который расширяет возможности стандартного HTML-элемента <button>, и пользователю нужно добавить в этот новый элемент изображение и какой-то текст. Вот как это выглядит:
<extended-button>
<!-- теги img и span - это Light DOM элемента extended-button -->
<img align="center" src="boot.png" slot="image">
<span>Launch</span>
</extended-button>
Элемент <extended-button> — это пользовательский компонент, описанный программистом самостоятельно, а HTML-код внутри этого компонента — это его Light DOM — то, что добавил в него пользователь этого компонента.
Теневой DOM в этом примере — это компонент <extended-button>. Это — локальная объектная модель компонента, которая описывает его внутреннюю структуру, изолированный от внешнего мира CSS, и инкапсулирует детали реализации компонента.
Дерево Flattened DOM представляет собой то, как браузер выводит компонент на экран, объединяя Light DOM и Shadow DOM. Именно такое дерево DOM можно видеть в инструментах разработчика, и именно оно выводится на страницу. Выглядеть это может примерно так:
<extended-button>
#shadow-root
<style>…</style>
<slot name="image">
<img align="center" src="boot.png" slot="image">
</slot>
<span id="container">
<slot>
<span>Launch</span>
</slot>
</span>
</extended-button>
Если вам приходится постоянно применять одни и те же структуры в HTML-разметке веб-страниц, полезно будет воспользоваться неким шаблоном вместо того, чтобы снова и снова писать один и тот же код. Подобное было возможно и раньше, но теперь всё значительно упростилось благодаря появлению HTML-тега <template>, который пользуется отличной поддержкой современных браузеров. Этот элемент и его содержимое не выводится в DOM, но с ним можно работать из JavaScript. Рассмотрим простой пример:
<template id="my-paragraph">
<p> Paragraph content. </p>
</template>
Если включить такую конструкцию в состав HTML-разметки страницы, содержимое описываемого ей тега <p> не появится на экране до тех пор, пока не будет явным образом присоединено к DOM документа. Например, это может выглядеть так:
var template = document.getElementById('my-paragraph');
var templateContent = template.content;
document.body.appendChild(templateContent);
Существуют и другие средства, позволяющие достичь того же эффекта, но, как уже было сказано, шаблоны — очень удобный стандартный инструмент, пользующийся хорошей поддержкой браузеров.

Поддержка HTML-шаблонов современными браузерами
Шаблоны полезны и сами по себе, но в полной мере их возможности раскрываются при использовании с пользовательскими элементами. Пользовательские элементы — это тема для отдельного материала, а сейчас, для понимания происходящего, достаточно учитывать то, что API браузеров customElement позволяет программисту описывать собственные HTML-теги и задавать то, как элементы, создаваемые с помощью этих тегов, будут выглядеть на экране.
Определим веб-компонент, который использует наш шаблон в качестве содержимого для своего теневого DOM. Назовём этот новый элемент <my-paragraph>:
customElements.define('my-paragraph',
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById('my-paragraph');
let templateContent = template.content;
const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));
}
});
Самое важное, на что тут надо обратить внимание — это то, что мы присоединили клон содержимого шаблона, сделанный с помощью метода Node.cloneNode() [20], к теневому корню.
Так как мы присоединяем содержимое шаблона к теневому DOM, мы можем включить в шаблон некую информацию о стилизации, в элементе <style> [21], которая затем будет инкапсулирована в пользовательский элемент. Вся эта схема не будет работать так, как ожидается, если вместо Shadow DOM работать с обычным DOM.
Например, шаблон можно доработать следующим образом, включив в него сведения о стилях:
<template id="my-paragraph">
<style>
p {
color: white;
background-color: #666;
padding: 5px;
}
</style>
<p>Paragraph content. </p>
</template>
Теперь описанный нами пользовательский элемент можно использовать на обычных веб-страницах следующим образом:
<my-paragraph></my-paragraph>
У HTML-шаблонов есть несколько недостатков, главный из них заключается в том, что шаблоны содержат статическую разметку, что не позволяет, например, выводить с их помощью содержимое неких переменных для того, чтобы работать с ними так же, как работают со стандартными HTML-шаблонами. Здесь в дело вступает тег <slot>.
Слоты можно воспринимать как местозаполнители, которые позволяют включать в шаблон собственный HTML-код. Это позволяет создавать универсальные HTML-шаблоны, а затем делать их настраиваемыми, добавляя в них слоты.
Взглянем на то, как будет выглядеть вышеописанный шаблон с использованием тега <slot>:
<template id="my-paragraph">
<p>
<slot name="my-text">Default text</slot>
</p>
</template>
Если содержимое слота не задано когда элемент включается в разметку, или если браузер не поддерживает работу со слотами, элемент <my-paragraph> будет включать в себя лишь стандартное содержимое Default text.
Для того чтобы задать содержимое слота, нужно включить в элемент <my-paragraph> HTML-код с атрибутом slot, значение которого эквивалентно имени слота, в который нужно поместить этот код.
Как и ранее, тут может быть всё, что угодно. Например:
<my-paragraph>
<span slot="my-text">Let's have some different text!</span>
</my-paragraph>
Элементы, которые можно помещать в слоты, называются Slotable [22]-элементами.
Обратите внимание на то, что в предыдущем примере мы добавили в слот элемент <span>, он является так называемым slotted-элементом. У него есть атрибут slot, которому присвоено значение my-text, то есть — то же самое значение, которое использовано в атрибуте name слота, описанного в шаблоне.
После обработки вышеописанной разметки браузером будет создано следующее дерево Flattened DOM:
<my-paragraph>
#shadow-root
<p>
<slot name="my-text">
<span slot="my-text">Let's have some different text!</span>
</slot>
</p>
</my-paragraph>
Обратите внимание на элемент #shadow-root. Это — всего лишь индикатор существования Shadow DOM.
Компоненты, которые используют технологию Shadow DOM, можно стилизовать на общих основаниях, они могут определять собственные стили, или предоставлять хуки в форме пользовательских свойств CSS [23], которые позволяют пользователям компонентов переопределять стили, заданные по умолчанию.
Изоляция CSS — это одно из самых замечательных свойств технологии Shadow DOM. А именно, речь идёт о следующем:
CSS-селекторы, использованные внутри теневого DOM, применяются к содержимому компонента локально. На практике это означает возможность многократного использования одних и тех же идентификаторов и имён классов в разных компонентах и отсутствие необходимости беспокоиться о конфликтах имён. Простые CSS-селекторы означают и более высокую производительность решений, в которых они используются.
Взглянем на элемент #shadow-root, который определяет некоторые стили:
#shadow-root
<style>
#container {
background: white;
}
#container-items {
display: inline-flex;
}
</style>
<div id="container"></div>
<div id="container-items"></div>
Все вышеописанные стили являются локальными для #shadow-root.
Кроме того, для включения в #shadow-root внешних таблиц стилей можно использовать тег <link>. Такие стили тоже будут локальными.
Псевдокласс :host позволяет обращаться к элементу, содержащему теневое дерево DOM и стилизовать этот элемент:
<style>
:host {
display: block; /* по умолчанию у пользовательских элементов это display: inline */
}
</style>
Пользуясь псевдоклассом :host следует помнить о том, что правила родительской страницы имеют более высокий приоритет, чем те, которые заданы в элементе с использованием этого псевдокласса. Это позволяет пользователям переопределять стили хост-компонента, заданные в нём, извне. Кроме того, псевдокласс :host работает лишь в контексте теневого корневого элемента, за пределами теневого дерева DOM пользоваться им нельзя.
Функциональная форма псевдокласса, :host(<selector>), позволяет обращаться к хост-элементу, если он соответствует заданному элементу <selector>. Это — отличный способ, позволяющий компонентам инкапсулировать поведение, которое реагирует на действия пользователя или на изменение состояния компонента, и позволяет стилизовать внутренние узлы, основываясь на хост-компоненте:
<style>
:host {
opacity: 0.4;
}
:host(:hover) {
opacity: 1;
}
:host([disabled]) { /* стилизация при условии наличия у хост-элемента атрибута disabled. */
background: grey;
pointer-events: none;
opacity: 0.4;
}
:host(.pink) > #tabs {
color: pink; /* задаёт цвет внутреннего узла #tabs если у хост-элемента есть class="pink". */
}
</style>
Псевдокласс :host-context(<selector>) соответствует хост-элементу, если он или любые его предки соответствуют заданному элементу <selector>.
Обычный вариант использования этой возможности заключается в стилизации элементов с помощью тем. Например, часто темы применяют, назначая соответствующий класс тегам <html> или <body>:
<body class="lightheme">
<custom-container>
…
</custom-container>
</body>
Псевдокласс :host-context(.lightheme) будет применяться к <fancy-tabs> в том случае, если этот элемент является потомком .lightteme:
:host-context(.lightheme) {
color: black;
background: white;
}
Конструкция :host-context() может быть полезной для применения тем, но для этой цели лучше использовать хуки с применением пользовательских свойств CSS [24].
Хост-элемент компонента можно стилизовать извне, используя имя его тега в качестве селектора:
custom-container {
color: red;
}
Внешние стили имеют более высокий приоритет, чем стили, определённые в теневом DOM.
Предположим, пользователь создал следующий селектор:
custom-container {
width: 500px;
}
Он переопределит правило, заданное в самом компоненте:
:host {
width: 300px;
}
Используя этот подход можно стилизовать лишь сам компонент. Как стилизовать внутренние структуры компонента? Для этой цели используются пользовательские свойства CSS.
Пользователи могут настраивать стили внутренних структур компонентов если автор компонента предоставляет им хуки стилей, применяя пользовательские свойства CSS [23].
В основе этого подхода лежит механизм, похожий на тот, которым пользуются при работе с тегами <slot>, но он, в данном случае, применяется к стилям.
Рассмотрим пример:
<!-- main page -->
<style>
custom-container {
margin-bottom: 60px;
- custom-container-bg: black;
}
</style>
<custom-container background>…</custom-container>
Вот что находится внутри теневого дерева DOM:
:host([background]) {
background: var( - custom-container-bg, #CECECE);
border-radius: 10px;
padding: 10px;
}
В данном случае компонент, в качестве цвета фона, использует чёрный, так как именно его задал пользователь. В противном случае цветом фона будет #CECECE.
В роли автора компонента вы ответственны за то, чтобы сообщить его пользователям о том, какие именно пользовательские CSS-свойства они могут использовать. Считайте это частью открытого интерфейса вашего компонента.
API Shadow DOM предоставляет возможности работы со слотами.
Событие slotchange вызывается при изменении узлов, помещённых в слот. Например, если пользователь добавляет дочерние узлы в Light DOM или удаляет их из него:
var slot = this.shadowRoot.querySelector('#some_slot');
slot.addEventListener('slotchange', function(e) {
console.log('Light DOM change');
});
Для отслеживания других типов изменений в Light DOM, можно, в конструкторе элемента, использовать MutationObserver. Подробнее об этом читайте здесь [25].
Метод assignedNodes() может оказаться полезным в том случае, если нужно узнать о том, какие элементы связаны со слотом. Вызов метода slot.assignedNodes() позволяет узнать о том, какие именно элементы выводятся средствами слота. Использование опции {flatten: true} позволяет получить стандартное содержимое слота (выводимое в том случае, если к нему не было присоединено никаких узлов).
Рассмотрим пример:
<slot name=’slot1’><p>Default content</p></slot>
Представим, что этот слот размещён в компоненте <my-container>.
Взглянем на различные варианты использования этого компонента, и на то, что будет выдано при вызове метода assignedNodes().
В первом случае мы добавляем в слот собственное содержимое:
<my-container>
<span slot="slot1"> container text </span>
</my-container>
В данном случае вызов assignedNodes() вернёт [ container text ]. Обратите внимание на то, что это значение является массивом узлов.
Во втором случае мы не заполняем слот собственным содержимым:
<my-container> </my-container>
Вызов assignedNodes() вернёт пустой массив — [].
Если, однако, передать этому методу параметр {flatten: true}, то его вызов для того же самого элемента выдаст его содержимое, выводимое по умолчанию: [
Default content
].
Кроме того, для того, чтобы получить доступ к элементу внутри слота, вы можете вызвать assignedNodes(), что позволить узнать о том, какому из слотов компонента назначен ваш элемент.
Поговорим о том, что происходит при всплытии события, возникшего в теневом дереве DOM. Цель события задаётся с учётом инкапсуляции, поддерживаемой технологией Shadow DOM. Когда событие перенаправляется, это выглядит так, как будто оно исходит от самого компонента, а не от его внутреннего элемента, который находится в теневом дереве DOM и является частью этого компонента.
Вот список событий, которые передаются из теневого дерева DOM (некоторым событиям такое поведение не свойственно):
blur, focus, focusin, focusout.click, dblclick, mousedown, mouseenter, mousemove и другие.wheel.beforeinput, input.keydown, keyup.compositionstart, compositionupdate, compositionend.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, можно проверить наличие attachShadow:
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
Вот сведения о поддержке этой технологии различными браузерами.

Поддержка технологии Shadow DOM в браузерах
Теневое дерево DOM ведёт себя не так, как обычное дерево DOM. В частности, по словам автора данного материала, в библиотеке SessionStack [26] это выражается в усложнении процедуры отслеживания изменений DOM, сведения о которых нужны для воспроизведения того, что происходило со страницей. А именно, для отслеживания изменений используется MutationObserver. При этом теневое дерево DOM не вызывает события MutationObserver в глобальной области видимости, что приводит к необходимости использования особых подходов для работы с компонентами, использующими Shadow DOM.
Практика показывает, что всё больше современных веб-приложений используют Shadow DOM, что позволяет говорить о том, что эту технологию, вероятно, ждёт дальнейшее развитие и распространение.
Уважаемые читатели! Пользуетесь ли вы веб-компонентами, построенными на основе технологии Shadow DOM?
Автор: ru_vds
Источник [28]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/284735
Ссылки в тексте:
[1] Обзор движка, механизмов времени выполнения, стека вызовов: https://habrahabr.ru/company/ruvds/blog/337042/
[2] О внутреннем устройстве V8 и оптимизации кода: https://habrahabr.ru/company/ruvds/blog/337460/
[3] Управление памятью, четыре вида утечек памяти и борьба с ними: https://habrahabr.ru/company/ruvds/blog/338150/
[4] Цикл событий, асинхронность и пять способов улучшения кода с помощью async / await: https://habrahabr.ru/company/ruvds/blog/340508/
[5] WebSocket и HTTP/2+SSE. Что выбрать?: https://habrahabr.ru/company/ruvds/blog/342346/
[6] Особенности и сфера применения WebAssembly: https://habrahabr.ru/company/ruvds/blog/343568/
[7] Веб-воркеры и пять сценариев их использования: https://habrahabr.ru/company/ruvds/blog/348424/
[8] Сервис-воркеры: https://habrahabr.ru/company/ruvds/blog/349858/
[9] Веб push-уведомления: https://habrahabr.ru/company/ruvds/blog/350486/
[10] Отслеживание изменений в DOM с помощью MutationObserver: https://habrahabr.ru/company/ruvds/blog/351256/
[11] Движки рендеринга веб-страниц и советы по оптимизации их производительности: https://habrahabr.ru/company/ruvds/blog/351802/
[12] Сетевая подсистема браузеров, оптимизация её производительности и безопасности: https://habr.com/company/ruvds/blog/354070/
[13] Анимация средствами CSS и JavaScript: https://habr.com/company/ruvds/blog/354438/
[14] Как работает JS: абстрактные синтаксические деревья, парсинг и его оптимизация: https://habr.com/company/ruvds/blog/415269/
[15] Как работает JS: классы и наследование, транспиляция в Babel и TypeScript: https://habr.com/company/ruvds/blog/415377/
[16] Как работает JS: системы хранения данных: https://habr.com/company/ruvds/blog/415505/
[17] Image: https://habr.com/company/ruvds/blog/415881/
[18] этот: https://developer.mozilla.org/ru/docs/DOM/DOM_Reference/%D0%92%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5
[19] спецификации: http://w3c.github.io/webcomponents/spec/shadow/#h-methods
[20] Node.cloneNode(): https://developer.mozilla.org/en-US/docs/Web/API/Node/cloneNode
[21] <style>: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style
[22] Slotable: https://developer.mozilla.org/en-US/docs/Web/API/Slotable
[23] пользовательских свойств CSS: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables
[24] пользовательских свойств CSS: https://developers.google.com/web/fundamentals/web-components/shadowdom#stylehooks
[25] здесь: https://habr.com/company/ruvds/blog/351256/
[26] SessionStack: https://www.sessionstack.com/
[27] Image: https://ruvds.com/ru-rub/#order
[28] Источник: https://habr.com/post/415881/?utm_campaign=415881
Нажмите здесь для печати.