Перейти к содержимому

JavaScript Shadow DOM

Теневой DOM — ключевая возможность Web Components, позволяющая разработчикам создавать инкапсулированные DOM-деревья и области стилей. Это руководство предназначено для глубокого понимания Shadow DOM, а также содержит практические примеры кода, демонстрирующие его использование.

Что такое Shadow DOM?

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


html
<head>
  <style>
    .shadow-box {
      padding: 10px;
      border: 1px solid #000;
      background-color: lightcoral;
      color: white;
    }
  </style>
</head>
<body>
  <div class="shadow-box">This is styled by the main document</div>
  <div id="host"></div>
  <script>
    // Create a shadow root
    const hostElement = document.getElementById('host');
    const shadowRoot = hostElement.attachShadow({ mode: 'open' });

    // Attach shadow DOM content
    shadowRoot.innerHTML = `
      <style>
        .shadow-box {
          padding: 10px;
          border: 1px solid #000;
          background-color: lightblue;
          color: black;
        }
      </style>
      <div class="shadow-box">Hello, Shadow DOM!</div>
    `;
  </script>
</body>

В этом примере есть два элемента с одинаковым именем класса shadow-box. Первый элемент стилизуется CSS основного документа, а второй — CSS Shadow DOM. Как видите, стили, определённые в Shadow DOM, не влияют на элементы в основном документе и наоборот. Это демонстрирует инкапсуляцию, которую обеспечивает Shadow DOM, позволяя создавать изолированные и повторно используемые компоненты без опасений по поводу конфликтов стилей.

Создание Shadow Root

Чтобы создать shadow root, используйте метод attachShadow для элемента. Shadow root может быть либо open, либо closed. Открытый shadow root доступен из JavaScript вне shadow tree, тогда как закрытый shadow root — нет.

Открытый Shadow Root

Открытый shadow root позволяет доступ и изменение из внешнего JavaScript. В примере ниже мы изменяем текстовое содержимое внутри shadow root после его создания.


html
<body>
  <div id="open-shadow-host"></div>
  <button id="open-shadow-btn">Change Shadow Content</button>

  <script>
    const openShadowHost = document.getElementById('open-shadow-host');
    const openShadowRoot = openShadowHost.attachShadow({ mode: 'open' });

    openShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: blue;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is an open shadow root</div>
    `;

    document.getElementById('open-shadow-btn').addEventListener('click', () => {
      openShadowRoot.querySelector('.shadow-content').textContent = 'Open Shadow Root content updated!';
    });
  </script>
</body>

В этом примере предусмотрена кнопка для изменения содержимого shadow DOM. Поскольку shadow root открыт, мы можем получать доступ к его содержимому и изменять его из основного документа.

Закрытый Shadow Root

Закрытый shadow root ограничивает доступ из внешних скриптов, обеспечивая лучшую инкапсуляцию. В примере ниже мы пытаемся изменить текстовое содержимое внутри shadow root после его создания, но это невозможно, поскольку он closed.


html
<body>
  <div id="closed-shadow-host"></div>
  <button id="closed-shadow-btn">Try to Change Shadow Content</button>

  <script>
    const closedShadowHost = document.getElementById('closed-shadow-host');
    const closedShadowRoot = closedShadowHost.attachShadow({ mode: 'closed' });

    closedShadowRoot.innerHTML = `
      <style>
        .shadow-content {
          color: red;
          padding: 10px;
          border: 1px solid black;
        }
      </style>
      <div class="shadow-content">This is a closed shadow root</div>
    `;

    // closedShadowHost.shadowRoot is null for closed roots, so this throws a TypeError
    document.getElementById('closed-shadow-btn').addEventListener('click', () => {
      try {
        closedShadowHost.shadowRoot.querySelector('.shadow-content').textContent = 'Attempted to update closed shadow root!';
      } catch (e) {
        alert('Cannot access shadow root content from outside!');
      }
    });
  </script>
</body>

В этом примере попытка получить доступ к содержимому shadow DOM и изменить его завершается неудачей, потому что shadow root закрыт. Обратите внимание, что для закрытых shadow root свойство shadowRoot у host-элемента равно null, из-за чего вызов null.querySelector вызывает TypeError. Это демонстрирует, как закрытые shadow root обеспечивают лучшую инкапсуляцию, ограничивая доступ. Примечание: закрытые shadow root поддерживаются во всех современных браузерах, но они намеренно скрывают root от внешнего JavaScript, чтобы обеспечить инкапсуляцию. Используйте их, когда хотите предотвратить случайный внешний доступ.

Стилизация внутри Shadow DOM

WARNING

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

Стили, определённые внутри shadow root, не влияют на элементы вне его, и наоборот. Такая инкапсуляция полезна для создания повторно используемых компонентов.


html
<head>
  <style>
    .styled-box {
      color: red;
      background-color: yellow;
      padding: 10px;
      border: 1px solid green;
    }
  </style>
</head>
<body>
  <div class="styled-box">This is styled by the main document</div>
  <div id="styled-host"></div>

  <script>
    const styledHost = document.getElementById('styled-host');
    const shadowRoot = styledHost.attachShadow({ mode: 'open' });

    shadowRoot.innerHTML = `
      <style>
        .styled-box {
          color: white;
          background-color: black;
          padding: 10px;
          border-radius: 5px;
        }
      </style>
      <div class="styled-box">Styled by Shadow DOM</div>
    `;
  </script>
</body>

В этом примере есть два элемента с именем класса styled-box. Первый элемент стилизуется CSS основного документа, а второй — CSS Shadow DOM. Стили, определённые в Shadow DOM, не влияют на элементы в основном документе, а стили, определённые в основном документе, не влияют на элементы в Shadow DOM. Это демонстрирует, как Shadow DOM инкапсулирует стили, обеспечивая отсутствие конфликтов между стилями компонента и глобальными стилями.

Чтобы стилизовать элементы внутри Shadow DOM снаружи, используйте CSS Shadow Parts (::part()) и slotted pseudo-elements (::slotted()). Они позволяют внешнему CSS нацеливаться на определённые внутренние части или проецируемое содержимое, не нарушая инкапсуляцию.

Slotting: Light DOM Content in Shadow DOM

Слоты позволяют разработчикам передавать содержимое light DOM (обычного DOM) в shadow DOM, делая Shadow DOM более гибким и повторно используемым.


html
<div id="slot-host">
  <span slot="title">Shadow DOM Slot Example</span>
</div>

<script>
  const slotHost = document.getElementById('slot-host');
  const shadowRoot = slotHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <style>
      .container {
        border: 1px solid #ccc;
        padding: 10px;
      }
    </style>
    <div class="container">
      <h1><slot name="title"></slot></h1>
      <p>This is a Shadow DOM component with a slot for the title.</p>
    </div>
  `;
</script>

В этом примере элемент <slot> используется для передачи содержимого из light DOM в shadow DOM. Атрибут slot у элемента span совпадает со значением атрибута name у элемента slot в shadow DOM, что позволяет проецировать содержимое span в shadow DOM.

Взаимодействие JavaScript с Shadow DOM

Взаимодействие с Shadow DOM через JavaScript требует понимания границ инкапсуляции. Прямое изменение внутри shadow root несложно, но внешнее взаимодействие требует аккуратного обращения.

Доступ к элементам Shadow DOM

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


html
<div id="interactive-host"></div>

<script>
  const interactiveHost = document.getElementById('interactive-host');
  const shadowRoot = interactiveHost.attachShadow({ mode: 'open' });

  shadowRoot.innerHTML = `
    <button id="shadow-btn">Click me</button>
  `;

  const shadowButton = shadowRoot.querySelector('#shadow-btn');
  shadowButton.addEventListener('click', () => {
    alert('Button inside Shadow DOM clicked!');
  });
</script>

В этом примере мы получаем доступ к кнопке внутри shadow DOM с помощью querySelector на shadow root. Поскольку shadow root открыт, мы можем навешивать обработчики событий и напрямую изменять элементы из основного документа.

Практические примеры Shadow DOM

Создание повторно используемого Web Component

Создание повторно используемого web component с использованием Shadow DOM включает определение пользовательского элемента и присоединение к нему shadow root.


html
<body>
  <custom-card title="Hello World"></custom-card>

  <script>
    class CustomCard extends HTMLElement {
      constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
          <style>
            .card {
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 5px;
              box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            }
            .card-title {
              font-size: 1.2em;
              margin-bottom: 5px;
            }
          </style>
          <div class="card">
            <div class="card-title">${this.getAttribute('title')}</div>
            <div class="card-content"><slot></slot></div>
          </div>
        `;
      }
    }

    customElements.define('custom-card', CustomCard);
  </script>
</body>

В этом примере создаётся пользовательский элемент &lt;custom-card&gt; с shadow DOM. Shadow DOM инкапсулирует стили и структуру компонента, делая его повторно используемым без опасений по поводу конфликтов стилей с основным документом.

Интеграция с фреймворками

Shadow DOM можно без проблем использовать с современными JavaScript-фреймворками, такими как React, Angular и Vue.

Пример React

В React можно присоединить shadow DOM к контейнерному элементу следующим образом:


html
<body>
  <div id="root"></div>
  <!-- React and ReactDOM CDN links -->
  <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <script type="text/babel">
    const { useRef, useLayoutEffect } = React;

    const CustomCard = ({ title, content }) => {
      const cardRef = useRef(null);

      useLayoutEffect(() => {
        if (cardRef.current) {
          const shadowRoot = cardRef.current.attachShadow({ mode: 'open' });

          shadowRoot.innerHTML = `
            <style>
              .card {
                padding: 10px;
                border: 1px solid #ddd;
                border-radius: 5px;
                box-shadow: 0 2px 5px rgba(0,0,0,0.2);
              }
              .card-title {
                font-size: 1.2em;
                margin-bottom: 5px;
              }
            </style>
            <div class="card">
              <div class="card-title">${title}</div>
              <div class="card-content">${content}</div>
            </div>
          `;
        }
      }, [title, content]);

      return <div ref={cardRef}></div>;
    };

    const App = () => (
      <CustomCard title="Hello World" content="This is content inside the shadow DOM.">
      </CustomCard>
    );

    const rootElement = document.getElementById('root');
    const root = ReactDOM.createRoot(rootElement);
    root.render(<App />);
  </script>
</body>

В этом примере компонент React CustomCard создаётся так, что он присоединяет shadow DOM к обычному div. Shadow DOM обеспечивает инкапсуляцию стилей и структуры компонента, предоставляя бесшовную интеграцию с React.

Заключение

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

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

Practice

Which method is used to create a shadow root in JavaScript?

Считаете ли это полезным?

Предпросмотр dual-run — сравните с маршрутами Symfony на продакшене.