Что такое shadow dom
Shadow DOM
Теневой DOM («Shadow DOM») используется для инкапсуляции. Благодаря ему в компоненте есть собственное «теневое» DOM-дерево, к которому нельзя просто так обратиться из главного документа, у него могут быть изолированные CSS-правила и т.д.
Встроенный теневой DOM
Задумывались ли вы о том, как устроены и стилизованы сложные браузерные элементы управления?
Браузер рисует их своими силами и по своему усмотрению. Их DOM-структура обычно нам не видна, но в инструментах разработчика можно её посмотреть. К примеру, в Chrome для этого нужно активировать пункт «Show user agent shadow DOM».
После этого выглядит так:
То, что находится под #shadow-root – и называется «shadow DOM» (теневой DOM).
Мы не можем получить доступ к теневому DOM встроенных элементов с помощью обычных JavaScript-вызовов или с помощью селекторов. Это не просто обычные потомки, это мощное средство инкапсуляции.
Ещё раз заметим, что pseudo – нестандартный атрибут. Если говорить хронологически, то сначала браузеры начали экспериментировать с инкапсуляцией внутренних DOM-структур для элементов, а уже потом, через некоторое время, появился стандарт Shadow DOM, который позволяет делать то же самое нам, разработчикам.
Далее мы воспользуемся современным стандартом Shadow DOM, описанным в спецификации DOM spec и других спецификациях.
Теневое дерево
Каждый DOM-элемент может иметь 2 типа поддеревьев DOM:
Если у элемента имеются оба поддерева, браузер отрисовывает только теневое дерево. Также мы всё же можем задать «композицию» теневого и обычного деревьев. Позже в главе Слоты теневого DOM, композиция мы рассмотрим детали.
Теневое дерево можно использовать в пользовательских элементах (Custom Elements), чтобы спрятать внутренности компонента и применить к ним локальные стили.
Например, этот элемент прячет свой внутренний DOM в теневом дереве:
А вот как получившийся DOM выглядит в инструментах разработчика в Chrome, весь контент внутри «#shadow-root»:
Итак, вызов elem.attachShadow(
Есть два ограничения:
Свойство mode задаёт уровень инкапсуляции. У него может быть только два значения:
Элемент с корнем теневого дерева называется – «хозяин» (host) теневого дерева, и он доступен в качестве свойства host у shadow root:
Инкапсуляция
Теневой DOM отделён от главного документа:
Ссылки
Итого
Теневой DOM – это способ создать свой, изолированный, DOM для компонента.
Элементы теневого DOM:
Теневой DOM, если имеется, отрисовывается браузером вместо обычных потомков (light DOM). В главе Слоты теневого DOM, композиция мы разберём, как делать их композицию.
Теневой DOM и события
Смысл создания теневого DOM-дерева – это инкапсуляция внутренних деталей компонента.
Поэтому, чтобы не нарушать инкапсуляцию, браузер меняет у этого события целевой элемент.
Рассмотрим простой пример:
Если нажать на кнопку, то выведется следующее:
Подмена целевого элемента не происходит, если событие берёт начало на элементе из слота, который фактически находится в обычном, светлом DOM.
Например, если пользователь кликнет на в примере ниже – целевой элемент события будет именно этот span для обоих обработчиков – теневого и обычного (светлого):
Всплытие и метод event.composedPath()
Для обеспечения всплытия событий используется развёрнутый DOM.
Таким образом, если у нас есть элемент в слоте, и событие происходит где-то внутри него, то оно всплывает до и выше.
В примере выше развёрнутое DOM-дерево будет таким:
Если теневое DOM-дерево было создано с
Этот метод следует тем же принципам, что и остальные. Внутреннее устройство закрытых DOM-деревьев совершенно скрыто.
Свойство: event.composed
Большинство событий успешно всплывают сквозь границу теневого DOM. Но не все.
Если посмотреть в спецификацию UI Events, то большинство событий имеют composed: true :
Хотя есть и события, имеющие composed: false :
Эти события могут быть пойманы только на элементах того же DOM, в котором находится целевой элемент события.
Генерация событий
Например, здесь мы создаём элемент div#inner в теневом DOM-дереве элемента div#outer и генерируем на нём два события. Только одно с флагом composed: true выйдет наружу, в документ:
Итого
У некоторых встроенных событий всё же стоит composed: false :
Эти события могут быть пойманы только на элементах, принадлежащих тому же DOM-дереву.
Теневой DOM (Shady DOM)
На Google I/O нам был представлен Polymer 1.0. Это новый релиз инструмента, который включает ряд особенностей и нововведении. Пожалуй начать стоит именно с Shady DOM.
Зачем нам еще один DOM?
Инкапсуляция является основой веб-компонентов.
Целью веб-компонентов является предоставление пользователю простого интерфейса для отображения сложных элементов, реализация которых скрыта.
Shadow DOM нацелен на решение данной проблемы. Браузеры которые поддерживают shadow DOM могут отображать сложные элементы скрывая реализацию (DOM, CSS, JS).
И, допустим, мы реализовали плагин для него:
Автор будет очень доволен, так как он добьется необходимого ему поведения.
По факту, это все, что нам надо от веб-компонентов — простая разметка для достижения необходимого поведения. Но подход основанный на плагине имеет ряд недостатков, который решает shadow DOM.
Загрязнение DOM
Tree Scoping
Инкапсуляция Shadow DOM
Если мы решим взглянуть на полную картину того, что же мы получили, то мы увидим следующее:
Shadow DOM так крут, так зачем нам еще один shady DOM?!
Сделать полифил для такого поведения ОЧЕНЬ сложно. Нам необходимо добиться такого же композиционного отображения DOM дерева, при этом скрыть его от логического кода.
Это означает, что нам необходимо модифицировать все доступные методы по работе с элементами, что бы возвращать кастомную информацию.
Мы реализовали такой полифил, но цена:
Shady DOM
А-ля франкенштейн, который Гугл всячески пытается похвалить. Жаль, но другого выхода нет
Грубо говоря Shady DOM, предоставляет нам совместимую с shadow DOM модель области видимости дерева. Результат работы мы получим абсолютно точно такой же DOM как и при работе с jQuery плагином.
А другими словами господа, все те самые недостатки которые мы якобы побороли — открытая реализация, проблемы со стилями и остальные.
Все, что смог от части сохранить Гугл, так это то, как представлено дерево в коде. Но для этого нам ОБЯЗАТЕЛЬНО, необходимо использовать новое API по работе с DOM и только тогда мы будем работать с элементами будто нечего и не произошло и видеть его так:
На самом деле в пределах элемента, это выглядит вполне себе достойно:
Таким образом мы можем работать как и с внутренним DOM так и со светлым DOM.
Из полимера не была вытиснута полностью модель shadow DOM’а. Совместимость shady с shadow позволяет нам писать в одном стиле. Если хотите, вы можете сделать так, что бы полимер решал, где он может использовать shadow DOM нативно, а где включать в работу shady.
Выводы
На самом деле я очень доволен самим полимером. Как было сказано на конференции, компоненты реакта работают только в реакте, компоненты ангуляра только с ангуляром, а компоненты, написанные с использование полимера — работают везде. Они занимают уровень между веб-платформой и фреймворками. Вы можете использовать их с любым фреймворком или же написать приложение используя только компоненты.
У меня был опыт скрещивания Backbone с React компонентами, но это не так круто, как может показаться. А вот компоненты полимера + Backbone прям конфетка.
Про Shadow DOM
Какие основные вопросы решает Shadow DOM?
Инкапсуляция. Внутри Shadow DOM создается отдельный «поддокумент», к которому можно применять свои стили, экранированные от воздействий внешней среды (вам не нужно писать многоэтажные имена классов, чтобы обезопасить ваш элемент или внешний документ от «протечек») и где создается свой контекст для методов DOM API, где, к примеру, с помощью селекторов можно получить только те элементы которые находятся внутри и остаются почти невидимыми снаружи (при этом, допустимо использование одинаковых ID у элементов в разных контекстах, без опасности все поломать).
Теперь у нашего div есть реакция на наведение мыши при том, что мы не создавали для этого никаких классов и не вносили никаких изменений во внешние стили. Shadow DOM дает нам доступ к своему элементу-контейнеру через селектор :host, и используя этот селектор, мы можем создавать любые сложные стили для элемента в JS. Прошу принять во внимание, что код приведенный выше, написан исключительно для демонстрации самого принципа, в бою все может выглядеть немного иначе.
Когда стоит применять Shadow DOM?
Думаю, основными областями применения можно считать те-же, что и для веб-компонентов в целом: для создания встраиваемых виджетов и UI-библиотек-агностиков, минимально зависимых от конкретных мета-платформ, экосистем и фреймворков. Везде, где важна инкапсуляция, возможность спрятать «под ковер» реализацию и удобная независимая композиция.
Модные микрофронтенды также являются интересной областью для применения возможностей Shadow DOM.
Также, можно рассмотреть вариант применения в случаях, когда вам необходимо внедрить свое локальное решение в большой и неповоротливый проект с кучей легаси-кода, в котором можно закопаться неоправданно надолго и есть опасность сломать что-то неочевидное: изоляция участка может оказаться быстрым и эффективным решением.
Вялотекущий рефакторинг сложной системы тоже можно проводить через создание «островков безопасности».
Какие могут возникнуть сложности?
Следует понимать, что Shadow DOM в отдельности, НЕ решает вопрос контроля жизненного цикла ваших компонентов и инициализации компонентов во внешней среде (помните, для этого есть Custom Elements).
Shadow DOM в документе может быть создан только через JavaScript, а потому, вы не сможете напрямую использовать предварительный рендер (SSR) для внутренней разметки. Данное ограничение можно обойти, но это отдельный непростой разговор.
На создание Shadow DOM, при прочих равных условиях, уходят дополнительные ресурсы, поэтому, если для вас важен вопрос производительности, старайтесь не использовать теневые участки DOM бездумно повсюду. Часто для создания виджета достаточно всего одного общего теневого DOM, без лишней вложенности и сопутствующих этому дополнительных расходов.
Вывод
Shadow DOM
Итак, что же такое shadow DOM:
Shadow DOM (или теневая модель документа) — часть документа, реализующая инкапсуляцию в DOM дереве. Она (теневая модель) является частью документа и встраивается непосредственно внутрь страницы.
Для упрощения отладки shadow DOM, в хроме можно включить отображение в веб-инспекторе (Settings — General — Show shadow DOM).
Надо заметить, что в стандарте реализуемая инкапсуляция называется функциональной, поскольку shadow DOM встраивается в документ и является одной из многих его частей, работающих «независимо» (более-менее независимо) друг от друга. Соответственно, при проектировании реализации, нужно было установить функциональные границы в дереве документа, чтобы как-то оперировать с множеством таких «независимых» фрагментов. Для решения проблемы инкапсуляции, и была введена новая абстракция — shadow DOM, позволяющая создавать несколько DOM деревьев в пределах одного родительского дерева и был разработан документ, описывающий ее.
Дочернее дерево размещается внутри некоторого элемента на странице. Функциональные границы между главным деревом документа и теневым называются shadow boundaries (теневые границы). Элемент, который размещает в себе теневое дерево, называется shadow host, а корень теневого дерево, соответственно, называется shadow root.
Во время рендеринга shadow tree занимает место содержимого shadow host (элемента).
Пример реализации в chromium:
Insertion points
Для композиции потомков shadow host и shadow tree используются insertion points. Insertion points определяют местонахождение потомков shadow host в shadow tree. При рендеринге shadow tree потомки проецируются в это место. Механизм, определяющий какие потомки shadow host будут спроецированы в insertion point называется distribution.
Псевдо-элемент ::distributed()
::distributed(selector) — функциональный псевдо-элемент принимающий относительный селектор в качестве аргумента. Он представляет отношение между insertion point в shadow tree и элементом, перенесенным в insertion point.
Реализация (chrome canary only):
Один shadow host может вмещать в себя несколько shadow tree — они будут отображены в порядке их добавления. Такой набор деревьев называется shadow stack. Более «старый» shadow tree так же можно переносить в другой shadow tree посредством shadow insertion point.
Reprojection (перепроецирование)
Перепроецирование это ситуация, при которой первое shadow tree уже имеет insertion point, а второй shadow tree имеет shadow insetion point, при этом контент, взятый из shadow host сначала проецируется в первом shadow tree, а затем во втором.
Псевдо-элементы (в контексте shadow DOM)
In certain situations, the author of a shadow tree may wish to designate one or more elements from that tree as a structural abstraction that provides additional information about the contents of the shadow tree.
В определенных ситуациях, автору shadow tree захочется назначить один или несколько элементов из shadow tree как стукртурную абстракцию, дающую дополнительную информацию о контенте shadow tree.
Что я понимаю как возможность использовать css селекторы вне shadow tree для доступа к элементам внутри него:
События
Некоторые события пропускаются через shadow boundary, некоторые нет. Исключение составляют mutation events — они вообще не должны возникать в shadow tree и, соответственно, переходить через shadow boundary. При прохождении события через shadow boundary у него меняется event.target для поддержания инкапсуляции.
Вот интересный пример:
События спроецированного элемента всплывают в shadow host, как-будто он все еще находится непосредственно внутри shadow host. События first-inner-element не всплывают в shadow host, в отличие от second-inner-element, который абсолютно спозиционирован и вынесен за пределы shadow host (при этом event.target сменился).
Стили
Есть два метода, позволяющие манипулировать стилями shadow tree:
shadowRoot.resetStyleInheritance (false by default)
Сбрасывает наследование стилей для shadow tree (стили снаружи не применяются на shadow tree).
shadowRoot.applyAuthorStyles (false by default)
Применяет стили авторского (главного) документа.
Можно сказать, что некоторой «инкапсуляции» для html не хватало. Это открывает большие возможности по созданию и шаблонизации различных, заранее подготовленных, виджетов на странице. Удивляет только отсутствие инкапсуляции JavaScript кода внутри виджетов, хотя мне казалось бы это довольно логичным.