Deklaratives Schatten-DOM

Eine neue Möglichkeit, Shadow DOM direkt in HTML zu implementieren und zu verwenden.

Das deklarative Shadow DOM ist eine Standard-Webplattformfunktion, die in Chrome ab Version 90 unterstützt wird. Die Spezifikation für diese Funktion hat sich 2023 geändert (einschließlich der Umbenennung von shadowroot in shadowrootmode). Die neuesten standardisierten Versionen aller Teile der Funktion sind in Chrome-Version 124 verfügbar.

Shadow DOM ist einer der drei Webkomponenten-Standards, die durch HTML-Vorlagen und Benutzerdefinierte Elemente abgerundet werden. Shadow DOM bietet eine Möglichkeit, CSS-Stile auf eine bestimmte DOM-Unterstruktur zu beschränken und diese Unterstruktur vom Rest des Dokuments zu isolieren. Mit dem <slot>-Element können Sie steuern, wo die untergeordneten Elemente eines benutzerdefinierten Elements in dessen Schattenbaum eingefügt werden sollen. Diese Kombination ermöglicht ein System zum Erstellen eigenständiger, wiederverwendbarer Komponenten, die sich wie ein integriertes HTML-Element nahtlos in vorhandene Anwendungen einbinden lassen.

Bisher konnte Shadow DOM nur durch Erstellen eines Schattenstamms mit JavaScript verwendet werden:

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

Eine solche API funktioniert gut für das clientseitige Rendering: Dieselben JavaScript-Module, mit denen unsere benutzerdefinierten Elemente definiert werden, erstellen auch ihre Schattenwurzeln und legen ihren Inhalt fest. Viele Webanwendungen müssen Inhalte jedoch zum Zeitpunkt der Erstellung serverseitig oder in statischem HTML rendern. Dies kann dazu beitragen, dass Besucher, die kein JavaScript ausführen können, eine angemessene Nutzererfahrung bieten können.

Die Begründungen für das serverseitige Rendering (SSR) variieren von Projekt zu Projekt. Einige Websites müssen voll funktionsfähigen, vom Server gerenderten HTML-Code bereitstellen, um die Richtlinien für Barrierefreiheit zu erfüllen. Andere möchten eine Baseline ohne JavaScript bereitstellen, um eine gute Leistung auch bei langsamen Verbindungen oder Geräten zu gewährleisten.

In der Vergangenheit war es schwierig, Shadow DOM in Kombination mit serverseitigem Rendering zu verwenden, da es keine integrierte Möglichkeit gab, Shadow Roots im servergenerierten HTML-Code auszudrücken. Wenn Sie Shadow Roots mit DOM-Elementen verknüpfen, die bereits ohne sie gerendert wurden, hat dies Auswirkungen auf die Leistung. Dies kann dazu führen, dass sich nach dem Laden der Seite Layoutverschiebungen ergeben oder dass beim Laden der Stylesheets des Shadow Root vorübergehend unformatierte Inhalte („FOUC“) angezeigt werden.

Beim deklarativen Schatten-DOM (DSD) wird diese Einschränkung aufgehoben und es wird das Schatten-DOM auf den Server übertragen.

Eine deklarative Schattenwurzel erstellen

Eine deklarative Schattenwurzel ist ein <template>-Element mit einem shadowrootmode-Attribut:

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

Ein Vorlagenelement mit dem Attribut shadowrootmode wird vom HTML-Parser erkannt und sofort als Schattenstamm des übergeordneten Elements angewendet. Wenn das reine HTML-Markup aus dem obigen Beispiel geladen wird, ergibt sich die folgende DOM-Struktur:

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

Dieses Codebeispiel folgt den Konventionen des Steuerfelds „Elemente“ in Chrome DevTools für die Anzeige von Shadow DOM-Inhalten. Zum Beispiel steht das Zeichen CPV für Slotting-Light-DOM-Inhalte.

Dies bringt uns die Vorteile der Kapselung und Slot-Projektion von Shadow DOM in statischem HTML. Zum Erstellen des gesamten Baums, einschließlich der Schattenwurzel, ist kein JavaScript erforderlich.

Komponenten Hydration

Ein deklaratives Shadow DOM kann allein verwendet werden, um Stile zu kapseln oder untergeordnete Placements anzupassen. Am stärksten ist es jedoch, wenn es mit benutzerdefinierten Elementen verwendet wird. Komponenten, die mit benutzerdefinierten Elementen erstellt wurden, werden automatisch aus statischem HTML-Code aktualisiert. Mit der Einführung des deklarativen Schatten-DOM ist es nun möglich, dass ein benutzerdefiniertes Element einen Schattenstamm hat, bevor es aktualisiert wird.

Bei einem benutzerdefinierten Element, das von HTML aktualisiert wird und das einen deklarativen Schattenstamm enthält, ist dieser Schattenstamm bereits angehängt. Das bedeutet, dass für das Element bereits eine shadowRoot-Eigenschaft zur Verfügung steht, wenn es instanziiert wird, ohne dass Ihr Code explizit eine erstellt. Sie sollten this.shadowRoot auf vorhandene Schattenstammelemente im Konstruktor Ihres Elements prüfen. Wenn bereits ein Wert vorhanden ist, enthält der HTML-Code für diese Komponente einen deklarativen Schattenstamm. Wenn der Wert null ist, war kein deklarativer Schattenstamm im HTML-Code vorhanden oder der Browser unterstützt kein deklaratives Schatten-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>

Benutzerdefinierte Elemente gibt es schon eine Weile und bisher gab es keinen Grund, vor dem Erstellen eines Schattenstamms mit attachShadow() nach einem vorhandenen Schattenstamm zu suchen. Das deklarative Shadow DOM umfasst eine kleine Änderung, durch die vorhandene Komponenten trotzdem funktionieren können: Wenn die Methode attachShadow() für ein Element mit einem vorhandenen deklarativen Schattenstamm aufgerufen wird, wird kein Fehler ausgelöst. Stattdessen wird die deklarierte Schattenwurzel geleert und zurückgegeben. Dadurch können ältere Komponenten, die nicht für ein deklaratives Shadow DOM erstellt wurden, weiterhin funktionieren, da deklarative Wurzeln beibehalten werden, bis eine imperative Ersetzung erstellt wird.

Bei neu erstellten benutzerdefinierten Elementen bietet die neue Eigenschaft ElementInternals.shadowRoot eine explizite Möglichkeit, einen Verweis auf den vorhandenen deklarativen Schattenstamm eines Elements (offen und geschlossen) zu erhalten. Damit kann nach jedem deklarativen Schattenwurzel gesucht und dieser verwendet werden. Falls kein Root angegeben wurde, wird auf attachShadow() zurückgegriffen.

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);

Ein Schatten pro Wurzel

Eine deklarative Schattenwurzel ist nur mit ihrem übergeordneten Element verknüpft. Dies bedeutet, dass Schattenwurzeln immer mit dem zugehörigen Element am selben Standort sind. Diese Designentscheidung stellt sicher, dass Schattenwurzeln wie der Rest eines HTML-Dokuments gestreamt werden können. Es ist auch praktisch für das Erstellen und Generieren, da für das Hinzufügen eines Schattenstamms zu einem Element keine Registry für vorhandene Schattenwurzeln erforderlich ist.

Der Nachteil, dass Schattenwurzeln mit ihrem übergeordneten Element verknüpft werden, besteht darin, dass es nicht möglich ist, mehrere Elemente von derselben deklarativen Schattenstamm-<template> aus zu initialisieren. In den meisten Fällen, in denen das deklarative Schatten-DOM verwendet wird, ist dies jedoch unwahrscheinlich, da der Inhalt der einzelnen Schattenwurzeln selten identisch ist. Während vom Server gerenderter HTML-Code häufig wiederholte Elementstrukturen enthält, unterscheiden sich ihre Inhalte in der Regel, z. B. leichte Variationen von Text oder Attributen. Da der Inhalt eines serialisierten deklarativen Schattenstamms vollständig statisch ist, funktioniert das Upgrade mehrerer Elemente von einem einzelnen deklarativen Schattenstamm nur dann, wenn die Elemente identisch sind. Schließlich ist die Auswirkung von wiederholten ähnlichen Schattenwurzeln auf die Größe der Netzwerkübertragung aufgrund der Komprimierung relativ gering.

In Zukunft ist es möglicherweise möglich, gemeinsame Schattenwurzeln noch einmal zu verwenden. Wenn das DOM integrierte Vorlagen unterstützt, können die deklarativen Schattenwurzeln als Vorlagen behandelt werden, die instanziiert werden, um den Schattenstamm für ein bestimmtes Element zu erstellen. Das aktuelle Design des deklarativen Schatten-DOM ermöglicht diese Möglichkeit in Zukunft, indem die Schatten-Stammverknüpfung auf ein einzelnes Element beschränkt wird.

Streaming ist cool

Die direkte Verknüpfung von deklarativen Schattenwurzeln mit dem übergeordneten Element vereinfacht das Upgrade und die Verknüpfung mit diesem Element. Deklarative Shadow Roots werden beim HTML-Parsing erkannt und sofort angehängt, wenn ihr öffneendes <template>-Tag gefunden wird. Analysierter HTML-Code in <template> wird direkt in den Schattenstamm geparst, sodass er "gestreamt" werden kann, während er empfangen wird.

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

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

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

Nur Parser

Das deklarative Shadow DOM ist eine Funktion des HTML-Parsers. Das bedeutet, dass ein deklarativer Schattenstamm nur für <template>-Tags mit einem shadowrootmode-Attribut geparst und angehängt wird, die beim HTML-Parsing vorhanden sind. Mit anderen Worten, Deklarative Schattenwurzeln können während des anfänglichen HTML-Parsings erstellt werden:

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

Wenn Sie das Attribut shadowrootmode eines <template>-Elements festlegen, passiert nichts. Die Vorlage bleibt ein gewöhnliches Vorlagenelement:

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

Um einige wichtige Sicherheitsaspekte zu vermeiden, können deklarative Schattenwurzeln auch nicht mit APIs zum Parsen von Fragmenten wie innerHTML oder insertAdjacentHTML() erstellt werden. Die einzige Möglichkeit, HTML mit angewendeten deklarativen Schattenwurzeln zu parsen, ist die Verwendung von setHTMLUnsafe() oder 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>

Server-Rendering mit Stil

Inline- und externe Stylesheets werden in deklarativen Schatten-Roots mithilfe der standardmäßigen <style>- und <link>-Tags vollständig unterstützt:

<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>

Stile, die auf diese Weise angegeben werden, sind ebenfalls stark optimiert: Wenn dasselbe Stylesheet in mehreren deklarativen Schattenwurzeln vorhanden ist, wird es nur einmal geladen und geparst. Der Browser verwendet ein einzelnes Sicherungs-CSSStyleSheet, das von allen Shadow-Roots gemeinsam genutzt wird, sodass doppelter Arbeitsspeicher-Overhead eliminiert wird.

Konstruktierbare Stylesheets werden im deklarativen Schatten-DOM nicht unterstützt. Das liegt daran, dass es derzeit keine Möglichkeit gibt, konstruktive Stylesheets in HTML zu serialisieren und beim Ausfüllen von adoptedStyleSheets auf sie zu verweisen.

Ungefaltete Inhalte vermeiden

Ein potenzielles Problem in Browsern, die noch kein deklaratives Schatten-DOM unterstützen, ist die Vermeidung von "Flash of Unstyled Content" (FOUC), bei dem die Rohinhalte für benutzerdefinierte Elemente angezeigt werden, die noch nicht aktualisiert wurden. Vor dem deklarativen Schatten-DOM wurde eine gängige Methode zur Vermeidung von FOUC darin, die Stilregel display:none auf benutzerdefinierte Elemente anzuwenden, die noch nicht geladen wurden, da ihr Schattenstamm nicht angehängt und ausgefüllt wurde. So werden Inhalte erst angezeigt, wenn sie „ready“ (bereit) verfügbar sind:

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

Mit der Einführung des deklarativen Schatten-DOM können benutzerdefinierte Elemente in HTML so gerendert oder erstellt werden, dass ihr Schatteninhalt an Ort und Stelle bereitsteht, bevor die clientseitige Komponentenimplementierung geladen wird:

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

In diesem Fall würde die „FOUC“-Regel display:none verhindern, dass der Inhalt des deklarativen Schattenstamms angezeigt wird. Das Entfernen dieser Regel würde jedoch dazu führen, dass Browser, die kein deklaratives Schatten-DOM unterstützen, falsche oder unformatierte Inhalte anzeigen, bis der polyfill des deklarativen Schatten-DOM geladen und die Schattenstammvorlage in einen echten Schattenstamm umgewandelt hat.

Glücklicherweise kann dies in CSS gelöst werden, indem die FOUC-Stilregel geändert wird. In Browsern, die deklaratives Schatten-DOM unterstützen, wird das <template shadowrootmode>-Element sofort in einen Schattenstamm umgewandelt, sodass kein <template>-Element in der DOM-Struktur verbleibt. Browser, die kein deklaratives Schatten-DOM unterstützen, behalten das Element <template> bei, mit dem wir FOUC verhindern können:

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

Anstatt das noch nicht definierte benutzerdefinierte Element auszublenden, werden durch die überarbeitete „FOUC“-Regel die zugehörigen untergeordneten Elemente ausgeblendet, wenn sie auf ein <template shadowrootmode>-Element folgen. Sobald das Custom Element definiert ist, stimmt die Regel nicht mehr überein. Die Regel wird in Browsern ignoriert, die deklaratives Schatten-DOM unterstützen, da das untergeordnete <template shadowrootmode>-Element beim HTML-Parsing entfernt wird.

Funktionserkennung und Browserunterstützung

Das deklarative Shadow DOM ist seit Chrome 90 und Edge 91 verfügbar. Es wurde jedoch ein älteres nicht standardmäßiges Attribut namens shadowroot anstelle des standardisierten Attributs shadowrootmode verwendet. Das neue shadowrootmode-Attribut und das neue Streamingverhalten sind in Chrome 111 und Edge 111 verfügbar.

Da es sich um ein neues Webplattform-API handelt, wird das deklarative Shadow DOM noch nicht von allen Browsern unterstützt. Um festzustellen, ob ein Browser unterstützt wird, muss im Prototyp von HTMLTemplateElement geprüft werden, ob das Attribut shadowRootMode vorhanden ist:

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

Polyfill

Die Erstellung eines vereinfachten Polyfills für ein deklaratives Schatten-DOM ist relativ einfach, da ein Polyfill die Timing-Semantik oder die Parser-spezifischen Eigenschaften, mit denen eine Browserimplementierung zu tun hat, nicht perfekt replizieren muss. Um ein deklaratives Schatten-DOM mit Polyfill zu erstellen, können wir das DOM durchsuchen, um alle <template shadowrootmode>-Elemente zu finden, und sie dann in angehängte Schattenwurzeln für ihr übergeordnetes Element konvertieren. Dieser Vorgang kann erfolgen, sobald das Dokument fertig ist, oder durch spezifischere Ereignisse wie den Lebenszyklus benutzerdefinierter Elemente ausgelöst.

(function attachShadowRoots(root) {
  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);

Weitere Informationen