Статьи

Введение в Shadow DOM

Возьмите любую современную веб-страницу, и вы заметите, что она неизменно содержит контент, сшитый из разных источников; он может включать в себя виджеты для обмена социальными сетями из Twitter или Facebook или виджет для воспроизведения видео на Youtube, он может показывать персонализированную рекламу с какого-либо рекламного сервера, или он может включать в себя некоторые служебные сценарии или стили из сторонней библиотеки, размещенной через CDN, и так далее. И если все основано на HTML (как это предпочитается в наши дни), существует высокая вероятность коллизий между разметкой, сценариями или стилями из разных источников. Как правило, пространства имен используются для предотвращения таких коллизий, которые в некоторой степени решают проблему, но они не обеспечивают инкапсуляцию .

Инкапсуляция является одним из столпов, на которых основана парадигма объектно-ориентированного программирования, и обычно используется для ограничения внутреннего представления объекта из внешнего мира.

Возвращаясь к нашей проблеме, мы можем уверенно инкапсулировать код JavaScript, используя замыкания или шаблон модуля, но можем ли мы сделать то же самое для нашей разметки HTML? Представьте, что нам нужно создать виджет пользовательского интерфейса, можем ли мы скрыть детали реализации нашего виджета от кода JavaScript и CSS, который включен на страницу, которая использует наш виджет? В качестве альтернативы, можем ли мы не допустить, чтобы потребительский код испортил функциональность или внешний вид нашего виджета?


Единственное существующее решение, которое создает границу между кодом, который вы пишете, и кодом, который потребляет, уродливо — и работает с использованием громоздкого и ограничительного iFrame , который приносит с собой еще один набор проблем. Так мы вынуждены всегда адаптироваться к этому подходу?

Уже нет! Shadow DOM предоставляет нам элегантный способ наложения обычного поддерева DOM специальным фрагментом документа, который содержит другое поддерево узлов, неприступное для сценариев и стилей. Самое интересное, что это не что-то новое! Различные браузеры уже используют эту методологию для реализации собственных виджетов, таких как дата, ползунки, аудио , видеоплееры и т. Д.

На момент написания этой статьи текущая версия Chrome (v29) поддерживает проверку Shadow DOM с использованием Chrome DevTools. Откройте Devtools и нажмите кнопку cog в правом нижнем углу экрана, чтобы открыть панель настроек , прокрутите немного вниз, и вы увидите флажок для отображения Shadow DOM.

Включить Shadow DOM

Теперь, когда мы включили наш браузер, давайте посмотрим на внутренние компоненты аудиоплеера по умолчанию. Просто введите:

1
<audio width=»300″ height=»32″ src=»http://developer.mozilla.org/@api/deki/files/2926/=AudioTest_(1).ogg» autoplay=»autoplay» controls=»controls»> Your browser does not support the HTML5 Audio.

В вашу HTML-разметку. Он показывает следующий собственный аудиоплеер в поддерживаемых браузерах:

аудиоплеер

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

Теневой ДОМ Родного Датчика Виджета

Вот это да! Он показывает внутреннее представление аудиоплеера, который в противном случае был скрыт. Как мы видим, элемент audio использует фрагмент документа для хранения внутреннего содержимого виджета и добавляет его к элементу контейнера (который известен как Shadow Host).

  • Shadow Host : это элемент DOM, в котором размещается поддерево Shadow DOM или это узел DOM, который содержит Shadow Root.
  • Корень тени : корень поддерева DOM, содержащий теневые узлы DOM. Это специальный узел, который создает границу между обычными DOM-узлами и Shadow DOM-узлами. Именно эта граница инкапсулирует узлы Shadow DOM из любого кода JavaScript или CSS на странице-потребителе.
  • Теневой DOM : позволяет объединить несколько поддеревьев DOM в одно большее дерево. Следующие изображения из рабочего проекта W3C наилучшим образом объясняют концепцию наложения узлов. Вот как это выглядит до того, как содержимое Shadow Root присоединяется к элементу Shadow Host:
    Обычное дерево документов. Теневые поддеревы DOM.

    При рендеринге Shadow Tree занимает место содержимого Shadow Host.

    Композиция завершена

    Этот процесс наложения узлов часто называют Композицией.

  • Граница тени : обозначена пунктирной линией на изображении выше. Это обозначает разделение между нормальным миром DOM и миром Shadow DOM. Сценарии с любой стороны не могут пересечь эту границу и создать хаос на другой стороне.

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

1
<div id=»welcomeMessage»>Welcome to My World</div>

Добавьте следующий код JavaScript или используйте эту скрипку :

Здесь мы создаем Shadow Root с помощью функции webkitCreateShadowRoot() , присоединяем его к Shadow Host, а затем просто меняем содержимое.

Обратите внимание на специфичный для поставщика префикс webkit перед именем функции. Это указывает на то, что эта функция в настоящее время поддерживается только в некоторых браузерах на основе webkit.

Если вы пойдете дальше и запустите этот пример в поддерживаемом браузере, то увидите «Hello Shadow DOM World» вместо «Welcome to My World», поскольку узлы Shadow DOM затеняют нормальные узлы.

Отказ от ответственности : Как некоторые из вас могут заметить, мы смешиваем разметку со скриптами, что обычно не рекомендуется, и Shadow DOM не является исключением. Мы сознательно избегали использования шаблонов в самом начале игры, чтобы избежать путаницы. В противном случае Shadow DOM предоставляет элегантное решение этой проблемы, и мы скоро туда доберемся.


Если вы попытаетесь получить доступ к содержимому визуализированного дерева, используя JavaScript, вот так:

Вы получите оригинальный контент «Добро пожаловать в мой мир», а не контент, который фактически отображается на странице, поскольку дерево Shadow DOM инкапсулировано из любых сценариев. Это также означает, что виджет, который вы создаете с помощью Shadow DOM, защищен от любых нежелательных / конфликтующих сценариев, уже присутствующих на странице.

Точно так же любому селектору CSS запрещено пересекать границу тени. Проверьте следующий код, где мы применили красный цвет к элементам списка, но этот стиль применяется только к узлам, которые являются частью родительской страницы, и элементы списка, которые являются частью Shadow Root, не затрагиваются этим стилем.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<div class=»outer»>
  <div id=»welcomeMessage»>Welcome to My World</div>
  <div class=»normalTree»>Sample List
  <ul>
      <li>Item 1</li>
      <li>Item 2</li>
  </ul>
  </div>
</div>
<style>
   div.outer li {
      color: red;
   }
   div.outer{
      border: solid 1px;
   }
</style>
<script type=»text/javascript»>
    var shadowHost = document.querySelector(«#welcomeMessage»);
    var shadowRoot = shadowHost.webkitCreateShadowRoot();
    shadowRoot.innerHTML = [«<div class=’shadowChild’>»,
                            «Shadow DOM offers us Encapsulation from»,
                            «<ul>»,
                            «<li>Scripts</li>»,
                            «<li>Styles</li>»,
                            «</ul>»,
                            «</div>»
                            ].join(‘,’).replace(/,/g,»»);
</script>

Вы можете увидеть код в действии на Fiddle . Эта инкапсуляция применяется, даже если мы поменяем направление обхода. Любые стили, определенные в Shadow DOM, не влияют на родительский документ и остаются только в Shadow Root. Проверьте эту скрипку для примера, где мы применяем синий цвет к элементам списка в Shadow DOM, но элементы списка родительского документа не затрагиваются.

Однако здесь есть одно заметное исключение; Shadow DOM дает нам возможность стилизовать Shadow Host, узел DOM, который содержит Shadow DOM. В идеале он находится за границей Shadow и не является частью Shadow Root, но, используя правило @host , можно указать стили, которые можно применить к Shadow Host, как мы создали стилевое сообщение в приведенном ниже примере.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<div id=»welcomeMessage»>Welcome to My World</div>
<script type=»text/javascript»>
  var shadowHost = document.querySelector(«#welcomeMessage»);
  var shadowRoot = shadowHost.webkitCreateShadowRoot();
  shadowRoot.innerHTML = [«<style>»,
                          «@host{ «,
                             «#welcomeMessage{ «,
                                «font-size: 28px;»,
                                «font-family:cursive;»,
                                «font-weight:bold;»,
                             «}»,
                          «}»,
                          «</style>»,
                          «<content select=»></content>»
                          ].join(‘,’).replace(/,/g,»»);
</script>

Проверьте эту скрипку, когда мы создаем приветственное сообщение Shadow Host, используя стили, определенные в Shadow DOM.

Как разработчик виджетов, я бы хотел, чтобы пользователь моего виджета мог стилизовать определенные элементы. Этого можно достичь, подключив отверстие к границе тени с помощью пользовательских псевдоэлементов . Это похоже на то, как некоторые браузеры создают хуки стиля для разработчика, чтобы стилизовать некоторые внутренние элементы собственного виджета. Например, для стилизации большого пальца и дорожки собственного слайдера вы можете использовать ::-webkit-slider-thumb и ::webkit-slider-runnable-track следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
input[type=range]{
   -webkit-appearance:none;
}
input[type=range]::-webkit-slider-thumb {
   -webkit-appearance:none;
   height:12px;
   width:12px;
   border-radius:6px;
   background:yellow;
   position:relative;
   top:-5px;
}
input[type=range]::-webkit-slider-runnable-track {
   background:red;
   height:2px;
}

Создайте эту скрипку и примените к ней свои собственные стили!

Если событие, которое происходит от одного из узлов в Shadow DOM, пересекает Shadow Boundary, то оно перенаправляется для обращения к Shadow Host для поддержания инкапсуляции. Рассмотрим следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
<input id=»normalText» type=»text» value=»Normal DOM Text Node» />
<div id=»shadowHost»></div>
<input id=»shadowText» type=»text» value=»Shadow DOM Node» />
<script type=»text/javascript»>
    var shadowHost = document.querySelector(‘#shadowHost’);
    var shadowRoot = shadowHost.webkitCreateShadowRoot();
    var template = document.querySelector(‘template’);
    shadowRoot.appendChild(template.content.cloneNode(true));
    template.remove();
    document.addEventListener(‘click’, function(e) {
                                 console.log(e.target.id + ‘ clicked!’);
                              });
</script>

Он визуализирует два элемента ввода текста, один через Normal DOM, а другой через Shadow DOM, а затем прослушивает событие click в document . Теперь, когда щелкают по второму вводу текста, событие происходит изнутри Shadow DOM, и когда оно пересекает границу Shadow, событие модифицируется, чтобы изменить целевой элемент на элемент <div> узла Shadow вместо <input> текста <input> , Мы также ввели новый элемент <template> здесь; это концептуально аналогично шаблонным решениям на стороне клиента, таким как Handlebars и Underscore, но не настолько развито и не поддерживает браузер. При этом использование шаблонов — это идеальный способ написания Shadow DOM, а не использование тегов скрипта, как это делалось до сих пор в этой статье.


Мы уже знаем, что всегда полезно отделять реальный контент от презентации; Shadow DOM не должен встраивать какой-либо контент, который должен быть окончательно показан пользователю. Скорее, контент всегда должен присутствовать на исходной странице, а не скрываться внутри шаблона Shadow DOM. Когда происходит композиция, этот контент должен быть спроецирован в соответствующие точки вставки, определенные в шаблоне Shadow DOM. Давайте перепишем пример Hello World, учитывая вышеприведенное разделение — живой пример можно найти на Fiddle .

1
2
3
4
5
6
7
<div id=»welcomeMessage»>Welcome to Shadow DOM World</div>
<script type=»text/javascript»>
    var shadowRoot = document.querySelector(«#welcomeMessage»).webkitCreateShadowRoot();
    var template = document.querySelector(«template»);
    shadowRoot.appendChild(template.content);
    template.remove();
</script>

Когда страница отображается, содержимое теневого хоста проецируется на место, где появляется элемент <content> . Это очень упрощенный пример, когда <content> собирает все внутри Shadow Host во время композиции. Но он вполне может быть избирательным при выборе контента из Shadow Host, используя атрибут select как показано ниже

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<div id=»outer»>How about some cool demo, eh ?
    <div class=»cursiveButton»>My Awesome Button</div>
</div>
<button>
  Fallback Content
</button>
<style>
button{
   font-family: cursive;
   font-size: 24px;
   color: red;
}
</style>
<script type=»text/javascript»>
    var shadowRoot = document.querySelector(«#outer»).webkitCreateShadowRoot();
    var template = document.querySelector(«template»);
    shadowRoot.appendChild(template.content.cloneNode(true));
    template.remove();
</script>

Посмотрите живую демоверсию и поиграйте с ней, чтобы лучше понять концепцию точек вставки и проекций.


Как вы, возможно, уже знаете, Shadow DOM является частью спецификации веб-компонентов , которая предлагает другие полезные функции, такие как:

  1. Шаблоны — используются для хранения инертной разметки, которая будет использоваться позднее. Под инертным мы подразумеваем, что все изображения в разметке не загружаются, включенные сценарии отсутствуют, пока содержимое шаблона фактически не станет частью страницы.
  2. Декораторы — используются для применения шаблонов, основанных на CSS-селекторах, и, следовательно, могут рассматриваться как украшающие существующие элементы путем улучшения их представления.
  3. Импорт HTML — предоставляет нам возможность повторно использовать другие документы HTML в нашем документе без необходимости явно делать вызовы XHR и писать обработчики событий для него.
  4. Пользовательские элементы — позволяет нам определять новые типы элементов HTML, которые затем могут быть декларативно использованы в разметке. Например, если вы хотите создать свой собственный виджет навигации, вы определяете свой элемент навигации, унаследованный от HTMLElement и предоставляющий определенные обратные вызовы жизненного цикла, которые реализуют определенные события, такие как конструирование, изменение, уничтожение виджета, и просто используете этот виджет в разметке. as <myAwesomeNavigation attr1="value1"..></myAwesomeNavigation> . Таким образом, пользовательские элементы, по сути, дают нам возможность объединить всю магию Shadow DOM, скрывая внутренние детали и упаковывая все вместе.

Я не буду много болтать о других аспектах спецификации веб-компонентов в этой статье, но было бы полезно вспомнить, что вместе они позволяют нам создавать многократно используемые виджеты пользовательского интерфейса, которые переносятся во все браузеры во внешнем виде и полностью инкапсулированы всеми скрипты и стили потребляющей страницы.


Спецификация веб-компонентов находится в стадии разработки, и включенный образец кода, который работает сегодня, может не работать в более позднем выпуске. Например, более ранние тексты на эту тему используют метод webkitShadowRoot() который больше не работает; Вместо этого используйте createWebkitShadowRoot() для создания Shadow Root. Поэтому, если вы хотите использовать это для создания некоторых классных демо-версий с использованием Shadow DOM, всегда лучше обратиться к спецификации для получения подробной информации.

В настоящее время только Chrome и Opera поддерживают его, поэтому я бы с осторожностью включил Shadow DOM в свой производственный экземпляр, но с выходом Google с Polymer, который построен на основе веб-компонентов, и Polyfills , который изначально поддерживает Shadow DOM, это безусловно, то, что каждый веб-разработчик должен испачкать руки.

Вы также можете оставаться в курсе последних событий на Shadow DOM, следя за этим каналом Google+ . Также ознакомьтесь с инструментом Shadow DOM Visualizer , который поможет вам визуализировать, как Shadow DOM визуализирует в браузере.