Глубокое понимание Shadow DOM v1

DOM

Перевод: сумасшедший технический ботаникblog.log Rocket.com/понимаю я…

img

Shadow DOM — это не злодей из фильма о супергероях и не темная сторона DOM. Shadow DOM — это просто способ решить проблему отсутствия инкапсуляции дерева в объектной модели документа (или DOM для краткости).

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

Тем не менее, при написании больших программ эти усилия кажутся не такими уж эффективными, и много времени тратится на предотвращение конфликтов CSS и JavaScript. API Shadow DOM призван решить эти проблемы, предоставляя механизм для инкапсуляции дерева DOM.

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

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

Что такое ДОМ?

Прежде чем погрузиться в создание теневого DOM, важно понять, что такое DOM. Объектная модель документа W3C (DOM) предоставляет независимый от платформы и языка интерфейс прикладного программирования (API) для представления и управления информацией, хранящейся в документах HTML и XML.

Используя DOM, программисты могут получать доступ, добавлять, удалять или изменять элементы и содержимое. DOM рассматривает веб-страницу как древовидную структуру, каждая ветвь которой заканчивается узлом, а каждый узел содержит объект, который можно изменить с помощью языков сценариев, таких как JavaScript. Рассмотрим следующий HTML-документ:

<html>
  <head>
    <title>Sample document</title>
  </head>
  <body>
    <h1>Heading</h1>
    <a href="https://example.com">Link</a>
  </body>
</html>

Представление DOM этого HTML выглядит следующим образом:

此图中所有的框都是节点

Все прямоугольники на этой диаграмме являются узлами.

Терминология, используемая для описания частей DOM, аналогична реальному генеалогическому древу:

  • Узел над данным узлом является родительским узлом этого узла.
  • Узлы рядом с данным узлом являются дочерними элементами этого узла.
  • Узлы с одним и тем же родителем являются братьями и сестрами.
  • Все узлы выше данного узла (включая родителя и прародителя) называются предками этого узла.
  • Наконец, все узлы под данным узлом называются потомками этого узла.

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

Для доступа к дереву DOM предоставляет набор методов, которые программисты могут использовать для изменения содержимого и структуры документа. Например, когда вы пишетеdocument.createElement('p');При использовании методов, предоставляемых DOM. Без DOM JavaScript не может понять структуру документов HTML и XML.

В следующем коде JavaScript показано, как использовать методы DOM для создания двух элементов HTML, вложения одного в другой, установки текстового содержимого и, наконец, добавления их в тело документа:

const section = document.createElement('section');
const p = document.createElement('p');
p.textContent = 'Hello!';
section.appendChild(p);
document.body.appendChild(section);

Вот результирующая структура DOM после запуска этого кода JavaScript:

<body>
  <section>
    <p>Hello!</p>
  </section>
</body>

Что такое теневой DOM?

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

В соответствии с этим определением объект предоставляет интерфейс в виде общедоступных методов как способ взаимодействия со своими данными. Таким образом, внутреннее представление объекта не может быть напрямую доступно извне объекта.

Shadow DOM переносит эту концепцию в HTML. Это позволяет вам связать скрытые, отдельные DOM с элементами, что означает, что вы можете использовать нативную область HTML и CSS. Теперь можно использовать более общие селекторы CSS, не беспокоясь о конфликтах имен, а стили больше не протекают и не применяются к неподходящим элементам.

На самом деле, Shadow DOM API — это именно то, что нужно разработчикам библиотек и виджетов для отделения структуры, стилей и поведения HTML от остального кода.

Теневой корень — это самый верхний узел в теневом дереве, и это то, что добавляется к обычному узлу DOM при создании теневого DOM. Узел со связанным с ним теневым корнем называется теневым хостом.

Вы можете присоединять элементы к теневому корню, как и к обычному DOM. Узлы, связанные с теневым корнем, образуют теневое дерево. Диаграмма должна быть в состоянии выразить более четко:

img

Термин светлый DOM часто используется, чтобы отличить нормальный DOM от теневого DOM. Теневой DOM и светлый DOM вместе называются логическим DOM. Точка, в которой светлый DOM отделяется от теневого DOM, называется границей тени. Запросы DOM и правила CSS не могут достичь другой стороны теневой границы, создавая инкапсуляцию.

Создайте теневой DOM

Чтобы создать теневой DOM, вам нужно использоватьElement.attachShadow()Метод прикрепляет теневой корень к элементу:

var shadowroot = element.attachShadow(shadowRootInit);

Давайте посмотрим на простой пример:

<div id="host"><p>Default text</p></div>
    
<script>
  const elem = document.querySelector('#host');
     
  // attach a shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'open'});
     
  // create a <p> element
  const p = document.createElement('p');
     
  // add <p> to the shadow DOM
  shadowRoot.appendChild(p);
     
  // add text to <p> 
  p.textContent = 'Hello!';
</script>

Этот код добавляет теневое дерево DOM кdivэлементы, которыеidдаhost. это дерево сdivФактические дочерние элементы являются отдельными, и все, что добавляется поверх них, будет локальным для управляемого элемента.

Chrome DevTools中的Shadow root

Теневой корень в Chrome DevTools.

Уведомление#hostКак существующие элементы заменяются теневым корнем. Браузеры, не поддерживающие теневой DOM, будут использовать содержимое по умолчанию.

Теперь при добавлении CSS в основной документ правила стиля не влияют на теневой DOM:

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
 
  // attach a shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  // set the HTML contained within the shadow root
  shadowRoot.innerHTML = '<p>Shadow DOM</p>';
</script>
 
<style>
  p {color: red}
</style>

Стили, определенные в светлом DOM, не могут пересекать границу тени. Таким образом, только абзацы в светлом DOM станут красными.

img

Вместо этого CSS, который вы добавляете в теневую модель DOM, является локальным для основного элемента и не влияет на другие элементы модели:

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>p {color: red}</style>`;
 
</script>

img

Вы также можете поместить правила стиля во внешние таблицы стилей следующим образом:

shadowRoot.innerHTML = `
  <p>Shadow DOM</p>
  <link rel="stylesheet" href="style.css">`;

получитьshadowRootСсылка на элемент для добавления, используяhostАтрибуты:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  console.log(shadowRoot.host);    // => <div id="host"></div>
</script>

Чтобы сделать обратное и получить ссылку на теневой корень, размещенный в элементе, используйте свойство элементаshadowRootАтрибуты:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  console.log(elem.shadowRoot);    // => #shadow-root (open)
</script>

shadowRoot mod

при звонкеElement.attachShadow()Чтобы прикрепить теневой корень, вы должны указать режим инкапсуляции теневого DOM-дерева, передав объект в качестве параметра, иначе он выдастTypeError. Объект должен иметьmodeимущество, стоимость которогоopenилиclosed.

Открытый теневой корень позволяет вам использоватьshadowRootАтрибуты обращаются к элементу теневого корня из-за пределов корня, как показано в следующем примере:

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
 
  // attach an open shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `<p>Shadow DOM</p>`;
  // Nodes of an open shadow DOM are accessible
  // from outside the shadow root
  elem.shadowRoot.querySelector('p').innerText = 'Changed from outside the shadow root';
  elem.shadowRoot.querySelector('p').style.color = 'red';
</script>

img

Но если значение свойства режима «закрыто», попытка доступа к теневому корневому элементу с помощью JavaScript из-за пределов корня приводит к ошибке.TypeError:

<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
 
  // attach a closed shadow root to #host
  const shadowRoot = elem.attachShadow({mode: 'closed'});
 
  shadowRoot.innerHTML = `<p>Shadow DOM</p>`;
 
  elem.shadowRoot.querySelector('p').innerText = 'Now nodes cannot be accessed from outside';
  // => TypeError: Cannot read property 'querySelector' of null 
</script>

Когда установлен режимclosedчас,shadowRootвозврат собственностиnull. потому чтоnullУ значения нет никаких свойств или методов, поэтому вызовите егоquerySelector()приведет кTypeError. Браузеры часто используют закрытый теневой доступ, чтобы сделать некоторые реализации элементов недоступными внутри и не изменяемыми из JavaScript.

Чтобы определить, находится ли теневой DOM в открытом или закрытом режиме, вы можете обратиться к корневому каталогу теней.modeАтрибуты:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'closed'});
 
  console.log(shadowRoot.mode);    // => closed
</script>

На первый взгляд закрытый теневой DOM может показаться очень удобным для авторов веб-компонентов, которые не хотят раскрывать теневой корень своих компонентов, однако на практике обойти закрытый теневой DOM несложно. Обычно требуется больше усилий, чтобы полностью скрыть теневой DOM, чем оно того стоит.

Не все элементы HTML могут содержать теневой DOM

Только ограниченный набор элементов может содержать теневой DOM. В следующей таблице перечислены поддерживаемые элементы:

+----------------+----------------+----------------+
|    article     |      aside     |   blockquote   |
+----------------+----------------+----------------+
|     body       |       div      |     footer     |
+----------------+----------------+----------------+
|      h1        |       h2       |       h3       |
+----------------+----------------+----------------+
|      h4        |       h5       |       h6       |
+----------------+----------------+----------------+
|    header      |      main      |      nav       |
+----------------+----------------+----------------+
|      p         |     section    |      span      |
+----------------+----------------+----------------+

Попытка добавить теневое дерево DOM к другим элементам приведет к ошибке «DOMException». Например:

document.createElement('img').attachShadow({mode: 'open'});    
// => DOMException

использовать<img>Неразумно, чтобы элемент был теневым хостом, поэтому неудивительно, что этот код выдает ошибку. вы можете получитьDOMExceptionДругая причина ошибки заключается в том, что в браузере уже размещен теневой DOM с элементом.

Браузеры автоматически прикрепляют теневой DOM к определенным элементам

Shadow DOM существует уже давно, и браузеры используют его, чтобы скрыть внутреннюю структуру элементов, таких как<input>,<textarea>а также<video>.

когда вы используете в HTML<video>элемент, браузер автоматически прикрепляет теневой DOM к элементу, содержащему элементы управления браузера по умолчанию. Но единственное, что видно в DOM, это<video>Сам элемент:

img

Чтобы отобразить теневой корень таких элементов в Chrome, откройте настройки Chrome DevTools (нажмите F1) и установите флажок «Показать теневой DOM пользовательского агента» в разделе «элементы»:

img

Когда установлен флажок «Показать теневой DOM пользовательского агента», теневой корневой узел и его дочерние элементы станут видимыми. Вот как выглядит тот же код с включенной опцией:

img

Хост-теневой DOM на пользовательском элементе

Пользовательские элементы, созданные API пользовательских элементов, могут содержать теневую модель DOM, как и любой другой элемент. См. следующий пример:

<my-element></my-element>
<script>
  class MyElement extends HTMLElement {
    constructor() {
 
      // must be called before the this keyword
      super();
 
      // attach a shadow root to <my-element>
      const shadowRoot = this.attachShadow({mode: 'open'});
 
      shadowRoot.innerHTML = `
        <style>p {color: red}</style>
        <p>Hello</p>`;
    }
  }
 
  // register a custom element on the page
  customElements.define('my-element', MyElement);
</script>

Этот код создает пользовательский элемент, в котором размещается теневой DOM. это звонитcustomElements.define()с именем элемента в качестве первого параметра и объектом класса в качестве второго параметра. Этот класс расширяетHTMLElementИ определяет поведение элемента.

В конструктореsuper()Используется для создания цепочек прототипов и прикрепления корней Shadow к пользовательским элементам. когда вы используете<my-element>, он создает свой собственный теневой DOM:

img

Помните, что допустимый пользовательский элемент не может состоять из одного слова и должен содержать дефис (-) в имени. Например,myelementне может использоваться в качестве имени пользовательского элемента и вызоветDOMExceptionошибка.

Стилизация основных элементов

Как правило, чтобы стилизовать основной элемент, вам нужно добавить CSS в светлый DOM, так как именно там живет основной элемент. Но что, если вам нужно стилизовать хост-элемент в теневой модели DOM?

Этоhost()Вот тут-то и появляются функции псевдокласса. Этот селектор позволяет получить доступ к теневому хосту из любой точки теневого корня. Вот пример:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>
      :host {
        display: inline-block;
        border: solid 3px #ccc;
        padding: 0 15px;
      }
    </style>`;
</script>

Стоит отметить, что:hostРаботает только в теневом корне. Также имейте в виду, что правила стиля, определенные за пределами теневого корня, менее:hostПравила, определенные в, имеют более высокую специфичность.

Например,#host { font-size: 16px; }имеет более высокий приоритет, чем теневой DOM:host { font-size: 20px; }. Это на самом деле полезно, это позволяет вам определить стили по умолчанию для вашего компонента и позволить пользователю компонента переопределить ваши стили. Единственное исключение!importantправила, специфичные для теневого DOM.

Вы также можете передавать селекторы в качестве аргументов:host(), что позволяет нацеливаться на узел, только если узел соответствует указанному селектору. Другими словами, он позволяет вам ориентироваться на разные состояния одного и того же хоста:

<style>
  :host(:focus) {
    /* style host only if it has received focus */
  }
 
  :host(.blue) {
    /* style host only if has a blue class */
  }
 
  :host([disabled]) {
    /* style host only if it's disabled */
  }
</style>

Контекстно-ориентированный стиль

Чтобы выбрать теневой корневой хост внутри определенного предка, используйте:host-context()Функция псевдокласса. Например:

:host-context(.main) {
  font-weight: bold;
}

только если это.mainЭтот код CSS выбирает теневой хост, только если он является потомком:

<body class="main">
  <div id="host">
  </div>
</body>

:host-context()Особенно полезно для создания тем, поскольку позволяет авторам стилизовать компоненты в зависимости от контекста, в котором они используются.

стильный крючок

Одной из интересных особенностей теневого DOM является его способность создавать «заполнители стилей» и позволять пользователям заполнять их. Это можно сделать с помощьюПользовательские свойства CSS来完成。 Давайте рассмотрим простой пример:

<div id="host"></div>
 
<style>
  #host {--size: 20px;}
</style>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>p {font-size: var(--size, 16px);}</style>`;
 
</script>

Этот теневой DOM позволяет пользователям переопределять размер шрифта своих абзацев. Используйте нотацию пользовательского атрибута (— size: 20px) для установки значения, а теневой DOM используетvar()функция(font-size: var( — size, 16px)), чтобы получить значение. Концептуально это похоже на<slot>Как работают элементы.

Наследуемые стили

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

некоторые свойства (например,color,backgroundа такжеfont-family) пройдет границы теней и будет применен к теневому дереву. Так что теневой DOM не является очень сильным барьером по сравнению с iframe.

<style>
  div {
    font-size: 25px;
    text-transform: uppercase;
    color: red;
  }
</style>
 
<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `<p>Shadow DOM</p>`;
</script>

Решение простое: объявивall: initialСбросьте наследуемый стиль к его начальному значению следующим образом:

<style>
  div {
    font-size: 25px;
    text-transform: uppercase;
    color: red;
  }
</style>
 
<div><p>Light DOM</p></div>
<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <p>Shadow DOM</p>
    <style>
      :host p {
        all: initial;
      }
    </style>`;
</script>

В этом примере элемент принудительно возвращается в исходное состояние, поэтому стили, пересекающие границу тени, не действуют.

событие переезда

События, запускаемые внутри теневого DOM, могут пересекать границу теневого объекта и всплывать в светлом DOM; однакоEvent.targetЗначение автоматически изменяется, поэтому создается впечатление, что событие произошло из содержащего его теневого дерева, а не из основного элемента фактического элемента.

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

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <ul>
      <li>One</li>
      <li>Two</li>
      <li>Three</li>
    <ul>
    `;
 
  document.addEventListener('click', (event) => {
    console.log(event.target);
  }, false);
</script>

Когда вы щелкаете в любом месте теневого DOM, этот код<div id =“host”> ... </div>Записывает в консоль, поэтому слушатели не могут видеть фактический элемент, отправивший событие.

Но в теневом DOM ретаргетинг не происходит, вы можете легко найти фактический элемент, связанный с событием:

<div id="host"></div>
 
<script>
  const elem = document.querySelector('#host');
  const shadowRoot = elem.attachShadow({mode: 'open'});
 
  shadowRoot.innerHTML = `
    <ul>
      <li>One</li>
      <li>Two</li>
      <li>Three</li>
    </ul>`;
   
  shadowRoot.querySelector('ul').addEventListener('click', (event) => {
    console.log(event.target);
  }, false);  
</script>

Обратите внимание, что не все события распространяются из теневого DOM. Те, что есть, перемещаются, а другие просто игнорируются. если вы используетепользовательское событие, вам нужно использоватьcomposed:trueфлаг, иначе события не будут выходить за границу тени.

Shadow DOM V0 и V1

Первоначальная версия спецификации Shadow DOM была реализована в Chrome 25 и тогда называлась Shadow DOM v0. Новая версия спецификации улучшает многие аспекты API Shadow DOM.

Например, элемент больше не может содержать несколько теневых DOM, а некоторые элементы вообще не могут содержать теневые DOM. Нарушение этих правил приведет к ошибкам.

Кроме того, Shadow DOM v1 предоставляет набор новых функций, таких как включение теневого режима, резервный контент и многое другое. Вы можете найти всестороннее сравнение между v0 и v1, написанное одним из авторов спецификации (Хайя опубликовала oh.IO/2016/shadow…W3CНайдите полное описание Shadow DOM v1.

Браузерная поддержка Shadow DOM v1

На момент написания этой статьи Firefox и Chrome уже полностью поддерживают Shadow DOM v1. К сожалению, Edge еще не реализовал v1, Safari поддерживает его лишь частично. существуетМогу ли я использовать ...Актуальный список поддерживаемых браузеров доступен на .

Чтобы реализовать теневой DOM в браузерах, не поддерживающих Shadow DOM v1, вы можете использоватьshadydomа такжеshadycssполифиллы.

Суммировать

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

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

Как упоминалось ранее, веб-компоненты состоят из трех основных технологий, ключевой частью которых является теневой DOM. Надеюсь, после прочтения этой статьи вам будет легче понять, как эти три технологии работают вместе для создания веб-компонентов.

Добро пожаловать в общедоступную учетную запись внешнего интерфейса: пионер внешнего интерфейса, получайте больше галантереи внешнего интерфейса.