Shadow root – это инкапсулированное поддерево DOM, прикрепленное к элементу хоста. Стили внутри него не выходят за его пределы, а стили страницы не проникают внутрь (за исключением унаследованных свойств). Шаблоны позволяют эффективно создавать дерево, а слоты – проецировать в него контент Light DOM.
Прикрепление корня теневого дерева
this.attachShadow({ mode: 'open', delegatesFocus: true });
delegatesFocus: true позволяет перемещать фокус на первый элемент внутри теневого дерева, направляет стили :focus на хост, а в связанных с формой пользовательских элементах (static formAssociated = true) перенаправляет клики <label> на внутренний элемент управления. Устанавливайте его, когда компонент содержит элементы управления, на которые можно перевести фокус. Почти во всех случаях используйте mode: 'open', чтобы инструменты разработчика, скрипты страниц и гидратация сервера могли достигать element.shadowRoot (см. Web Components Orientation).
Шаблоны: эффективное построение дерева
Определите <template> один раз (область действия модуля или статическое поле) и клонируйте его для каждого экземпляра. Клонирование обработанного фрагмента происходит намного быстрее, чем повторная обработка строки innerHTML при каждом создании.
const template = document.createElement('template');
template.innerHTML = `
<div class="card">
<slot name="header"></slot>
<div class="body"><slot></slot></div>
<slot name="footer"></slot>
</div>
`;
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// MANDATORY: deep-clone the template content so each instance gets its own nodes.
this.shadowRoot.append(template.content.cloneNode(true));
}
}
customElements.define('my-card', MyCard);
Не перегружайте шаблоны. Глубоко вложенные элементы div и многоуровневые вложенные теневые корни увеличивают время, необходимое для разрешения стилей и компоновки макета, что значительно снижает производительность. Используйте плоскую структуру и семантические элементы.
Слоты: то, что удивляет людей
Стандартные и именованные слоты работают так, как описано в документации. Вот что может вызвать проблемы:
- Пробелы учитываются. Текстовые узлы между элементами (включая пробелы) назначаются слоту по умолчанию, поэтому он редко бывает по-настоящему пустым.
- Сохраняется порядок DOM, а не порядок рекламных мест. Проецируемые узлы сохраняют исходный порядок Light DOM для
:first-child/:last-childнезависимо от того, какой слот они заполняют. Правило::slotted(*:first-child)применяется к первому дочернему элементу DOM, а не к первому дочернему элементу этого рекламного места, поэтому результат часто не соответствует ожиданиям. slotchange– сигнал о времени обновления. Оно срабатывает, когда назначенные узлы меняются (в том числе при первоначальном назначении). Это надежный способ узнать, что дочерние элементы Light DOM появились после обновления, когда они могли не существовать во времяconnectedCallback.
this.shadowRoot.querySelector('slot[name="header"]')
.addEventListener('slotchange', (e) => {
const nodes = e.target.assignedElements();
this.toggleAttribute('has-header', nodes.length > 0);
});
Загрузка таблиц стилей в теневой корень
| Метод | Преимущества | Недостатки |
|---|---|---|
<style> в шаблоне |
Простой, полностью инкапсулированный, работает без JS. | Дублируется для каждого экземпляра, что приводит к раздуванию DOM. |
<link rel="stylesheet"> |
Кешируемый внешний файл. | Дополнительный запрос; риск появления нестилизованного контента. |
adoptedStyleSheets |
Один общий CSSStyleSheet для всех экземпляров; самый быстрый при масштабировании. |
Требуется JS; созданные таблицы не поддерживают @import. |
Если у вас много экземпляров одного компонента, лучше использовать общую таблицу стилей:
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// The same sheet object is reused by every instance; no per-instance parse.
this.shadowRoot.adoptedStyleSheets = [sheet];
}
}