Пользователь
0,0
рейтинг
21 мая 2013 в 05:37

Разработка → 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 кода внутри виджетов, хотя мне казалось бы это довольно логичным.
@mycrashedmind
карма
7,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

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