Использование теневого DOM
Важная особенность пользовательских элементов — инкапсуляция, поскольку пользовательский элемент, по определению, представляет собой фрагмент многократно используемой функциональности: его можно поместить в любую веб-страницу, и он должен работать. Поэтому важно, чтобы код, работающий на странице, не смог случайно сломать пользовательский элемент, изменяя его внутреннюю реализацию. Теневой DOM позволяет прикрепить дерево DOM к элементу и скрыть внутреннюю часть этого дерева от JavaScript и CSS, работающих на странице.
В этой статье рассматриваются основы использования теневого DOM.
Общий обзор
В этой статье предполагается, что вы уже знакомы с понятием DOM (Document Object Model) — древовидной структуры взаимосвязанных узлов, представляющей различные элементы и строки текста, отображаемые в документе разметки (обычно HTML-документе в случае веб-документов). В качестве примера рассмотрим следующий фрагмент HTML:
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>DOM example</title>
</head>
<body>
<section>
<img src="dinosaur.png" alt="A red Tyrannosaurus Rex." />
<p>
Here we will add a link to the
<a href="https://www.mozilla.org/">Mozilla homepage</a>
</p>
</section>
</body>
</html>
Этот фрагмент создает следующую структуру DOM (исключая только содержащие пробелы текстовые узлы):
- HTML
- HEAD
- META charset="utf-8"
- TITLE
- #text: DOM example
- BODY
- SECTION
- IMG src="dinosaur.png" alt="A red Tyrannosaurus Rex."
- P
- #text: Here we will add a link to the
- A href="https://www.mozilla.org/"
- #text: Mozilla homepage
Теневой DOM позволяет прикреплять скрытые деревья DOM к элементам в обычном дереве DOM — это дерево теневого DOM начинается со корневого узла, под которым вы можете прикрепить любой элемент так же, как и в обычном DOM.
Следует ознакомиться с некоторыми терминами теневого DOM:
- Хост теневого DOM: обычный узел DOM, к которому прикреплен теневой DOM.
- Дерево теневого DOM: дерево DOM внутри теневого DOM.
- Граница теневого DOM: место, где заканчивается теневой DOM и начинается обычный DOM.
- Корневой узел теневого DOM: корневой узел теневого дерева.
Вы можете воздействовать на узлы в теневом DOM точно так же, как на узлы вне теневого DOM — например, добавлять дочерние элементы или устанавливать атрибуты, стилизовать отдельные узлы с помощью element.style.foo или добавлять стили ко всему дереву теневого DOM внутри элемента <style>. Разница в том, что ни один код внутри теневого DOM не может воздействовать на что-либо снаружи, что позволяет удобно реализовать инкапсуляцию.
До того, как теневой DOM стал доступен веб-разработчикам, браузеры уже использовали его для инкапсуляции внутренней структуры элемента. Например, рассмотрите элемент <video> с отображаемыми по умолчанию средствами браузера. Всё, что вы видите в DOM, это элемент <video>, но он содержит ряд кнопок и других элементов управления внутри своего теневого DOM. Спецификация теневого DOM позволяет вам управлять теневым DOM ваших собственных пользовательских элементов.
Наследование атрибутов
Создание теневого DOM
Императивное создание с помощью JavaScript
На данной странице есть два элемента: элемент <div> с id значением "host", и элемент <span> с текстом:
<div id="host"></div> <span>I'm not in the shadow DOM</span>
Мы собираемся использовать элемент "host" в качестве хоста теневого DOM. Мы вызываем метод attachShadow() на хосте для создания теневого DOM, а затем можем добавлять узлы в теневой DOM так же, как и в основной DOM. В этом примере мы добавляем один элемент <span>:
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const span = document.createElement("span");
span.textContent = "I'm in the shadow DOM";
shadow.appendChild(span);
Результат выглядит следующим образом:
Декларативное создание с помощью HTML
Создание теневого DOM с помощью JavaScript API может быть хорошим вариантом для приложений, работающих на стороне клиента. Для других приложений пользовательский интерфейс, отрисовываемый на стороне сервера, может обеспечить лучшую производительность и, следовательно, лучший пользовательский опыт. В таких случаях вы можете использовать элемент <template> для декларативного определения теневого DOM. Ключом к этому поведению является атрибут shadowrootmode, который может быть установлен в значение open или closed, совпадающие со значениями параметра mode метода attachShadow().
<div id="host">
<template shadowrootmode="open">
<span>I'm in the shadow DOM</span>
</template>
</div>
Примечание: По умолчанию содержимое <template> не отображается. В данном случае, поскольку был включён shadowrootmode="open", теневой корень отображается. В поддерживающих браузерах отображается видимое содержимое внутри этого теневого корня.
После того, как браузер обработает HTML, он заменит элемент <template> его содержимым, заключённым в теневой корень, который прикреплён к родительскому элементу, <div id="host"> в нашем примере. Результирующее дерево DOM выглядит следующим образом (элемента <template> в дереве DOM нет):
- DIV id="host"
- #shadow-root
- SPAN
- #text: I'm in the shadow DOM
Обратите внимание, что помимо shadowrootmode, вы можете использовать атрибуты <template>, такие как shadowrootclonable и shadowrootdelegatesfocus, для указания других свойств сгенерированного теневого корня.
Инкапсуляция от JavaScript
Пока это может показаться не таким уж и существенным. Но давайте посмотрим, что произойдёт, если код, работающий на странице, попытается получить доступ к элементам в теневом DOM.
Эта страница такая же, как предыдущая, за исключением добавления двух элементов <button>.
<div id="host"></div> <span>I'm not in the shadow DOM</span> <br /> <button id="upper" type="button">Uppercase span elements</button> <button id="reload" type="button">Reload</button>
Нажатие кнопки "Преобразовать span-элементы в верхний регистр" ищет все элементы <span> на странице и изменяет их текст на верхний регистр. Нажатие кнопки "Перезагрузить" просто перезагружает страницу, чтобы вы могли повторить попытку.
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const span = document.createElement("span");
span.textContent = "I'm in the shadow DOM";
shadow.appendChild(span);
const upper = document.querySelector("button#upper");
upper.addEventListener("click", () => {
const spans = Array.from(document.querySelectorAll("span"));
for (const span of spans) {
span.textContent = span.textContent.toUpperCase();
}
});
const reload = document.querySelector("#reload");
reload.addEventListener("click", () => document.location.reload());
Если вы нажмёте "Преобразовать span-элементы в верхний регистр", вы увидите, что Document.querySelectorAll() не находит элементы в нашем теневом DOM: они фактически скрыты от JavaScript на странице:
Element.shadowRoot и параметр "mode"
В примере выше мы передаём аргумент { mode: "open" } методу attachShadow(). При mode равном "open", JavaScript на странице может получить доступ к внутренностям вашего теневого DOM через свойство shadowRoot хоста теневого DOM.
В этом примере, как и раньше, HTML содержит хост теневого DOM, элемент <span> в главном дереве DOM и две кнопки:
<div id="host"></div> <span>I'm not in the shadow DOM</span> <br /> <button id="upper" type="button">Uppercase shadow DOM span elements</button> <button id="reload" type="button">Reload</button>
На этот раз кнопка "Преобразовать в верхний регистр" использует shadowRoot для поиска элементов <span> в DOM:
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const span = document.createElement("span");
span.textContent = "I'm in the shadow DOM";
shadow.appendChild(span);
const upper = document.querySelector("button#upper");
upper.addEventListener("click", () => {
const spans = Array.from(host.shadowRoot.querySelectorAll("span"));
for (const span of spans) {
span.textContent = span.textContent.toUpperCase();
}
});
const reload = document.querySelector("#reload");
reload.addEventListener("click", () => document.location.reload());
На этот раз JavaScript на странице может получить доступ к внутренностям теневого DOM:
Аргумент {mode: "open"} даёт странице возможность нарушить инкапсуляцию вашего теневого DOM. Если вы не хотите предоставлять страницу эту возможность, передайте {mode: "closed"} вместо этого, и тогда shadowRoot вернёт null.
Однако, не стоит рассматривать это как надёжный механизм безопасности, так как есть способы его обойти, например, с помощью расширений браузера, работающих на странице. Это скорее указание на то, что страница не должна получать доступ к внутренностям вашего дерева теневого DOM.
Инкапсуляция от CSS
В этой версии страницы HTML такой же, как и в оригинале:
<div id="host"></div> <span>I'm not in the shadow DOM</span>
В JavaScript мы создаём теневой DOM:
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const span = document.createElement("span");
span.textContent = "I'm in the shadow DOM";
shadow.appendChild(span);
На этот раз у нас будет CSS, нацеленный на элементы <span> на странице:
span {
color: blue;
border: 1px solid black;
}
CSS страницы не влияет на узлы внутри теневого DOM:
Применение стилей внутри теневого DOM
В этом разделе мы рассмотрим два разных способа применения стилей внутри дерева тенистого DOM:
-
Программно, создав объект
CSSStyleSheetи прикрепив его к корню тени. -
Декларативно, добавив элемент
<style>в объявление элемента<template>.
В обоих случаях стили, определенные в дереве тенистого DOM, ограничены этим деревом, поэтому, как стили страницы не влияют на элементы в тенистом DOM, так и стили тенистого DOM не влияют на элементы в остальной части страницы.
Конструируемые таблицы стилей
Для стилизации элементов страницы в тенистом DOM с помощью конструируемых таблиц стилей, мы можем:
- Создать пустой объект
CSSStyleSheet - Установить его содержимое, используя
CSSStyleSheet.replace()илиCSSStyleSheet.replaceSync() - Добавить его в корень тени, присвоив его свойству
ShadowRoot.adoptedStyleSheets
Правила, определенные в CSSStyleSheet будут ограничены деревом тенистого DOM, а также любыми другими деревьями DOM, которым мы его присвоили.
Вот, снова, HTML, содержащий наш хост и <span>:
<div id="host"></div> <span>I'm not in the shadow DOM</span>
На этот раз мы создадим тенистый DOM и присвоим ему объект CSSStyleSheet:
const sheet = new CSSStyleSheet();
sheet.replaceSync("span { color: red; border: 2px dotted black;}");
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
shadow.adoptedStyleSheets = [sheet];
const span = document.createElement("span");
span.textContent = "I'm in the shadow DOM";
shadow.appendChild(span);
Стили, определенные в дереве тенистого DOM, не применяются в остальной части страницы:
Добавление <style> элементов в <template> объявления
Альтернативой созданию объектов CSSStyleSheet является вставка элемента <style> внутри элемента <template>, используемого для определения веб-компонента.
В этом случае HTML включает объявление <template>
<template id="my-element">
<style>
span {
color: red;
border: 2px dotted black;
}
</style>
<span>I'm in the shadow DOM</span>
</template>
<div id="host"></div>
<span>I'm not in the shadow DOM</span>
В JavaScript мы создадим тенистый DOM и добавим содержимое <template> в него:
const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });
const template = document.getElementById("my-element");
shadow.appendChild(template.content);
Опять же, стили, определенные в <template> применяются только внутри дерева тенистого DOM, а не в остальной части страницы:
Выбор между программными и декларативными вариантами
Выбор между этими вариантами зависит от вашего приложения и личных предпочтений.
Создание CSSStyleSheet и присвоение его корню тени с помощью adoptedStyleSheets позволяет создать одну таблицу стилей и использовать ее в нескольких деревьях DOM. Например, библиотека компонентов может создать одну таблицу стилей и затем использовать ее для всех пользовательских элементов, принадлежащих этой библиотеке. Браузер будет парсить эту таблицу стилей один раз. Кроме того, вы можете внести динамические изменения в таблицу стилей, и они будут применяться ко всем компонентам, использующим эту таблицу.
Подход к добавлению элемента <style> хорош, если вы хотите использовать декларативный подход, иметь немного стилей и не нужно делиться стилями между различными компонентами.
Тенистый DOM и пользовательские элементы
Без инкапсуляции, предоставляемой тенистым DOM, пользовательские элементы были бы чрезвычайно уязвимы. Было бы слишком легко случайно нарушить поведение или макет пользовательского элемента, выполнив какой-либо JavaScript или CSS страницы. Как разработчик пользовательского элемента, вы никогда не знаете, могут ли селекторы, применимые внутри вашего пользовательского элемента, конфликтовать с теми, которые применяются на странице, которая решила использовать ваш пользовательский элемент.
Пользовательские элементы реализуются как класс, который расширяет базовый HTMLElement или встроенный элемент HTML, такой как HTMLParagraphElement. Обычно сам пользовательский элемент является хостом тенистого DOM, и элемент создает несколько элементов под этим корнем, чтобы предоставить внутреннюю реализацию элемента.
В примере ниже создается пользовательский элемент <filled-circle>, который просто отображает круг, заполненный сплошным цветом.
class FilledCircle extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
// Create a shadow root
// The custom element itself is the shadow host
const shadow = this.attachShadow({ mode: "open" });
// create the internal implementation
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const circle = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle",
);
circle.setAttribute("cx", "50");
circle.setAttribute("cy", "50");
circle.setAttribute("r", "50");
circle.setAttribute("fill", this.getAttribute("color"));
svg.appendChild(circle);
shadow.appendChild(svg);
}
}
customElements.define("filled-circle", FilledCircle);
<filled-circle color="blue"></filled-circle>
Для получения дополнительных примеров, иллюстрирующих различные аспекты реализации пользовательских элементов, см. наше руководство по пользовательским элементам.
См. также
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM