Продвинутые техники работы с DOM
Освоение продвинутых техник работы с DOM значительно улучшит ваши навыки веб-разработки и позволит создавать более динамичный, модульный и поддерживаемый код.
Освоение продвинутых техник работы с DOM поможет вам строить динамичные, модульные и поддерживаемые интерфейсы на чистом JavaScript — без необходимости использовать фреймворки. В этом руководстве рассматриваются два ключевых инструмента современной разработки компонентов: элемент <template> для объявления переиспользуемой инертной разметки и Shadow DOM для инкапсуляции структуры, стилей и поведения компонента. Вместе они составляют основу, на которой строятся веб-компоненты и пользовательские элементы.
Создание и использование шаблонов
Зачем нужны шаблоны
До появления <template> разработчики создавали переиспользуемую разметку, помещая HTML в скрытые элементы <div>, строковые литералы JavaScript или блоки <script type="text/template">. У каждого подхода есть недостатки: скрытые <div> всё равно требуют от браузера парсинга и загрузки ресурсов (изображения загружаются, скрипты выполняются), а строковые шаблоны лишены подсветки синтаксиса и легко ломаются.
Элемент <template> решает эту проблему. Его содержимое парсится, но остаётся инертным: браузер строит узлы DOM, но не отображает их, не выполняет их скрипты и не загружает изображения или медиафайлы до тех пор, пока вы явно не клонируете содержимое в живой документ. Именно поэтому <template> является правильным инструментом для объявления разметки, которую вы планируете многократно инстанцировать.
Использование элемента <template>
Элемент <template> позволяет определить HTML, который не отображается при загрузке страницы. Доступ к его содержимому осуществляется через свойство content (только для чтения), которое возвращает DocumentFragment.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Using the <template> Element</title>
</head>
<body>
<template id="my-template">
<div class="card">
<h2>Title</h2>
<p>Content goes here...</p>
</div>
</template>
<button id="show-template">Show Template</button>
<div id="content"></div>
<script>
document.getElementById('show-template').addEventListener('click', () => {
const template = document.getElementById('my-template');
const content = document.getElementById('content');
const clone = template.content.cloneNode(true);
content.appendChild(clone);
});
</script>
</body>
</html>В этом примере показана базовая структура элемента <template>, содержащего карточку с заголовком и текстом. Содержимое шаблона клонируется и вставляется в DOM при нажатии кнопки. Подробнее об этом элементе рассказано в разделе элемент <template>.
Клонирование и вставка содержимого шаблона
Чтобы переиспользовать <template>, необходимо клонировать его content и вставить клон в DOM. Всегда передавайте true в cloneNode, чтобы скопировать всё поддерево целиком (элемент и всех его потомков) — cloneNode(false) копирует только верхний узел и вернёт пустой фрагмент.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Cloning and Inserting Template Content</title>
</head>
<body>
<template id="card-template">
<div class="card">
<h2 class="title"></h2>
<p class="body"></p>
</div>
</template>
<button id="add-card">Add Card</button>
<div id="container"></div>
<script>
let count = 0;
document.getElementById('add-card').addEventListener('click', () => {
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);
// Fill the clone with dynamic data before inserting it.
count++;
clone.querySelector('.title').textContent = 'Card ' + count;
clone.querySelector('.body').textContent = 'Created at ' + new Date().toLocaleTimeString();
document.getElementById('container').appendChild(clone);
});
</script>
</body>
</html>Настоящая ценность шаблонов заключается именно в этом паттерне: клонировать, затем заполнить клон данными перед вставкой. Несколько важных деталей, которые стоит запомнить:
DocumentFragmentочищается при добавлении в DOM. ПослеappendChild(clone)дочерние элементы фрагмента перемещаются в контейнер, а сам фрагмент остаётся пустым — поэтому вызывайтеcloneNodeпо одному разу для каждого добавляемого элемента.- Выполняйте запросы к клону, а не к документу. Селекторы вида
clone.querySelector('.title')работают с фрагментом, который ещё не вставлен в DOM, поэтому вы заполняете его прежде, чем он попадёт в живой документ (что позволяет избежать лишних перерасчётов макета). Подробнее см. в разделе поиск с помощью querySelector. document.importNode(template.content, true)— это кросс-документный эквивалент; используйте его, когда шаблон находится в другом документе или iframe, чтобы импортированные узлы принадлежали текущему документу.
Shadow DOM
Введение в Shadow DOM
Shadow DOM — это веб-стандарт, обеспечивающий инкапсуляцию в веб-компонентах. Он прикрепляет к элементу (называемому shadow host) отдельное скрытое дерево DOM — shadow tree. Узлы этого дерева недоступны обычному методу document.querySelector страницы, а стили, определённые внутри него, не проникают в остальную часть страницы. Это позволяет изолировать внутреннюю структуру, стили и поведение компонента от глобального документа.
Несколько терминов, которые будут встречаться в тексте:
- Shadow host — обычный элемент, к которому прикреплено shadow tree.
- Shadow root — корневой узел shadow tree, возвращаемый методом
attachShadow(). - Shadow boundary — граница между shadow tree и остальной частью документа, которую область видимости не пересекает.
Режимы open и closed
Метод attachShadow() требует указания параметра mode:
const open = host.attachShadow({ mode: 'open' });
// host.shadowRoot → the shadow root (accessible from outside)
const closed = host2.attachShadow({ mode: 'closed' });
// host2.shadowRoot → null (the root is hidden from outside scripts)На практике предпочтительнее использовать open. Режим closed не обеспечивает реальной безопасности — кто угодно может переопределить attachShadow до выполнения вашего кода — и лишь усложняет тестирование и отладку компонента.
Инкапсуляция и компонентная разработка
Инкапсуляция гарантирует, что стили и скрипты, определённые внутри компонента, не вытекают наружу и не влияют на остальную часть документа — и что внешние стили не проникают внутрь. В примере ниже к элементу прикрепляется shadow root, а затем его содержимое формируется в DocumentFragment, чтобы всё поддерево было вставлено за одну операцию. Элемент <slot> проецирует существующее («light DOM») содержимое хоста в shadow tree вместе с собственной разметкой компонента.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Shadow DOM Example</title>
<style>
.card {
padding: 20px;
margin: 10px;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<div id="shadow-host" class="card">
<span>This is the light DOM content</span>
</div>
<script>
const host = document.getElementById('shadow-host');
const shadowRoot = host.attachShadow({ mode: 'open' });
const fragment = document.createDocumentFragment();
const style = document.createElement('style');
style.textContent = `.shadow-card { padding: 20px; margin: 10px; border: 1px solid blue; color: blue; }`;
const slot = document.createElement('slot');
const card = document.createElement('div');
card.className = 'shadow-card';
card.textContent = 'This is inside the Shadow DOM';
fragment.appendChild(style);
fragment.appendChild(slot);
fragment.appendChild(card);
shadowRoot.appendChild(fragment);
</script>
</body>
</html>В этом примере на элементе #shadow-host создаётся shadow tree, в которое внедряются стили и содержимое. Light DOM содержимое (This is the light DOM content) остаётся в хосте и отображается внутри shadow tree через элемент <slot>, появляясь рядом с shadow-содержимым, а не заменяясь им.
Обратите внимание, что инкапсуляция влияет на стили следующим образом. Правило .shadow-card находится внутри shadow tree и применяется только к узлам этого дерева; оно не может совпасть с .card на остальной странице, а правило .card страницы не может проникнуть в shadow tree. Исключением являются наследуемые свойства — color, font-family, line-height и подобные — они по-прежнему передаются через границу в slotted-содержимое. Инкапсуляция блокирует совпадение селекторов, но не наследование. Подробнее см. в разделах стилизация Shadow DOM и слоты и композиция.
Когда использовать каждую технику
- Используйте
<template>, когда нужно многократно инстанцировать одну и ту же разметку (строки списков, карточки, модальные окна) и хочется объявить её декларативно в HTML. - Используйте Shadow DOM, когда виджет требует стилей, которые не должны конфликтовать со стилями страницы-хоста — кнопка системы дизайна, выбор даты, встраиваемый виджет.
- Комбинируйте оба подхода — определяйте разметку в
<template>и клонируйте её в shadow root — чтобы строить полноценные переиспользуемые пользовательские элементы.
Лучшие практики
- Предпочитайте
DocumentFragmentдля пакетных вставок: добавление фрагмента в shadow root (или любой другой контейнер) за одну операцию минимизирует перерасчёты макета и улучшает производительность отрисовки. - Заполняйте клоны перед вставкой: выполняйте запросы и заполняйте клонированный фрагмент, пока он ещё не прикреплён к DOM — это позволит браузеру произвести только один перерасчёт макета при добавлении.
- Выбирайте режим shadow
open: это сохраняет возможность отладки и тестирования компонентов; режимclosedне обеспечивает реальной безопасности. - Используйте
document.importNode()при работе с разными документами: при клонировании содержимого из другого документа или iframe методimportNodeобеспечивает корректное владение узлами и предотвращает кросс-документные ошибки. - Минимизируйте light DOM: используйте элементы
<slot>для проецирования только того содержимого, которое действительно принадлежит странице, сохраняя хост предсказуемым.
Используйте Shadow DOM для инкапсуляции стилей и функциональности внутри компонентов, предотвращая конфликты стилей и обеспечивая модульный, поддерживаемый код.
Заключение
Продвинутые техники работы с DOM — такие как использование шаблонов и Shadow DOM — являются мощными инструментами для создания модульных, поддерживаемых и эффективных веб-приложений. Инкапсулируя стили и поведение компонентов и применяя переиспользуемые шаблоны, вы можете улучшить рабочий процесс разработки и создавать надёжные веб-приложения.