Тень ДОМ 101

Введение

Веб-компоненты — это набор передовых стандартов, которые:

  1. Сделайте возможным создание виджетов
  2. …которые можно надежно использовать повторно
  3. …и который не повредит страницы, если следующая версия компонента изменит детали внутренней реализации.

Означает ли это, что вам придется решать, когда использовать HTML/JavaScript, а когда веб-компоненты? Нет! HTML и JavaScript могут создавать интерактивные визуальные материалы. Виджеты — это интерактивные визуальные элементы. При разработке виджета имеет смысл использовать свои навыки HTML и JavaScript. Стандарты веб-компонентов призваны помочь вам в этом.

Но есть фундаментальная проблема, которая затрудняет использование виджетов, созданных на основе HTML и JavaScript: дерево DOM внутри виджета не инкапсулировано из остальной части страницы. Отсутствие инкапсуляции означает, что таблица стилей вашего документа может случайно примениться к частям внутри виджета; ваш JavaScript может случайно изменить части внутри виджета; ваши идентификаторы могут пересекаться с идентификаторами внутри виджета; и так далее.

Веб-компоненты состоят из трех частей:

  1. Шаблоны
  2. Тень ДОМ
  3. Пользовательские элементы

Shadow DOM решает проблему инкапсуляции дерева DOM. Четыре части веб-компонентов предназначены для совместной работы, но вы также можете выбирать, какие части веб-компонентов использовать. В этом руководстве показано, как использовать Shadow DOM.

Привет, мир теней

С помощью Shadow DOM элементы могут получить связанный с ними узел нового типа. Этот новый тип узла называется теневым корнем . Элемент, с которым связан теневой корень, называется теневым хостом. Содержимое теневого хоста не отображается; вместо этого отображается содержимое теневого корня.

Например, если у вас была такая разметка:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

тогда вместо

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

ваша страница выглядит так

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

Мало того, если JavaScript на странице запрашивает textContent кнопки, он получит не «こんにちは、影の世界!», а «Hello, world!» потому что поддерево DOM под теневым корнем инкапсулировано.

Отделение контента от презентации

Теперь мы рассмотрим использование Shadow DOM для отделения контента от представления. Допустим, у нас есть этот тег имени:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Вот разметка. Это то, что вы бы написали сегодня. Он не использует Shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

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

Мы можем избежать плохих времен.

Шаг 1. Скройте детали презентации

Семантически нас, вероятно, волнует только то, что:

  • Это именной тег.
  • Имя «Боб».

Сначала мы пишем разметку, которая ближе к истинной семантике, которую мы хотим:

<div id="nameTag">Bob</div>

Затем мы помещаем все стили и элементы div, используемые для представления, в элемент <template> :

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

На данный момент «Боб» — единственное, что визуализируется. Поскольку мы переместили презентационные элементы DOM внутрь элемента <template> , они не отображаются, но к ним можно получить доступ из JavaScript. Мы делаем это сейчас, чтобы заполнить теневой корень:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

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

<div id="nameTag">Bob</div>

Это демонстрирует, что с помощью Shadow DOM мы скрыли детали представления тега имени из документа. Детали презентации инкапсулированы в Shadow DOM.

Шаг 2. Отделите контент от презентации

Наш тег имени теперь скрывает детали презентации со страницы, но на самом деле он не отделяет презентацию от контента, поскольку, хотя контент (имя «Боб») находится на странице, отображается то имя, которое мы скопировали в теневой корень. Если мы хотим изменить имя в теге имени, нам придется сделать это в двух местах, и они могут не синхронизироваться.

Элементы HTML являются композиционными — например, вы можете поместить кнопку внутри таблицы. Здесь нам нужна композиция: бейдж с именем должен представлять собой композицию на красном фоне с надписью «Привет!» текст и содержимое тега имени.

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

Если мы изменим разметку в Shadow DOM на такую:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

При отображении тега имени содержимое теневого хоста проецируется в то место, где появляется элемент <content> .

Теперь структура документа стала проще, поскольку имя находится только в одном месте — документе. Если вашей странице когда-либо понадобится обновить имя пользователя, просто напишите:

document.querySelector('#nameTag').textContent = 'Shellie';

и это все. Отображение тега имени автоматически обновляется браузером, поскольку мы проецируем содержимое тега имени на место с помощью <content> .

<div id="ex2b">

Теперь мы добились разделения контента и представления. Содержимое находится в документе; презентация находится в Shadow DOM. Они автоматически синхронизируются браузером, когда приходит время что-то визуализировать.

Шаг 3: Прибыль

Разделив контент и представление, мы можем упростить код, который манипулирует контентом — в примере с тегом имени этому коду нужно иметь дело только с простой структурой, содержащей один <div> вместо нескольких.

Теперь, если мы изменим нашу презентацию, нам не нужно будет менять какой-либо код!

Например, предположим, что мы хотим локализовать наш тег имени. Это по-прежнему тег имени, поэтому смысловое содержание документа не меняется:

<div id="nameTag">Bob</div>

Код установки теневого корня остается прежним. Изменяется только то, что помещается в теневой корень:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

Это большое улучшение по сравнению с сегодняшней ситуацией в Интернете, поскольку ваш код обновления имени может зависеть от структуры компонента , которая является простой и последовательной. Коду обновления имени не обязательно знать структуру, используемую для рендеринга. Если мы посмотрим на то, что отображается, имя появится вторым на английском языке (после «Привет! Меня зовут»), но первым на японском (перед «と申します»). Это различие семантически бессмысленно с точки зрения обновления отображаемого имени, поэтому коду обновления имени не обязательно знать об этой детали.

Дополнительный балл: Продвинутая проекция

В приведенном выше примере элемент <content> выбирает весь контент с теневого хоста. Используя атрибут select , вы можете контролировать, что проецирует элемент контента. Вы также можете использовать несколько элементов контента.

Например, если у вас есть документ, который содержит следующее:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

и теневой корень, который использует селекторы CSS для выбора определенного контента:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

Элемент <div class="email"> совпадает с элементами <content select="div"> и <content select=".email"> . Сколько раз появляется адрес электронной почты Боба и какими цветами?

Ответ заключается в том, что адрес электронной почты Боба появляется один раз и имеет желтый цвет.

Причина в том, что, как знают люди, взламывающие Shadow DOM, построение дерева того, что на самом деле отображается на экране, похоже на огромную вечеринку. Элемент контента — это приглашение, которое позволяет контенту из документа попасть в закулисную часть рендеринга Shadow DOM. Эти приглашения доставляются по порядку; кто получит приглашение, зависит от того, кому оно адресовано (то есть атрибут select ). Содержимое, однажды приглашенное, всегда принимает приглашение (кто бы не стал?!) и уходит. Если последующее приглашение будет отправлено на этот адрес еще раз, что ж, никого нет дома, и оно не придет на вашу вечеринку.

В приведенном выше примере <div class="email"> соответствует как селектору div , так и селектору .email , но поскольку элемент содержимого с селектором div находится в документе раньше, <div class="email"> переходит к желтая вечеринка, а на синюю вечеринку некому прийти. (Возможно, поэтому он такой синий, хотя несчастье любит компанию, так что никогда не знаешь.)

Если что-то не приглашено ни на одну из сторон, то оно вообще не будет визуализировано. Именно это произошло с текстом «Hello, world» в самом первом примере. Это полезно, когда вы хотите добиться радикально иного рендеринга: напишите в документе семантическую модель, которая доступна для скриптов на странице, но скройте ее для целей рендеринга и подключите к действительно другой модели рендеринга в Shadow DOM. используя JavaScript.

Например, в HTML есть удобный инструмент выбора даты. Если вы напишете <input type="date"> вы получите аккуратный всплывающий календарь. Но что, если вы хотите, чтобы пользователь мог выбрать диапазон дат для отпуска на десертном острове (вы знаете… с гамаками, сделанными из красных лоз). Вы настраиваете свой документ следующим образом:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

но создайте Shadow DOM, который использует таблицу для создания удобного календаря, в котором выделяется диапазон дат и так далее. Когда пользователь щелкает дни в календаре, компонент обновляет состояние входных данных startDate и endDate; когда пользователь отправляет форму, значения из этих элементов ввода отправляются.

Зачем я включил в документ метки, если они не будут отображаться? Причина в том, что если пользователь просматривает форму в браузере, который не поддерживает Shadow DOM, форму по-прежнему можно использовать, но не так красиво. Пользователь видит что-то вроде:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

Вы проходите тень DOM 101

Это основы Shadow DOM — вы проходите Shadow DOM 101! Вы можете делать больше с помощью Shadow DOM, например, вы можете использовать несколько теней на одном теневом хосте или вложенные тени для инкапсуляции, или спроектировать свою страницу с помощью представлений на основе модели (MDV) и Shadow DOM. Веб-компоненты — это больше, чем просто Shadow DOM.

Мы объясним это в последующих постах.