Declaratieve schaduw-DOM

Een nieuwe manier om Shadow DOM rechtstreeks in HTML te implementeren en te gebruiken.

Declaratieve Shadow DOM is een webplatformfunctie die zich momenteel in het standaardisatieproces bevindt . Het is standaard ingeschakeld in Chrome-versie 111.

Shadow DOM is een van de drie Web Components-standaarden, aangevuld met HTML-sjablonen en Custom Elements . Shadow DOM biedt een manier om CSS-stijlen te beperken tot een specifieke DOM-subboom en die subboom te isoleren van de rest van het document. Het <slot> -element biedt ons een manier om te bepalen waar de kinderen van een aangepast element in de schaduwboom moeten worden ingevoegd. Deze functies gecombineerd maken een systeem mogelijk voor het bouwen van op zichzelf staande, herbruikbare componenten die naadloos in bestaande applicaties kunnen worden geïntegreerd, net als een ingebouwd HTML-element.

Tot nu toe was de enige manier om Shadow DOM te gebruiken het construeren van een schaduwwortel met behulp van JavaScript:

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

Een dergelijke imperatieve API werkt prima voor weergave aan de clientzijde: dezelfde JavaScript-modules die onze aangepaste elementen definiëren, creëren ook hun schaduwwortels en stellen hun inhoud in. Veel webapplicaties moeten echter tijdens het bouwen de inhoud op de server of in statische HTML weergeven. Dit kan een belangrijk onderdeel zijn van het bieden van een redelijke ervaring aan bezoekers die mogelijk niet in staat zijn JavaScript uit te voeren.

De rechtvaardigingen voor Server-Side Rendering (SSR) variëren van project tot project. Sommige websites moeten volledig functionele, door de server gegenereerde HTML bieden om aan de toegankelijkheidsrichtlijnen te voldoen, andere kiezen ervoor om een ​​basiservaring zonder JavaScript te bieden als een manier om goede prestaties op langzame verbindingen of apparaten te garanderen.

Historisch gezien was het moeilijk om Shadow DOM te gebruiken in combinatie met Server-Side Rendering, omdat er geen ingebouwde manier was om Shadow Roots uit te drukken in de door de server gegenereerde HTML. Er zijn ook gevolgen voor de prestaties bij het koppelen van schaduwwortels aan DOM-elementen die al zonder deze zijn weergegeven. Dit kan ervoor zorgen dat de lay-out verandert nadat de pagina is geladen, of dat er tijdelijk een flits van ongestylede inhoud ("FOUC") wordt weergegeven tijdens het laden van de stylesheets van de Shadow Root.

Declaratieve Shadow DOM (DSD) heft deze beperking op en brengt Shadow DOM naar de server.

Een declaratieve schaduwwortel bouwen

Een declaratieve schaduwwortel is een <template> -element met een shadowrootmode attribuut:

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

Een sjabloonelement met het kenmerk shadowrootmode wordt door de HTML-parser gedetecteerd en onmiddellijk toegepast als de schaduwwortel van het bovenliggende element. Het laden van de pure HTML-opmaak uit het bovenstaande voorbeeld resulteert in de volgende DOM-structuur:

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

Dit codevoorbeeld volgt de conventies van het Chrome DevTools Elements-paneel voor het weergeven van Shadow DOM-inhoud. Het teken ↳ vertegenwoordigt bijvoorbeeld Light DOM-inhoud met sleuven.

Dit geeft ons de voordelen van de inkapseling en slotprojectie van Shadow DOM in statische HTML. Er is geen JavaScript nodig om de hele boom te produceren, inclusief de schaduwwortel.

Component hydratatie

Declaratieve Shadow DOM kan op zichzelf worden gebruikt als een manier om stijlen in te kapselen of de plaatsing van onderliggende elementen aan te passen, maar is het krachtigst in combinatie met aangepaste elementen. Componenten die met aangepaste elementen zijn gebouwd, worden automatisch geüpgraded van statische HTML. Met de introductie van Declarative Shadow DOM is het nu mogelijk dat een aangepast element een schaduwroot heeft voordat het wordt geüpgraded.

Aan een aangepast element dat wordt geüpgraded vanuit HTML en dat een declaratieve schaduwwortel bevat, is die schaduwwortel al gekoppeld. Dit betekent dat voor het element al een shadowRoot-eigenschap beschikbaar is wanneer het wordt geïnstantieerd, zonder dat uw code er expliciet een maakt. Het is het beste om this.shadowRoot te controleren op elke bestaande schaduwwortel in de constructor van uw element. Als er al een waarde bestaat, bevat de HTML voor deze component een Declaratieve Schaduwwortel. Als de waarde nul is, was er geen Declarative Shadow Root aanwezig in de HTML of ondersteunt de browser geen Declarative Shadow 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>

Aangepaste elementen bestaan ​​al een tijdje en tot nu toe was er geen reden om te controleren op een bestaande schaduwwortel voordat u er een maakte met behulp van attachShadow() . Declarative Shadow DOM bevat een kleine wijziging waardoor bestaande componenten desondanks kunnen werken: het aanroepen van de attachShadow() -methode op een element met een bestaande Declarative Shadow Root levert geen fout op. In plaats daarvan wordt de Declaratieve Schaduwwortel geleegd en teruggegeven. Hierdoor kunnen oudere componenten die niet voor Declarative Shadow DOM zijn gebouwd, blijven werken, aangezien declaratieve wortels behouden blijven totdat er een dwingende vervanging wordt gecreëerd.

Voor nieuw gemaakte aangepaste elementen biedt een nieuwe eigenschap ElementInternals.shadowRoot een expliciete manier om een ​​verwijzing naar de bestaande Declaratieve Schaduwwortel van een element te krijgen, zowel open als gesloten. Dit kan worden gebruikt om elke Declaratieve Schaduwwortel te controleren en te gebruiken, terwijl je toch terug kunt vallen op attachShadow() in gevallen waarin er geen is opgegeven.

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

Eén schaduw per wortel

Een declaratieve schaduwwortel wordt alleen geassocieerd met het bovenliggende element. Dit betekent dat schaduwwortels altijd op dezelfde locatie liggen als het bijbehorende element. Deze ontwerpbeslissing zorgt ervoor dat schaduwwortels net als de rest van een HTML-document kunnen worden gestreamd. Het is ook handig voor het schrijven en genereren, omdat het toevoegen van een schaduwwortel aan een element geen registratie van bestaande schaduwwortels vereist.

De wisselwerking tussen het associëren van schaduwwortels met hun bovenliggende element is dat het niet mogelijk is dat meerdere elementen worden geïnitialiseerd vanuit dezelfde Declaratieve Schaduwwortel <template> . In de meeste gevallen waarin Declarative Shadow DOM wordt gebruikt, is dit echter onwaarschijnlijk, omdat de inhoud van elke schaduwwortel zelden identiek is. Hoewel door de server gegenereerde HTML vaak herhaalde elementstructuren bevat, verschilt hun inhoud over het algemeen, bijvoorbeeld kleine variaties in tekst of attributen. Omdat de inhoud van een geserialiseerde Declaratieve Schaduwwortel volledig statisch is, zou het upgraden van meerdere elementen vanuit een enkele Declaratieve Schaduwwortel alleen werken als de elementen identiek zouden zijn. Ten slotte is de impact van herhaalde soortgelijke schaduwwortels op de netwerkoverdrachtsgrootte relatief klein vanwege de effecten van compressie.

In de toekomst zou het mogelijk kunnen zijn om gedeelde schaduwwortels opnieuw te bekijken. Als de DOM ondersteuning krijgt voor ingebouwde templates , kunnen declaratieve schaduwwortels worden behandeld als sjablonen die worden geïnstantieerd om de schaduwwortel voor een bepaald element te construeren. Het huidige Declarative Shadow DOM-ontwerp zorgt ervoor dat deze mogelijkheid in de toekomst kan bestaan ​​door de schaduwwortelassociatie te beperken tot een enkel element.

Streamen is cool

Door declaratieve schaduwwortels rechtstreeks aan hun bovenliggende element te koppelen, wordt het proces van het upgraden en koppelen ervan aan dat element vereenvoudigd. Declaratieve schaduwwortels worden tijdens het parseren van HTML gedetecteerd en onmiddellijk toegevoegd wanneer hun openingstag <template> wordt aangetroffen. Geparseerde HTML binnen de <template> wordt rechtstreeks in de schaduwroot geparseerd, zodat het kan worden "gestreamd": weergegeven zoals het wordt ontvangen.

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

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

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

Alleen parser

Declaratieve Shadow DOM is een functie van de HTML-parser. Dit betekent dat een Declaratieve Schaduwwortel alleen wordt geparseerd en bijgevoegd voor <template> -tags met een shadowrootmode attribuut die aanwezig zijn tijdens het parseren van HTML. Met andere woorden, declaratieve schaduwwortels kunnen worden geconstrueerd tijdens de initiële HTML-parsering:

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

Het instellen van het shadowrootmode attribuut van een <template> element doet niets, en de template blijft een gewoon template element:

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

Om een ​​aantal belangrijke veiligheidsoverwegingen te vermijden, kunnen Declarative Shadow Roots ook niet worden gemaakt met behulp van API's voor het parseren van fragmenten, zoals innerHTML of insertAdjacentHTML() . De enige manier om HTML te parseren terwijl Declarative Shadow Roots is toegepast, is door een nieuwe includeShadowRoots optie door te geven aan DOMParser :

<script>
  const html = `
    <div>
      <template shadowrootmode="open"></template>
    </div>
  `;
  const div = document.createElement('div');
  div.innerHTML = html; // No shadow root here
  const fragment = new DOMParser().parseFromString(html, 'text/html', {
    includeShadowRoots: true
  }); // Shadow root here
</script>

Serverweergave met stijl

Inline en externe stylesheets worden volledig ondersteund binnen Declarative Shadow Roots met behulp van de standaard <style> en <link> tags:

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

Op deze manier gespecificeerde stijlen zijn ook sterk geoptimaliseerd: als hetzelfde stylesheet aanwezig is in meerdere Declaratieve Schaduwwortels, wordt het slechts één keer geladen en geparseerd. De browser gebruikt een enkele ondersteunende CSSStyleSheet die wordt gedeeld door alle schaduwwortels, waardoor dubbele geheugenoverhead wordt geëlimineerd.

Constructeerbare stylesheets worden niet ondersteund in Declarative Shadow DOM. Dit komt omdat er momenteel geen manier is om construeerbare stylesheets in HTML te serialiseren, en er is geen manier om ernaar te verwijzen bij het vullen van adoptedStyleSheets .

Vermijd de flits van ongestylede inhoud

Een mogelijk probleem in browsers die Declarative Shadow DOM nog niet ondersteunen, is het vermijden van "flash of unstyled content" (FOUC), waarbij de onbewerkte inhoud wordt weergegeven voor aangepaste elementen die nog niet zijn geüpgraded. Voorafgaand aan Declarative Shadow DOM was een veelgebruikte techniek om FOUC te vermijden het toepassen van een display:none stijlregel op aangepaste elementen die nog niet zijn geladen, omdat de schaduwwortel nog niet is gekoppeld en gevuld. Op deze manier wordt de inhoud pas weergegeven als deze "klaar" is:

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

Met de introductie van Declarative Shadow DOM kunnen aangepaste elementen worden weergegeven of geschreven in HTML, zodat hun schaduwinhoud aanwezig en gereed is voordat de implementatie van de component aan de clientzijde wordt geladen:

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

In dit geval zou de display:none "FOUC"-regel voorkomen dat de inhoud van de declaratieve schaduwwortel wordt weergegeven. Het verwijderen van die regel zou er echter voor zorgen dat browsers zonder Declarative Shadow DOM-ondersteuning onjuiste of niet-stijlvolle inhoud weergeven totdat de Declarative Shadow DOM- polyfill wordt geladen en de schaduwwortelsjabloon omzet in een echte schaduwwortel.

Gelukkig kan dit in CSS worden opgelost door de FOUC-stijlregel aan te passen. In browsers die Declarative Shadow DOM ondersteunen, wordt het <template shadowrootmode> -element onmiddellijk omgezet in een schaduwroot, waardoor er geen <template> -element in de DOM-structuur overblijft. Browsers die Declarative Shadow DOM niet ondersteunen, behouden het <template> -element, dat we kunnen gebruiken om FOUC te voorkomen:

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

In plaats van het nog niet gedefinieerde aangepaste element te verbergen, verbergt de herziene "FOUC"-regel de onderliggende elementen wanneer deze een <template shadowrootmode> -element volgen. Zodra het aangepaste element is gedefinieerd, komt de regel niet meer overeen. De regel wordt genegeerd in browsers die Declarative Shadow DOM ondersteunen, omdat het kind <template shadowrootmode> wordt verwijderd tijdens het parseren van HTML.

Functiedetectie en browserondersteuning

Declaratieve Shadow DOM is beschikbaar sinds Chrome 90 en Edge 91, maar gebruikte een ouder, niet-standaard attribuut genaamd shadowroot in plaats van het gestandaardiseerde shadowrootmode attribuut. Het nieuwere shadowrootmode kenmerk en streaminggedrag zijn beschikbaar in Chrome 111 en Edge 111.

Als nieuwe webplatform-API heeft Declarative Shadow DOM nog geen brede ondersteuning in alle browsers. Browserondersteuning kan worden gedetecteerd door te controleren op het bestaan ​​van een shadowRootMode eigenschap op het prototype van HTMLTemplateElement :

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

Polyvulling

Het bouwen van een vereenvoudigde polyfill voor Declarative Shadow DOM is relatief eenvoudig, omdat een polyfill niet perfect de timingsemantiek of alleen-parser-kenmerken hoeft te repliceren waar een browserimplementatie zich mee bezighoudt. Om declaratieve schaduw-DOM polyfill te maken, kunnen we de DOM scannen om alle <template shadowrootmode> -elementen te vinden en deze vervolgens converteren naar gekoppelde schaduwwortels op hun bovenliggende element. Dit proces kan worden uitgevoerd zodra het document klaar is, of kan worden geactiveerd door specifiekere gebeurtenissen, zoals de levenscycli van aangepaste elementen.

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

Verder lezen