Декларативный теневой DOM

Declarative Shadow DOM — это стандартная функция веб-платформы , которая поддерживается в Chrome с версии 90. Обратите внимание, что спецификация этой функции изменилась в 2023 году (включая shadowroot shadowrootmode ), и наиболее актуальные стандартизированные версии всех части этой функции появились в Chrome версии 124.

Browser Support

  • Хром: 111.
  • Край: 111.
  • Фаерфокс: 123.
  • Сафари: 16.4.

Source

Shadow DOM — это один из трех стандартов веб-компонентов, дополненный шаблонами HTML и пользовательскими элементами . Shadow DOM предоставляет возможность ограничить стили CSS определенным поддеревом DOM и изолировать это поддерево от остальной части документа. Элемент <slot> дает нам возможность контролировать, где дочерние элементы пользовательского элемента должны быть вставлены в его теневое дерево. Сочетание этих функций позволяет создать систему для создания автономных, повторно используемых компонентов, которые легко интегрируются в существующие приложения, как встроенный элемент HTML.

До сих пор единственным способом использования Shadow DOM было создание теневого корня с помощью JavaScript:

const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';

Подобный императивный API отлично работает для рендеринга на стороне клиента: те же модули JavaScript, которые определяют наши пользовательские элементы, также создают их теневые корни и устанавливают их содержимое. Однако многим веб-приложениям необходимо отображать контент на стороне сервера или в статический HTML во время сборки. Это может быть важной частью обеспечения приемлемого опыта для посетителей, которые не умеют использовать JavaScript.

Обоснования использования серверного рендеринга (SSR) варьируются от проекта к проекту. Некоторые веб-сайты должны предоставлять полнофункциональный HTML-код, отображаемый на сервере, чтобы соответствовать рекомендациям по доступности, другие предпочитают предоставлять базовый интерфейс без JavaScript как способ обеспечить хорошую производительность на медленных соединениях или устройствах.

Исторически сложилось так, что было сложно использовать Shadow DOM в сочетании с серверным рендерингом, поскольку не было встроенного способа выражения теневых корней в HTML, сгенерированном сервером. Прикрепление теневых корней к элементам DOM, которые уже были отображены без них, также влияет на производительность. Это может привести к смещению макета после загрузки страницы или к временному появлению вспышки нестилизованного контента («FOUC») при загрузке таблиц стилей Shadow Root.

Декларативный Shadow DOM (DSD) устраняет это ограничение, перенося Shadow DOM на сервер.

Как построить декларативный теневой корень

Декларативный теневой корень — это элемент <template> с shadowrootmode :

<host-element>
  <template shadowrootmode="open">
    <slot></slot>
  </template>
  <h2>Light content</h2>
</host-element>

Элемент шаблона с shadowrootmode обнаруживается анализатором HTML и немедленно применяется в качестве теневого корня его родительского элемента. Загрузка чистой HTML-разметки из приведенного выше примера приводит к следующему дереву DOM:

<host-element>
  #shadow-root (open)
  <slot>
    ↳
    <h2>Light content</h2>
  </slot>
</host-element>

Этот пример кода соответствует соглашениям панели элементов Chrome DevTools для отображения содержимого Shadow DOM. Например, символ представляет содержимое Light DOM с слотами.

Это дает нам преимущества инкапсуляции и проекции слотов Shadow DOM в статическом HTML. Для создания всего дерева, включая теневой корень, не требуется JavaScript.

Гидратация компонентов

Декларативный Shadow DOM можно использовать отдельно как способ инкапсуляции стилей или настройки размещения дочерних элементов, но он наиболее эффективен при использовании с пользовательскими элементами. Компоненты, созданные с использованием пользовательских элементов, автоматически обновляются из статического HTML. С появлением декларативного теневого DOM пользовательский элемент теперь может иметь теневой корень до его обновления.

Пользовательский элемент, обновляемый из HTML, который включает в себя декларативный теневой корень, уже будет иметь этот теневой корень. Это означает, что элемент будет иметь shadowRoot уже доступное при его создании, без явного создания вашего кода. Лучше всего проверить this.shadowRoot на наличие любого существующего теневого корня в конструкторе вашего элемента. Если значение уже существует, HTML для этого компонента включает декларативный теневой корень. Если значение равно нулю, в HTML не было декларативного теневого корня или браузер не поддерживает декларативную теневую модель DOM.

<menu-toggle>
  <template shadowrootmode="open">
    <button>
      <slot></slot>
    </button>
  </template>
  Open Menu
</menu-toggle>
<script>
  class MenuToggle extends HTMLElement {
    constructor() {
      super();

      // Detect whether we have SSR content already:
      if (this.shadowRoot) {
        // A Declarative Shadow Root exists!
        // wire up event listeners, references, etc.:
        const button = this.shadowRoot.firstElementChild;
        button.addEventListener('click', toggle);
      } else {
        // A Declarative Shadow Root doesn't exist.
        // Create a new shadow root and populate it:
        const shadow = this.attachShadow({mode: 'open'});
        shadow.innerHTML = `<button><slot></slot></button>`;
        shadow.firstChild.addEventListener('click', toggle);
      }
    }
  }

  customElements.define('menu-toggle', MenuToggle);
</script>

Пользовательские элементы существуют уже некоторое время, и до сих пор не было причин проверять существующий теневой корень перед его созданием с помощью attachShadow() . В Declarative Shadow DOM есть небольшое изменение, которое позволяет существующим компонентам работать, несмотря на это: вызов метода attachShadow() для элемента с существующим Declarative Shadow Root не будет вызывать ошибку. Вместо этого декларативный теневой корень очищается и возвращается. Это позволяет старым компонентам, не созданным для Declarative Shadow DOM, продолжать работу, поскольку декларативные корни сохраняются до тех пор, пока не будет создана императивная замена.

Для вновь созданных пользовательских элементов новое свойство ElementInternals.shadowRoot предоставляет явный способ получить ссылку на существующий декларативный теневой корень элемента, как открытый, так и закрытый. Это можно использовать для проверки и использования любого декларативного теневого корня, при этом возвращаясь к attachShadow() в тех случаях, когда он не был предоставлен.

class MenuToggle extends HTMLElement {
  constructor() {
    super();

    const internals = this.attachInternals();

    // check for a Declarative Shadow Root:
    let shadow = internals.shadowRoot;

    if (!shadow) {
      // there wasn't one. create a new Shadow Root:
      shadow = this.attachShadow({
        mode: 'open'
      });
      shadow.innerHTML = `<button><slot></slot></button>`;
    }

    // in either case, wire up our event listener:
    shadow.firstChild.addEventListener('click', toggle);
  }
}

customElements.define('menu-toggle', MenuToggle);

Одна тень на корень

Декларативный теневой корень связан только со своим родительским элементом. Это означает, что теневые корни всегда располагаются рядом со связанным с ними элементом. Такое дизайнерское решение обеспечивает потоковую передачу теневых корней, как и остальную часть HTML-документа. Это также удобно для разработки и создания, поскольку добавление теневого корня к элементу не требует ведения реестра существующих теневых корней.

Компромисс связывания теневых корней с их родительским элементом заключается в том, что невозможно инициализировать несколько элементов из одного и того же декларативного теневого корня <template> . Однако в большинстве случаев, когда используется декларативный теневой DOM, это вряд ли будет иметь значение, поскольку содержимое каждого теневого корня редко бывает идентичным. Хотя HTML, отображаемый на сервере, часто содержит повторяющиеся структуры элементов, их содержимое обычно различается, например, небольшими вариациями в тексте или атрибутах. Поскольку содержимое сериализованного декларативного теневого корня полностью статично, обновление нескольких элементов из одного декларативного теневого корня будет работать только в том случае, если элементы оказались идентичными. Наконец, влияние повторяющихся одинаковых теневых корней на размер сетевой передачи относительно невелико из-за эффектов сжатия.

В будущем, возможно, появится возможность вернуться к общим теневым корням. Если DOM получит поддержку встроенных шаблонов , декларативные теневые корни можно будет рассматривать как шаблоны, экземпляры которых создаются для создания теневого корня для данного элемента. Текущая конструкция Declarative Shadow DOM допускает такую ​​возможность в будущем, ограничивая ассоциацию теневого корня одним элементом.

Стриминг - это круто

Связывание декларативных теневых корней непосредственно с их родительским элементом упрощает процесс обновления и присоединения их к этому элементу. Декларативные теневые корни обнаруживаются во время анализа HTML и прикрепляются немедленно при обнаружении открывающего тега <template> . Анализируемый HTML внутри <template> анализируется непосредственно в теневом корне, поэтому его можно «передавать в потоковом режиме»: визуализировать по мере его получения.

<div id="el">
  <script>
    el.shadowRoot; // null
  </script>

  <template shadowrootmode="open">
    <!-- shadow realm -->
  </template>

  <script>
    el.shadowRoot; // ShadowRoot
  </script>
</div>

Только парсер

Декларативный Shadow DOM — это функция парсера HTML. Это означает, что декларативный теневой корень будет анализироваться и присоединяться только к тегам <template> с shadowrootmode , которые присутствуют во время анализа HTML. Другими словами, декларативные теневые корни могут быть созданы во время первоначального анализа HTML:

<some-element>
  <template shadowrootmode="open">
    shadow root content for some-element
  </template>
</some-element>

Установка shadowrootmode элемента <template> ничего не дает, и шаблон остается обычным элементом шаблона:

const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null

Чтобы избежать некоторых важных соображений безопасности, декларативные теневые корни также нельзя создавать с помощью API анализа фрагментов, таких как innerHTML или insertAdjacentHTML() . Единственный способ проанализировать HTML с применением декларативных теневых корней — использовать setHTMLUnsafe() или parseHTMLUnsafe() :

<script>
  const html = `
    <div>
      <template shadowrootmode="open"></template>
    </div>
  `;
  const div = document.createElement('div');
  div.innerHTML = html; // No shadow root here
  div.setHTMLUnsafe(html); // Shadow roots included
  const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>

Серверный рендеринг со стилем

Встроенные и внешние таблицы стилей полностью поддерживаются внутри декларативных теневых корней с использованием стандартных тегов <style> и <link> :

<nineties-button>
  <template shadowrootmode="open">
    <style>
      button {
        color: seagreen;
      }
    </style>
    <link rel="stylesheet" href="/comicsans.css" />
    <button>
      <slot></slot>
    </button>
  </template>
  I'm Blue
</nineties-button>

Стили, заданные таким образом, также хорошо оптимизированы: если одна и та же таблица стилей присутствует в нескольких декларативных теневых корнях, она загружается и анализируется только один раз. Браузер использует единый вспомогательный CSSStyleSheet , который используется всеми теневыми корнями, что исключает дублирование накладных расходов на память.

Конструируемые таблицы стилей не поддерживаются в декларативном теневом DOM. Это связано с тем, что в настоящее время нет возможности сериализовать конструируемые таблицы стилей в HTML, а также нет возможности ссылаться на них при заполнении adoptedStyleSheets .

Как избежать мелькания нестилизованного контента

Одной из потенциальных проблем в браузерах, которые еще не поддерживают Declarative Shadow DOM, является избежание «вспышки нестилизованного контента» (FOUC), когда необработанное содержимое отображается для пользовательских элементов, которые еще не были обновлены. До декларативного теневого DOM одним из распространенных способов избежать FOUC было применение правила стиля display:none к пользовательским элементам, которые еще не были загружены, поскольку к ним не был прикреплен и заполнен их теневой корень. Таким образом, контент не отображается, пока он не «готов»:

<style>
  x-foo:not(:defined) > * {
    display: none;
  }
</style>

С появлением Declarative Shadow DOM пользовательские элементы можно визуализировать или создавать в HTML, так что их теневое содержимое находится на месте и готово до загрузки реализации компонента на стороне клиента:

<x-foo>
  <template shadowrootmode="open">
    <style>h2 { color: blue; }</style>
    <h2>shadow content</h2>
  </template>
</x-foo>

В этом случае правило display:none «FOUC» предотвратит отображение содержимого декларативного теневого корня. Однако удаление этого правила приведет к тому, что браузеры без поддержки Declarative Shadow DOM будут отображать неправильный или нестилизованный контент до тех пор, пока полифил Declarative Shadow DOM не загрузится и не преобразует шаблон теневого корня в настоящий теневой корень.

К счастью, эту проблему можно решить в CSS, изменив правило стиля FOUC. В браузерах, поддерживающих декларативный теневой DOM, элемент <template shadowrootmode> немедленно преобразуется в теневой корень, не оставляя элемента <template> в дереве DOM. Браузеры, которые не поддерживают Declarative Shadow DOM, сохраняют элемент <template> , который мы можем использовать для предотвращения FOUC:

<style>
  x-foo:not(:defined) > template[shadowrootmode] ~ *  {
    display: none;
  }
</style>

Вместо того, чтобы скрывать еще не определенный пользовательский элемент, пересмотренное правило «FOUC» скрывает его дочерние элементы , когда они следуют за элементом <template shadowrootmode> . После определения пользовательского элемента правило больше не соответствует. Правило игнорируется в браузерах, поддерживающих Declarative Shadow DOM, поскольку дочерний элемент <template shadowrootmode> удаляется во время анализа HTML.

Обнаружение функций и поддержка браузера

Декларативный теневой DOM доступен начиная с Chrome 90 и Edge 91, но вместо стандартизированного shadowrootmode в нем использовался более старый нестандартный атрибут, называемый shadowroot . Новый shadowrootmode и поведение потоковой передачи доступны в Chrome 111 и Edge 111.

Declarative Shadow DOM, новый API веб-платформы, пока не имеет широкой поддержки во всех браузерах. Поддержку браузера можно определить, проверив наличие shadowRootMode в прототипе HTMLTemplateElement :

function supportsDeclarativeShadowDOM() {
  return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}

Полифилл

Создание упрощенного полифила для Declarative Shadow DOM относительно просто, поскольку полифилу не нужно идеально повторять семантику синхронизации или характеристики только для синтаксического анализатора, которыми занимается реализация браузера. Чтобы заполнить декларативный теневой DOM, мы можем просканировать DOM, чтобы найти все элементы <template shadowrootmode> , а затем преобразовать их в прикрепленные теневые корни в их родительском элементе. Этот процесс можно выполнить после того, как документ будет готов, или инициировать более конкретные события, например жизненные циклы пользовательских элементов.

(function attachShadowRoots(root) {
  if (supportsDeclarativeShadowDOM()) {
    // Declarative Shadow DOM is supported, no need to polyfill.
    return;
  }
  root.querySelectorAll("template[shadowrootmode]").forEach(template => {
    const mode = template.getAttribute("shadowrootmode");
    const shadowRoot = template.parentNode.attachShadow({ mode });

    shadowRoot.appendChild(template.content);
    template.remove();
    attachShadowRoots(shadowRoot);
  });
})(document);

Дальнейшее чтение