DOM shadow dichiarativo

Il DOM shadow dichiarativo è una funzionalità standard della piattaforma web, supportata in Chrome dalla versione 90. Tieni presente che le specifiche di questa funzionalità sono cambiate nel 2023 (inclusa la ridenominazione di shadowroot in shadowrootmode) e le versioni standardizzate più aggiornate di tutte le parti della funzionalità sono state implementate nella versione 124 di Chrome.

Supporto dei browser

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

Origine

Shadow DOM è uno dei tre standard Web Components, completato dai modelli HTML e dagli elementi personalizzati. Shadow DOM consente di limitare l'ambito degli stili CSS a un sottoalbero DOM specifico e di isolare questo sottoalbero dal resto del documento. L'elemento <slot> ci consente di controllare dove devono essere inseriti gli elementi secondari di un elemento personalizzato all'interno del relativo albero ombra. La combinazione di queste funzionalità consente di creare un sistema per la creazione di componenti autonomi e riutilizzabili che si integrano perfettamente nelle applicazioni esistenti, proprio come un elemento HTML integrato.

Fino ad ora, l'unico modo per utilizzare Shadow DOM era creare un'origine shadow utilizzando JavaScript:

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

Un'API imperativa come questa funziona bene per il rendering lato client: gli stessi moduli JavaScript che definiscono i nostri elementi personalizzati creano anche le relative radici shadow e impostano i relativi contenuti. Tuttavia, molte applicazioni web devono eseguire il rendering dei contenuti lato server o in HTML statico in fase di compilazione. Questo può essere un aspetto importante per offrire un'esperienza ragionevole ai visitatori che potrebbero non essere in grado di eseguire JavaScript.

Le giustificazioni del rendering lato server (SSR) variano da progetto a progetto. Alcuni siti web devono fornire HTML completamente funzionale visualizzato dal server per soddisfare le linee guida sull'accessibilità, mentre altri scelgono di offrire un'esperienza di base senza JavaScript per garantire buone prestazioni su connessioni o dispositivi lenti.

In passato, è stato difficile utilizzare Shadow DOM in combinazione con il rendering lato server perché non esisteva un modo integrato per esprimere le radici shadow nel codice HTML generato dal server. Esistono anche implicazioni sul rendimento quando si collegano le radici ombre agli elementi DOM che sono già stati visualizzati senza di esse. Ciò può causare uno spostamento del layout dopo il caricamento della pagina o mostrare temporaneamente un lampo di contenuti senza stile ("FOUC") durante il caricamento degli stili dell'elemento radice nascosto.

Il DOM shadow dichiarativo (DSD) rimuove questa limitazione, portando Shadow DOM sul server.

Come creare un'origine nascosta dichiarativa

Un elemento shadow root dichiarativo è un elemento <template> con un attributo shadowrootmode:

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

Un elemento del modello con l'attributo shadowrootmode viene rilevato dal parser HTML e applicato immediatamente come elemento radice shadow del relativo elemento principale. Il caricamento del markup HTML puro dell'esempio riportato sopra genera la seguente struttura DOM:

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

Questo esempio di codice segue le convenzioni del riquadro Elementi di DevTools di Chrome per la visualizzazione dei contenuti di Shadow DOM. Ad esempio, il carattere rappresenta i contenuti light DOM con slot.

In questo modo, possiamo usufruire dei vantaggi dell'incapsulamento e della proiezione degli slot di Shadow DOM in HTML statico. Non è necessario JavaScript per produrre l'intero albero, incluso l'elemento radice ombra.

Idratazione dei componenti

Il DOM ombra dichiarativo può essere utilizzato da solo per incapsulare gli stili o personalizzare il posizionamento dei componenti figlio, ma è più efficace se utilizzato con gli elementi personalizzati. Per i componenti creati utilizzando Elementi personalizzati viene eseguito automaticamente l'upgrade da HTML statico. Con l'introduzione di DOM ombreggiato dichiarativo, ora è possibile che un elemento personalizzato abbia un'origine ombreggiata prima dell'upgrade.

Un elemento personalizzato di cui viene eseguito l'upgrade da HTML e che include un'origine ombreggiata dichiarativa avrà già quest'origine ombreggiata collegata. Ciò significa che l'elemento avrà già una proprietà shadowRoot disponibile al momento dell'inizializzazione, senza che il codice ne crei una esplicitamente. Ti consigliamo di controllare this.shadowRoot per verificare la presenza di eventuali elementi principali nascosti nel costruttore dell'elemento. Se è già presente un valore, il codice HTML di questo componente include un'origine ombreggiata dichiarativa. Se il valore è null, nell'HTML non è presente un'origine animata dichiarativa o il browser non supporta il DOM ombra dichiarativo.

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

Gli elementi personalizzati sono disponibili da un po' di tempo e finora non c'era motivo di verificare la presenza di un'origine ombreggiata esistente prima di crearne una utilizzando attachShadow(). Il DOM ombra dichiarativo include una piccola modifica che consente il funzionamento dei componenti esistenti nonostante questo: l'utilizzo del metodo attachShadow() su un elemento con un'origine ombra declarativa esistente non genera un errore. Al contrario, l'elemento radice dell'ombra dichiarativa viene svuotato e restituito. In questo modo, i componenti precedenti non creati per il DOM ombra dichiarativo possono continuare a funzionare, poiché le radici dichiarative vengono conservate fino a quando non viene creata una sostituzione imperativa.

Per gli elementi personalizzati appena creati, una nuova proprietà ElementInternals.shadowRoot fornisce un modo esplicito per ottenere un riferimento all'elemento ombra dichiarativo esistente di un elemento, sia aperto che chiuso. Questo può essere utilizzato per verificare e utilizzare qualsiasi elemento radice ombra dichiarativo, pur facendo comunque ricorso a attachShadow() nei casi in cui non ne sia stato fornito uno.

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

Un'ombra per radice

Un elemento radice ombra dichiarativo è associato solo al suo elemento principale. Ciò significa che le radici shadow sono sempre colocate con l'elemento associato. Questa decisione di progettazione garantisce che le radici shadow siano riproducibili in streaming come il resto di un documento HTML. È anche pratico per la creazione e la generazione, poiché l'aggiunta di un'origine ombreggiata a un elemento non richiede la gestione di un registry delle origini ombreggiate esistenti.

Il compromesso dell'associazione delle radici shadow con il relativo elemento principale è che non è possibile inizializzare più elementi dallo stesso elemento shadow dichiarativo <template>. Tuttavia, è improbabile che questo sia importante nella maggior parte dei casi in cui viene utilizzato il DOM ombra dichiarativo, poiché i contenuti di ogni radice ombra sono raramente identici. Sebbene il codice HTML visualizzato dal server contenga spesso strutture di elementi ripetute, i relativi contenuti sono generalmente diversi, ad esempio lievi variazioni nel testo o negli attributi. Poiché i contenuti di un elemento shadow dichiarativo serializzato sono completamente statici, l'upgrade di più elementi da un singolo elemento shadow dichiarativo funzionerebbe solo se gli elementi fossero identici. Infine, l'impatto di radici shadow simili ripetute sulle dimensioni del trasferimento di rete è relativamente ridotto a causa degli effetti della compressione.

In futuro, potrebbe essere possibile rivedere le radici shadow condivise. Se il DOM supporta i modelli integrati, gli elementi shadow root dichiarativi potrebbero essere trattati come modelli che vengono istituite per costruire l'elemento shadow root di un determinato elemento. L'attuale design di Shadow DOM dichiarativo consente che questa possibilità esista in futuro limitando l'associazione dell'elemento radice dell'ombra a un singolo elemento.

Lo streaming è fantastico

L'associazione delle radici ombre declarative direttamente all'elemento principale semplifica il processo di upgrade e di allacciamento a quell'elemento. Le radici ombre dichiarative vengono rilevate durante l'analisi HTML e collegate immediatamente quando viene rilevato il tag <template> di apertura. Il codice HTML analizzato all'interno di <template> viene analizzato direttamente nell'elemento radice ombra, quindi può essere "trasmesso in streaming": viene visualizzato man mano che viene ricevuto.

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

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

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

Solo parser

Il DOM shadow dichiarativo è una funzionalità dell'interprete HTML. Ciò significa che un'origine animata dichiarativa verrà analizzata e collegata solo per i tag <template> con un attributo shadowrootmode presenti durante l'analisi HTML. In altre parole, le radici ombreggiate dichiarative possono essere costruite durante l'analisi iniziale dell'HTML:

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

L'impostazione dell'attributo shadowrootmode di un elemento <template> non ha alcun effetto e il modello rimane un normale elemento del modello:

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

Per evitare alcuni importanti aspetti di sicurezza, le radici ombreggiate dichiarative non possono essere create utilizzando API di analisi del frammento come innerHTML o insertAdjacentHTML(). L'unico modo per analizzare l'HTML con le radici shadow dichiarative applicate è utilizzare setHTMLUnsafe() o 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>

Rendering lato server con stile

I fogli di stile in linea ed esterni sono completamente supportati all'interno delle radici ombreggiate dichiarative utilizzando i tag <style> e <link> standard:

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

Gli stili specificati in questo modo sono anche altamente ottimizzati: se lo stesso foglio di stile è presente in più origini ombre declarative, viene caricato e analizzato solo una volta. Il browser utilizza un unico CSSStyleSheet di supporto condiviso da tutte le radici shadow, eliminando il sovraccarico di memoria duplicato.

I CSS componibili non sono supportati nel DOM ombra dichiarativo. Questo perché, al momento, non è possibile serializzare gli stili CSS componibili in HTML e non è possibile fare riferimento a questi stili durante il completamento di adoptedStyleSheets.

Come evitare il lampo di contenuti senza stile

Un potenziale problema nei browser che non supportano ancora Shadow DOM dichiarativo è evitare il "lampo di contenuti non formattati " (FOUC), in cui i contenuti non elaborati vengono mostrati per gli elementi personalizzati di cui non è stato ancora eseguito l'upgrade. Prima del DOM shadow dichiarativo, una tecnica comune per evitare il FOUC era applicare una regola di stile display:none agli elementi personalizzati che non sono stati ancora caricati, poiché non è stato collegato e compilato il relativo elemento shadow root. In questo modo, i contenuti non vengono visualizzati finché non sono "pronti":

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

Con l'introduzione del DOM ombra dichiarativo, gli elementi personalizzati possono essere visualizzati o creati in HTML in modo che i relativi contenuti ombra siano in posizione e pronti prima del caricamento dell'implementazione del componente lato client:

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

In questo caso, la regola "FOUC" display:none impedirebbe la visualizzazione dei contenuti dell'elemento shadow root dichiarativo. Tuttavia, la rimozione di questa regola causerebbe la visualizzazione di contenuti errati o senza stile nei browser senza supporto del DOM shadow dichiarativo finché il polyfill del DOM shadow dichiarativo non viene caricato e converte il modello di root shadow in un vero root shadow.

Fortunatamente, questo problema può essere risolto in CSS modificando la regola di stile FOUC. Nei browser che supportano il DOM shadow dichiarativo, l'elemento <template shadowrootmode> viene convertito immediatamente in un'origine shadow, senza lasciare elementi <template> nella struttura DOM. I browser che non supportano il DOM shadow dichiarativo conservano l'elemento <template>, che possiamo utilizzare per impedire il FOUC:

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

Invece di nascondere l'elemento personalizzato non ancora definito, la regola "FOUC" rivista nasconde i relativi elementi secondari quando seguono un elemento <template shadowrootmode>. Una volta definito l'elemento personalizzato, la regola non corrisponde più. La regola viene ignorata nei browser che supportano il DOM ombra dichiarativo perché l'elemento secondario <template shadowrootmode> viene rimosso durante l'analisi del codice HTML.

Rilevamento delle funzionalità e supporto dei browser

Il DOM shadow dichiarativo è disponibile da Chrome 90 ed Edge 91, ma utilizzava un attributo non standard precedente chiamato shadowroot anziché l'attributo standardizzato shadowrootmode. L'attributo shadowrootmode e il comportamento di streaming più recenti sono disponibili in Chrome 111 ed Edge 111.

In quanto nuova API della piattaforma web, il DOM ombra dichiarativo non è ancora supportato su tutti i browser. Il supporto del browser può essere rilevato verificando l'esistenza di una proprietà shadowRootMode nel prototipo di HTMLTemplateElement:

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

Polyfill

La creazione di un polyfill semplificato per il DOM ombra dichiarativo è relativamente semplice, poiché un polyfill non deve replicare perfettamente la semantica dei tempi o le caratteristiche solo per il parser che riguardano un'implementazione del browser. Per eseguire il polyfill di Declarative Shadow DOM, possiamo eseguire la scansione del DOM per trovare tutti gli elementi <template shadowrootmode>, quindi convertirli in radici shadow collegate nell'elemento principale. Questa procedura può essere eseguita quando il documento è pronto o attivata da eventi più specifici, come i cicli di vita degli elementi personalizzati.

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

Per approfondire