Deklaratives Schatten-DOM

Das deklarative Shadow DOM ist eine Standardfunktion der Webplattform, die in Chrome ab Version 90 unterstützt wird. Die Spezifikation für diese Funktion wurde 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.

Unterstützte Browser

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 123
  • Safari: 16.4

Quelle

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 innerhalb des Schattenbaums eingefügt werden sollen. Durch die Kombination dieser Funktionen wird ein System zum Erstellen in sich geschlossener, wiederverwendbarer Komponenten ermöglicht, das sich wie ein integriertes HTML-Element nahtlos in vorhandene Anwendungen integrieren lässt.

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, Besuchern, die kein JavaScript ausführen können, eine angemessene Nutzererfahrung zu bieten.

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 grundlegende Version ohne JavaScript bereitstellen, um auch bei langsamen Verbindungen oder Geräten eine gute Leistung 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 ohne sie gerendert wurden, hat dies auch 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.

Mit deklarativem Schatten-DOM (DSD) wird diese Einschränkung aufgehoben und der Server wird mit dem Shadow DOM verbunden.

So erstellst du eine deklarative Schattenwurzel

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. Beispielsweise steht das Zeichen für Slotted Light DOM-Inhalt.

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.

Einem benutzerdefinierten Element, das von HTML aktualisiert wird und das eine deklarative Schattenwurzel enthält, ist bereits mit diesem Schattenstamm verknüpft. Das bedeutet, dass das Element bei der Instanziierung bereits eine shadowRoot-Eigenschaft hat, ohne dass Ihr Code explizit eines erstellt. Es empfiehlt sich, this.shadowRoot auf vorhandene Schattenstammelemente im Konstruktor Ihres Elements zu 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, nach einem vorhandenen Schattenstamm zu suchen, bevor Sie einen mit attachShadow() erstellen. Das deklarative Shadow DOM umfasst eine kleine Änderung, durch die vorhandene Komponenten trotzdem funktionieren können: Das Aufrufen der Methode attachShadow() für ein Element mit einem vorhandenen deklarativen Schattenstamm löst keinen Fehler aus. Stattdessen wird die deklarierte Schattenwurzel geleert und zurückgegeben. Dadurch können ältere Komponenten, die nicht für deklaratives Schatten-DOM erstellt wurden, weiterhin funktionieren, da deklarative Wurzeln beibehalten werden, bis eine imperative Ersetzung erstellt wird.

Bei neu erstellten benutzerdefinierten Elementen bietet eine neue ElementInternals.shadowRoot-Eigenschaft eine explizite Möglichkeit, einen Verweis auf den vorhandenen deklarativen Schattenstamm eines Elements (offen und geschlossen) zu erhalten. Damit kann nach deklarativen Schattenwurzeln gesucht und diese 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 Registrierung vorhandener 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 die Inhalte der einzelnen Schattenstamm selten identisch sind. 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, würde das Upgrade mehrerer Elemente von einem einzigen deklarativen Schattenstamm nur funktionieren, 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 deklarative 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 öffnendes <template>-Tag erkannt 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 und 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 Style Sheet in mehreren deklarativen Schattenwurzeln vorhanden ist, wird es nur einmal geladen und geparst. Der Browser verwendet ein einziges Sicherungs-CSSStyleSheet, das von allen Shadow-Roots gemeinsam genutzt wird. Dadurch wird doppelter Arbeitsspeicher-Overhead vermieden.

Konstruktierbare Stylesheets werden im deklarativen Schatten-DOM nicht unterstützt. Der Grund hierfür ist, dass es derzeit keine Möglichkeit gibt, konstruierbare Stylesheets in HTML zu serialisieren und beim Ausfüllen von adoptedStyleSheets auf sie zu verweisen.

So vermeidest du unformatierte Inhalte

Ein potenzielles Problem bei Browsern, die noch kein deklaratives Schatten-DOM unterstützen, besteht darin, ein Flash-Element von nicht formatierten Inhalten zu vermeiden. (FOUC), wobei der Rohinhalt für benutzerdefinierte Elemente angezeigt wird, 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 lautet der Wert für display:none „FOUC“ verhindert, dass der Inhalt des deklarativen Schattenstamms angezeigt wird. Das Entfernen dieser Regel würde jedoch dazu führen, dass Browser ohne Unterstützung für deklaratives Schatten-DOM falsche oder unformatierte Inhalte anzeigen, bis der Polyfill des deklarativen Schatten-DOM geladen und die Schattenstammvorlage in einen echten Schattenstamm konvertiert wird.

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

Statt das noch nicht definierte Custom Element zu verbergen, wird die überarbeitete Version von „FOUC“ werden die 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, weil 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. Allerdings wurde 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 eine Browserunterstützung vorhanden ist, muss im Prototyp von HTMLTemplateElement geprüft werden, ob eine shadowRootMode-Eigenschaft vorhanden ist:

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

Polyfill

Die Erstellung eines vereinfachten Polyfills für das deklarative Shadow DOM ist relativ einfach, da ein Polyfill die Timing-Semantik oder die Parser-spezifischen Merkmale, die bei einer Browserimplementierung vorhanden sind, 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 Lebenszyklen von benutzerdefinierten Elementen ausgelöst werden.

(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