Пару недель назад был опубликован пост про то, что из себя представляет DOM. Чтобы освежить понятие в памяти, DOM (Объектное представление документа) это представление HTML кода в виде объекта, который используется браузером для определения того, что должно быть отрисовано в окне. Так же DOM используется в JavaScript для редактирования контента и структуры страницы, а так же для стилизации элементов.

Для примера рассмотрим следующий документ:

<!doctype html>
<html lang="en">
 <head>
 <title>My first web page</title>
  </head>
 <body>
    <h1>Hello, world!</h1>
    <p>How are you?</p>
 </body>
</html>

Этот HTMl, может быть представлен как дерево:

  • html
    • head
      • title
        • My first web page
  • body
    • h1
      • Hello, world!
    • p
      • How are you?

На протяжении последних лет, вы наверное слышали такие термины как: “Shadow DOM” и “Virtual DOM”. Они, конечно же имея отношение к оригинальному DOM, но представляют собой очень разные концепции. В этой статье мы рассмотрим что такое Shadow DOM на самом деле, и чем он отличается от обычного DOM-дерева. В следующей статье я сделаю так же, но уже про Virtual DOM.

Все находится в глобальной области видимости 👍🏾! Постойте, а ведь все находится в глобальной области видимости 👎🏾

Все элементы и стили внутри HTML-странницы располагаются в одной, глобальной области видимости. К Любому элементу на странице можно получить доступ через метод document.querySelector(), независимо от его расположения уровня вложенности внутри HTML-страницы. Похожим образом, CSS, на странице имеет доступ к любому элементу и не важно где он находится на странице.

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

* { box-sizing: border-box }

Однако, бывают случаи когда нам нужно изолировать компонент на странице, и мы не хотели бы чтобы он мог быть изменен извне глобальными стилями. Хороший пример такого случая это сторонний встраиваемый виджет, кнопка “follow me” в твиттере.

Представим как можно ее реализовать:

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

twitter button inside iframe

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

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

DOM внутри DOM

Вы можете считать что Shadow DOM это
«DOM внутри другого DOM». Это самостоятельное DOM-дерево, со своими собственными элементами и стилями, полностью изолированное от внешнего содержимого страницы.

Несмотря на то, что технология получила свое распространите относительно недавно, shadow DOM использовался браузерами годами для стилизации сложных интерфейсных элементов, таких как формы и их элементы. Возьмем для примера элемент типа Range (ползунок). Для его создания, все что, то нам нужно сделать это добавить следующий элемент: <input type="range">

Этот единственный элемент становится следующим компонентом:

Если мы копнем глубже, то увидим, что этот <input> элемент состоит из нескольких более мелких <div> элементов, контролирующих поведение трека и ручки ползунка (слайдера).

input type range structure

Такой компонент, сделан с помощью shadow DOM. Этот элемент отображается в коде как простой <input>, за которым, на самом деле располагаются другие элементы и стили связанные с ним, но не являющееся частью глобального DOM.

Как работает Shadow DOM

Для иллюстрации того, как работает Shadow DOM, давайте воссоздадим кнопку “follow me” для Twitter, но уже не используя <iframe>. Прежде всего надо начать с элемента, в котором будет располагаться Shadow DOM элемент. Это будет обычный HTML элемент, в составе общего DOM, в который мы добавим наш Shadow DOM элемент. Такой компонент как кнопка “follow me”, может содержать fallback элемент показываемый в случае если отключен javascript или же браузер не поддерживает Shadow DOM.

<span class="shadow-host">
  <a href="https://twitter.com/ireaderinokun">
     Follow @ireaderinokun
  </a>
</span>

Обратите внимание, что мы не использовали элемент <a> сразу как хост для нашего Shadow DOM, потому что некоторые элементы, в основном интерактивные, не могут быть хостами для shadow DOM элементов.

Для добавления Shadow DOM в хост элемент, мы должны использовать метод attachShadow().

const shadowEl = document.querySelector(".shadow-host");
const shadow = shadowEl.attachShadow({mode: 'open'});

Эти инструкции создадут пустой корневой Shadow элемент (Shadow root). Shadow root это стартовая точка для новых элементов Shadow DOM, в таком же отношении как <html> является стартовой точкой для всех HTML элементов страницы в целом. Мы можем видеть наш Shadow root элемент как #shadow-root в инспекторе объектов DevTools.

shadow DOM root element

Хотя изначальное содержимое Shadow Host - простые HTML элементы, мы можем увидеть в веб-инспекторе, в браузере уже будет отображаться новое содержимое из Shadow DOM.

Следующее что мы должны сделать, это заполнить содержимым новое дерево (Shadow tree). Shadow tree это то же самое что и DOM tree, только для Shadow DOM вместо обычного DOM на странице. Теперь же, для того, чтобы создать кнопку follow, все что нам нужно это добавить к Shadow DOM <a> элемент, точно такой же как созданный ранее, только с иконкой внутри.

const link = document.createElement("a");
link.href = shadowEl.querySelector("a").href;
link.innerHTML = `
    <span aria-label="Twitter icon"></span> 
    ${shadowEl.querySelector("a").textContent}
`;

Мы добавляем этот новый элемент к shadow DOM точно так же как, как мы добавляем любой другой дочерний по отношению к родительскому элементу на странице через метод appendChild().

shadow.appendChild(link);

На текущем этапе наш элемент выглядит так:

twitter button

Наконец, мы можем добавить стили, если создадим <style> тег и так же добавим его к shadow root, как и остальные элементы.

const styles = document.createElement("style");
styles.textContent = `
a, span {
  vertical-align: top;
  display: inline-block;
  box-sizing: border-box;
}

a {
    height: 20px;
    padding: 1px 8px 1px 6px;
    background-color: #1b95e0;
    color: #fff;
    border-radius: 3px;
    font-weight: 500;
    font-size: 11px;
    font-family:'Helvetica Neue', Arial, sans-serif;
    line-height: 18px;
    text-decoration: none;   
}

a:hover {  background-color: #0c7abf; }

span {
    position: relative;
    top: 2px;
    width: 14px;
    height: 14px;
    margin-right: 3px;
    background: transparent 0 0 no-repeat;
    background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%3Cpath%20fill%3D%22none%22%20d%3D%22M0%200h72v72H0z%22%2F%3E%3Cpath%20class%3D%22icon%22%20fill%3D%22%23fff%22%20d%3D%22M68.812%2015.14c-2.348%201.04-4.87%201.744-7.52%202.06%202.704-1.62%204.78-4.186%205.757-7.243-2.53%201.5-5.33%202.592-8.314%203.176C56.35%2010.59%2052.948%209%2049.182%209c-7.23%200-13.092%205.86-13.092%2013.093%200%201.026.118%202.02.338%202.98C25.543%2024.527%2015.9%2019.318%209.44%2011.396c-1.125%201.936-1.77%204.184-1.77%206.58%200%204.543%202.312%208.552%205.824%2010.9-2.146-.07-4.165-.658-5.93-1.64-.002.056-.002.11-.002.163%200%206.345%204.513%2011.638%2010.504%2012.84-1.1.298-2.256.457-3.45.457-.845%200-1.666-.078-2.464-.23%201.667%205.2%206.5%208.985%2012.23%209.09-4.482%203.51-10.13%205.605-16.26%205.605-1.055%200-2.096-.06-3.122-.184%205.794%203.717%2012.676%205.882%2020.067%205.882%2024.083%200%2037.25-19.95%2037.25-37.25%200-.565-.013-1.133-.038-1.693%202.558-1.847%204.778-4.15%206.532-6.774z%22%2F%3E%3C%2Fsvg%3E);
}
`;

shadow.appendChild(styles);

DOM против Shadow DOM

В некотором отношении, Shadow DOM это облегченная версия обычного DOM. Так же как и DOM, это объектное представление HTML элементов на странице, помогающее определить что будет отрисовано в конце и дающее возможность разработчику модифицировать элементы на странице. НО, в отличие от DOM, не базируется на целом самостоятельном документе. Shadow DOM, как видно из имени, всегда базируется на уже имеющемся в основном DOM готовом элементе. Без основного родительского DOM, Shadow DOM, не существует.

Данный текст это мой вольный перевод оригинальной статьи Ire Aderinokun - What is the Shadow DOM?