Shadow DOM

Ссылка на стандарт: www.w3.org/TR/2013/WD-shadow-dom-20130514

Итак, что же такое 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:

<div id="shadow-host"></div>


var shadowHost = document.querySelector("#shadow-host"),
	shadowRoot = shadowHost.webkitCreateShadowRoot();




Insertion points

Для композиции потомков shadow host и shadow tree используются insertion points. Insertion points определяют местонахождение потомков shadow host в shadow tree. При рендеринге shadow tree потомки проецируются в это место. Механизм, определяющий какие потомки shadow host будут спроецированы в insertion point называется distribution.

Реализация:

<div id="shadow-host">
	<span>Hi shadow DOM!</span>
</div>


var shadowHost = document.querySelector("#shadow-host"),
	shadowRoot = shadowHost.webkitCreateShadowRoot(),
	content = document.createElement("content");
content.select = "span"; // выбираем все спаны из shadow host
shadowRoot.appendChild(content);




Псевдо-элемент ::distributed()

::distributed(selector) — функциональный псевдо-элемент принимающий относительный селектор в качестве аргумента. Он представляет отношение между insertion point в shadow tree и элементом, перенесенным в insertion point.

Реализация (chrome canary only):

<html>
	<head>
		<script>
			function onLoad() {
				var shadowHost = document.querySelector("#shadow-host"),
					shadowRoot = shadowHost.webkitCreateShadowRoot();
				shadowRoot.innerHTML = document.querySelector("template").innerHTML;
			}
		</script>
	</head>
	<body onload="onLoad()">
		<div id="shadow-host">
			<span>Hi shadow DOM!</span>
		</div>
		<template>
			  <style>
			    content::-webkit-distributed(span) {
			      color: red !important;
			    }
		    </style>
		    <content></content>
		</template>
	</body>
</html>


Один shadow host может вмещать в себя несколько shadow tree — они будут отображены в порядке их добавления. Такой набор деревьев называется shadow stack. Более «старый» shadow tree так же можно переносить в другой shadow tree посредством shadow insertion point.

<html>
	<head>
		<script>
			function onLoad() {
				var shadowHost = document.querySelector("#shadow-host"),
					firstShadowRoot = shadowHost.webkitCreateShadowRoot(),
					secondShadowRoot = shadowHost.webkitCreateShadowRoot();

				firstShadowRoot.innerHTML = document.querySelector("#template-1").innerHTML;
				secondShadowRoot.innerHTML = document.querySelector("#template-2").innerHTML;
			}
		</script>
	</head>
	<body onload="onLoad()">
		<div id="shadow-host">
			<span>Hi shadow DOM!</span>
		</div>
		<template id="template-1">
		    <div>root 1</div>
		</template>
		<template id="template-2">
		    <div>root 2</div>
			<shadow></shadow>
		</template>
	</body>
</html>



Reprojection (перепроецирование)

Перепроецирование это ситуация, при которой первое shadow tree уже имеет insertion point, а второй shadow tree имеет shadow insetion point, при этом контент, взятый из shadow host сначала проецируется в первом shadow tree, а затем во втором.

<html>
	<head>
		<script>
			function onLoad() {
				var shadowHost = document.querySelector("#shadow-host"),
					firstShadowRoot = shadowHost.webkitCreateShadowRoot(),
					secondShadowRoot = shadowHost.webkitCreateShadowRoot();

				firstShadowRoot.innerHTML = document.querySelector("#template-1").innerHTML;
				secondShadowRoot.innerHTML = document.querySelector("#template-2").innerHTML;
			}
		</script>
	</head>
	<body onload="onLoad()">
		<div id="shadow-host">
			<span>Hi shadow DOM!</span>
		</div>
		<template id="template-1">
		    <div>root 1</div>
			<content select="span"></content>
		</template>
		<template id="template-2">
		    <div>root 2</div>
			<shadow></shadow>
		</template>
	</body>
</html>


Псевдо-элементы (в контексте 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 для доступа к элементам внутри него:
<html>
	<head>
		<script>
			function onLoad() {
				var shadowHost = document.querySelector("#shadow-host"),
					shadowRoot = shadowHost.webkitCreateShadowRoot();
				shadowRoot.innerHTML = document.querySelector("template").innerHTML;
			}
		</script>
		<style>
			div::x-thumb {
				width: 10px;
				height: 10px;
				background: black;
			}
		</style>
	</head>
	<body onload="onLoad()">
		<div id="shadow-host"></div>
		<template>
			<div pseudo="x-thumb"></div>
		</template>
	</body>
</html>


События

Некоторые события пропускаются через shadow boundary, некоторые нет. Исключение составляют mutation events — они вообще не должны возникать в shadow tree и, соответственно, переходить через shadow boundary. При прохождении события через shadow boundary у него меняется event.target для поддержания инкапсуляции.
Вот интересный пример:
<html>
	<head>
		<script>
			function onLoad() {
				var shadowHost = document.querySelector("#shadow-host"),
					shadowRoot = shadowHost.webkitCreateShadowRoot();
				shadowRoot.innerHTML = document.querySelector("template").innerHTML;
				shadowHost.addEventListener("mouseout", function(e) {
					console.log("mouse out", e.target);
				});
			}
		</script>
		<style>
			#shadow-host {
				width: 100px;
				height: 100px;
				background: blue;
			}
			#outer-element {
				width: 100%;
				height: 20px;
				background: red;
			}
		</style>
	</head>
	<body onload="onLoad()">
		<div id="shadow-host">
			<div id="outer-element"></div>
		</div>
		<template>
			<div id="first-inner-element"></div>
			<div id="second-inner-element"></div>
			<content></content>
			<style>
				#first-inner-element {
					width: 100px;
					height: 20px;
					background: green;
					position: absolute;
					top: 140px;
				}
				#second-inner-element {
					width: 100px;
					height: 20px;
					background: black;
					margin-bottom: 40px;
				}
			</style>
		</template>
	</body>
</html>


События спроецированного элемента всплывают в 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>
	<head>
		<script>
			function onLoad() {
				var shadowHost = document.querySelector("#shadow-host"),
					firstShadowRoot = shadowHost.webkitCreateShadowRoot();
				
				var secondShadowRoot = shadowHost.webkitCreateShadowRoot();
				secondShadowRoot.resetStyleInheritance = true;
				secondShadowRoot.applyAuthorStyles = true;

				firstShadowRoot.innerHTML = document.querySelector("#template-1").innerHTML;
				secondShadowRoot.innerHTML = document.querySelector("#template-2").innerHTML;
			}
		</script>
		<style>
			* {
				font-style: italic;
			}
		</style>
	</head>
	<body onload="onLoad()">
		<div id="shadow-host"></div>
		<template id="template-1">
			<style>
				* {
					color: red;
					font-weight: bold;
				}
			</style>
		    <div>root 1</div>
		</template>
		<template id="template-2">
		    <div>root 2</div>
			<shadow></shadow>
		</template>
	</body>
</html>


Итог

Можно сказать, что некоторой «инкапсуляции» для html не хватало. Это открывает большие возможности по созданию и шаблонизации различных, заранее подготовленных, виджетов на странице. Удивляет только отсутствие инкапсуляции JavaScript кода внутри виджетов, хотя мне казалось бы это довольно логичным.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 28
  • +2
    На html5rocks есть демка shadow dom visualizer.
    • +13
      Простите за глупый вопрос (я не являюсь web разработчиком). А чем полезен Shadow DOM? Желательно с примером и поподробнее.
      • +5
        Например, вы написали часть html кода (виджет), которую хотите использовать на нескольких страницах, причем так, чтобы стили этой страницы никак не влияли на визуальное оформление виджета, тогда вы создаете shadow tree и выставляете у него resetStyleInheritance в true.
        Либо, как при использовании связки xml + xslt, у вас есть некоторое дерево нод с данными, и используя shadow DOM и insertion points в нем вы можете оперировать с этими нодами из виджета как захотите.

        То есть основная фишка — это создание шаблонизации на клиенте. Но без «инкапсуляции» в DOM применяемые шаблоны могут воздействовать друг на друга (например, применение стили из других шаблонов), вот shadow DOM позволяет использовать шаблоны без оглядки на используемые на странице и во всех ее фрагментах стили.
        И вот здесь я не понимаю логики shadow DOM — почему нет инкапсуляции js кода внутри виджетов, тогда это была бы полноценная альтернатива фреймам.
        • +9
          Из второго абзаца этого комментария надо сделать небольшую статью с примерами, а саму статью закопать. Простите, я понимаю, что вы, наверное, старались, но получилась какая-то академическая статья с тяжёлым языком, ещё и с мешаниной русских и английских слов. У вас рука отвалится, если вместо «shadow tree» напишете «теневое дерево»?
          Некоторые события пропускаются через shadow boundary, некоторые нет. Исключение составляют mutation events — они вообще не должны возникать в shadow tree.
          Пушкин, с Далем в обнимку, вращаются в гробу.
          • 0
            Про шаблонизацию на клиенте уже есть статья: habrahabr.ru/post/152001
            Но ее автор так и не осветил, что же такое shadow DOM.
          • 0
            И вот здесь я не понимаю логики shadow DOM — почему нет инкапсуляции js кода внутри виджетов, тогда это была бы полноценная альтернатива фреймам.
            Не путайте инкапсуляцию и песочницу. Инкапсуляции js можно добиться средствами языка, и потому дополнительная поддержка со сторону shadow DOM не требуется.

            С CSS же наоборот — единственный способ добиться инкапсуляции на данный момент — это уникальные для виджетов префиксы, что никак не назвать красивым решением.
            • 0
              Не путайте инкапсуляцию и песочницу. Инкапсуляции js можно добиться средствами языка, и потому дополнительная поддержка со сторону shadow DOM не требуется.

              Но фреймы тоже не являются настоящей песочницей — дочерние фреймы (с учетом кроссдоменных особенностей, конечно) могут изменять содержимое родительского окна. Для инкапсуляции js, разумеется, можно написать специальный механизм, но было бы неплохо иметь это из коробки, разве нет?
              С CSS я полностью согласен.
              • 0
                Инкапсуляции js можно добиться средствами языка


                Например как?
                Можно ведь менять и прототипы встроеных обьектов, удалять что угодно, перезатирать глобальные переменные, и извне никак это не отследить (по моему).
                • 0
                  Очень просто — надо бить по рукам тех, кто меняет прототипы встроенных объектов, удаляет что угодно и использует глобальные переменные (кроме списка модулей).

                  Именно этим и отличаются задачи инкапсуляции и песочницы.
                  • 0
                    Ну ты же это не отследишь если это в постороннем виджете происходит? В этом то и беда. Сделать песочницу для js было бы идеально.
                    • 0
                      Это — задача автора виджета. Виджет, лезущий в глобальные объекты, называется глючным, и не используется.
                • +1
                  По CSS.
                  А как же параметр scoped?
                  • 0
                    Только в одну сторону. Хотя тоже неплохо…
              • +2
                Откройте настройки WebTools и включите «Show Shadow DOM», теперь вы можете посмотреть как устроен Video Player "изнутри". Так же нужно помнить, что наружу из компонента видны только те события, которые вы пробросите и никто не сможет подписаться на какой-нибудь `change` у `input` внутри компонента.

                Shadow DOM наконец-то дает возможность создания полноценных компонентов, единственное что в нем странно смотрится, так это «шаблонизация».
              • 0
                На счет стилей это понятно. Но если я, например, подключу какую нибудь js либу внутри shadow DOM, она будет видна снаружы?
                  • НЛО прилетело и опубликовало эту надпись здесь
                    • +1
                      А если чуть-чуть подождать, то видно, что будет :) Библиотека еще не подгрузилась.
                      • 0
                        В принципе довольно ожидаемо :) Спасибо
                        • НЛО прилетело и опубликовало эту надпись здесь
                          • 0
                            Это потому что вставлять тег script напрямую через innerHTML нельзя.
                            • НЛО прилетело и опубликовало эту надпись здесь
                        • 0
                          alert(typeof jQuery !== «undefined»? true: false);
                        • 0
                          Тест некорректен, посмотрите API — это лучше тысячи слов :]
                      • НЛО прилетело и опубликовало эту надпись здесь
                      • 0
                        Чем-то похоже на XBL.
                        Глобальные стили, правда, там не отключить – можно только добавить «местные».
                        Жаль, в веб оно так и не пошло, а потом – так и вообще отключили по умолчанию вместе с remote XUL.
                        • 0
                          Кстати, toolkitchen — полифилл для веб-компонентов.

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