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

https://blog.sessionstack.com/how-javascript-works-the-internals-of-shadow-dom-how-to-build-self-contained-components-244331c4de6e
  • Перевод
[Советуем почитать] Другие 19 частей цикла
Часть 1: Обзор движка, механизмов времени выполнения, стека вызовов
Часть 2: О внутреннем устройстве V8 и оптимизации кода
Часть 3: Управление памятью, четыре вида утечек памяти и борьба с ними
Часть 4: Цикл событий, асинхронность и пять способов улучшения кода с помощью async / await
Часть 5: WebSocket и HTTP/2+SSE. Что выбрать?
Часть 6: Особенности и сфера применения WebAssembly
Часть 7: Веб-воркеры и пять сценариев их использования
Часть 8: Сервис-воркеры
Часть 9: Веб push-уведомления
Часть 10: Отслеживание изменений в DOM с помощью MutationObserver
Часть 11: Движки рендеринга веб-страниц и советы по оптимизации их производительности
Часть 12: Сетевая подсистема браузеров, оптимизация её производительности и безопасности
Часть 12: Сетевая подсистема браузеров, оптимизация её производительности и безопасности
Часть 13: Анимация средствами CSS и JavaScript
Часть 14: Как работает JS: абстрактные синтаксические деревья, парсинг и его оптимизация
Часть 15: Как работает JS: классы и наследование, транспиляция в Babel и TypeScript
Часть 16: Как работает JS: системы хранения данных
Часть 17: Как работает JS: технология Shadow DOM и веб-компоненты
Часть 18: Как работает JS: WebRTC и механизмы P2P-коммуникаций
Часть 19: Как работает JS: пользовательские элементы

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



Обзор


Веб-компоненты — это семейство 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, оказывается локальным для элемента, к которому оно присоединено, в том числе — стили, описываемые с помощью тегов <style>. Именно так в рамках технологии Shadow DOM обеспечивается изоляция CSS.

Создание Shadow DOM


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>');

Надо отметить, что в спецификации Shadow DOM имеется список элементов, к которым нельзя подключать теневые поддеревья DOM.

Композиция в Shadow DOM


Композиция — это одна из важнейших возможностей Shadow DOM, это способ создания веб-приложений, который применяется в процессе написания HTML-кода. В ходе этого процесса программист комбинирует различные строительные блоки (элементы), из которых состоит страница, вкладывая их, при необходимости, друг в друга. Например, это такие элементы, как <div>, <header>, <form>, и другие, используемые для создания интерфейсов веб-приложений, в том числе, выступающие в роли контейнеров для других элементов.

Композиция определяет возможности элементов, таких, как <select>, <form>, <video>, по включению в их состав других HTML-элементов в качестве дочерних, и возможности организации особого поведения таких конструкций, состоящих из разных элементов.

Например, элемент <select> имеет средства для рендеринга элементов <option> в виде выпадающего списка с заранее заданным содержимым элементов такого списка.

Рассмотрим некоторые возможности Shadow DOM применяемые при композиции элементов.

Light 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


Дерево 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(), к теневому корню.

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

Обратите внимание на то, что в предыдущем примере мы добавили в слот элемент <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, которые позволяют пользователям компонентов переопределять стили, заданные по умолчанию.

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


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

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

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


Псевдокласс :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>)


Псевдокласс :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.

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


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

custom-container {
  color: red;
}

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

custom-container {
  width: 500px;
}

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

:host {
  width: 300px;
}

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

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


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

В основе этого подхода лежит механизм, похожий на тот, которым пользуются при работе с тегами <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 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} позволяет получить стандартное содержимое слота (выводимое в том случае, если к нему не было присоединено никаких узлов).

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

<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 (некоторым событиям такое поведение не свойственно):

  • События фокуса (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;

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


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

Итоги


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

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

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

  • +32
  • 14,4k
  • 1

RUVDS.com

701,00

RUVDS – хостинг VDS/VPS серверов

Поделиться публикацией

Похожие публикации

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

    Вы уверены, что это работает хоть где-нибудь?

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

    Самое читаемое