Lo shadow DOM dichiarativo è una funzionalità standard della piattaforma web, supportata in Chrome a partire dalla versione 90. Tieni presente che le specifiche per questa funzionalità sono cambiate nel 2023 (inclusa la ridenominazione di shadowroot
in shadowrootmode
) e che le versioni standardizzate più aggiornate di tutte le parti della funzionalità sono disponibili nella versione 124 di Chrome.
Shadow DOM è uno dei tre standard dei componenti web, completato da modelli HTML ed elementi personalizzati. Shadow DOM consente di definire l'ambito degli stili CSS in uno specifico sottoalbero DOM e isolare questo sottoalbero dal resto del documento. L'elemento <slot>
offre un modo per controllare dove inserire gli elementi secondari di un elemento personalizzato all'interno del suo albero ombra. La combinazione di queste caratteristiche consente di creare un sistema per la creazione di componenti autonomi e riutilizzabili che si integrano perfettamente con le applicazioni esistenti proprio come un elemento HTML integrato.
Finora, l'unico modo per utilizzare Shadow DOM era creare una radice 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 directory shadow e ne impostano i contenuti. Tuttavia, molte applicazioni web devono eseguire il rendering dei contenuti lato server o in formato HTML statico in fase di creazione. Questo può essere un aspetto importante per offrire un'esperienza ragionevole ai visitatori che potrebbero non essere in grado di eseguire JavaScript.
Le giustificazioni per il rendering lato server (SSR) variano in base al progetto. Per rispettare le linee guida sull'accessibilità, alcuni siti web devono fornire codice HTML sottoposto a rendering completamente funzionante dal server, altri scelgono di offrire un'esperienza senza JavaScript di base come metodo per garantire buone prestazioni su connessioni o dispositivi lenti.
Storicamente, è stato difficile utilizzare Shadow DOM in combinazione con il rendering lato server, perché non esisteva un modo integrato per esprimere le radici shadow nell'HTML generato dal server. Ci sono anche implicazioni in termini di prestazioni quando si collegano le root shadow agli elementi DOM che sono già stati sottoposti a rendering senza questi elementi. Questo può causare una variazione del layout dopo il caricamento della pagina o mostrare temporaneamente un lampo di contenuti senza stile ("FOUC") durante il caricamento dei fogli di stile della radice ombra.
Il Declarative Shadow DOM (DSD) rimuove questa limitazione, portando Shadow DOM sul server.
Come creare una radice ombra dichiarativa
Una radice ombra dichiarativa è 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 dall'analizzatore sintattico HTML e applicato immediatamente come radice ombra dell'elemento principale. Il caricamento del markup HTML puro dagli esempi di esempio precedenti 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 Chrome DevTools per la visualizzazione dei contenuti DOM shadow. Ad esempio, il carattere ↳
rappresenta i contenuti Light DOM con slotting.
Questo ci offre i vantaggi dell'incapsulamento e della proiezione degli slot di Shadow DOM in HTML statico. Non è necessario alcun codice JavaScript per produrre l'intero albero, inclusa la radice ombra.
Idratazione dei componenti
Lo Shadow DOM dichiarativo può essere utilizzato da solo per incapsulare gli stili o personalizzare il posizionamento figlio, ma è più efficace se utilizzato con Elementi personalizzati. L'upgrade dei componenti creati utilizzando Elementi personalizzati viene eseguito automaticamente dal codice HTML statico. Con l'introduzione del DOM dichiarativo di shadow, ora un elemento personalizzato può avere una radice shadow prima dell'upgrade.
Per un elemento personalizzato in fase di upgrade da HTML che include una radice ombra dichiarativa sarà già collegata questa radice shadow. Ciò significa che l'elemento avrà una proprietà shadowRoot
già disponibile quando viene creata un'istanza, senza che il codice ne crei una in modo esplicito. È meglio controllare this.shadowRoot
per verificare la presenza di eventuali root shadow esistenti nel costruttore dell'elemento. Se esiste già un valore, il codice HTML di questo componente include una radice ombra dichiarativa. Se il valore è nullo, nell'HTML non era presente alcuna radice shadow dichiarativa oppure il browser non supporta il DOM shadow 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 in uso da un po' di tempo e fino ad ora non c'era motivo di verificare la presenza di una radice shadow esistente prima di crearne una utilizzando attachShadow()
. Lo Shadow DOM dichiarativo include una piccola modifica che consente il funzionamento dei componenti esistenti nonostante ciò: la chiamata del metodo attachShadow()
su un elemento con una radice shadow dichiarativa esistente non genererà un errore. La radice ombra dichiarativa viene invece svuotata e restituita. Ciò consente ai componenti meno recenti non creati per Shadow DOM dichiarativo di 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 alla radice ombra dichiarativa esistente di un elemento, sia aperta che chiusa. Questa opzione può essere utilizzata per verificare la presenza di qualsiasi radice ombra dichiarativa e utilizzarla, ma può essere utilizzata a partire da attachShadow()
nei casi in cui non ne sia stata fornita una.
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
Una radice ombra dichiarativa è associata soltanto al suo elemento principale. Ciò significa che le radici shadow sono sempre cologate con l'elemento associato. Questa decisione di progettazione garantisce che le radici shadow possano essere trasmesse in streaming come il resto di un documento HTML. È utile anche per la creazione e la generazione, poiché l'aggiunta di una radice shadow a un elemento non richiede la gestione di un registro di root shadow esistenti.
Il compromesso derivante dall'associazione delle radici ombra all'elemento principale è che non è possibile inizializzare più elementi dalla stessa radice ombra dichiarativa <template>
. Tuttavia, è improbabile che questo sia importante nella maggior parte dei casi in cui viene utilizzato il DOM shadow dichiarativo, poiché i contenuti di ogni radice ombra sono raramente identici. Sebbene l'HTML visualizzato dal server spesso contenga strutture di elementi ripetute, generalmente i suoi contenuti differiscono, ad esempio leggere variazioni nel testo o negli attributi. Poiché i contenuti di una radice ombra dichiarativa serializzata sono completamente statici, l'upgrade di più elementi da una singola radice ombra dichiarativa funzionerebbe solo se gli elementi risultavano identici. Infine, l'impatto di radici shadow simili ripetute sulle dimensioni di trasferimento di rete è relativamente ridotto a causa degli effetti della compressione.
In futuro potrebbe essere possibile rivedere le radici shadow condivise. Se il DOM ottiene il supporto per i modelli integrati, le radici shadow dichiarative potrebbero essere trattate come modelli di cui viene creata un'istanza al fine di creare la radice ombra per un determinato elemento. L'attuale design dichiarativo del DOM shadow consente di avere questa possibilità in futuro limitando l'associazione della radice ombra a un singolo elemento.
Lo streaming è fantastico
L'associazione delle radici ombra dichiarative direttamente all'elemento padre semplifica il processo di upgrade e il loro collegamento a quell'elemento. Le radici ombra dichiarative vengono rilevate durante l'analisi dell'HTML e collegate immediatamente quando viene rilevato il tag <template>
di apertura. L'HTML analizzato all'interno di <template>
viene analizzato direttamente nella radice shadow, in modo che possa essere "in streaming": viene visualizzato non appena viene ricevuto.
<div id="el">
<script>
el.shadowRoot; // null
</script>
<template shadowrootmode="open">
<!-- shadow realm -->
</template>
<script>
el.shadowRoot; // ShadowRoot
</script>
</div>
Solo parser
Lo Shadow DOM dichiarativo è una funzionalità dell'analizzatore sintattico HTML. Ciò significa che una radice ombra dichiarativa verrà analizzata e collegata solo per i tag <template>
con un attributo shadowrootmode
presente durante l'analisi del codice HTML. In altre parole, le radici delle ombre dichiarative possono essere create durante l'analisi HTML iniziale:
<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, mentre 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 importanti considerazioni sulla sicurezza, non è inoltre possibile creare le directory shadow dichiarative utilizzando API di analisi dei frammenti come innerHTML
o insertAdjacentHTML()
. L'unico modo per analizzare l'HTML con le radici ombra 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 del server con stile
I fogli di stile in linea ed esterni sono completamente supportati all'interno delle directory shadow 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>
Anche gli stili specificati in questo modo sono altamente ottimizzati: se lo stesso foglio di stile è presente in più radici ombra dichiarative, viene caricato e analizzato una sola volta. Il browser utilizza un singolo CSSStyleSheet
di backup condiviso da tutte le root shadow, eliminando l'overhead della memoria duplicata.
I fogli di stile costruibili non sono supportati nel DOM dichiarativo shadow. Questo perché, al momento, non è possibile serializzare i fogli di stile costruibili in HTML e non è possibile farvi riferimento quando compili adoptedStyleSheets
.
Come evitare il flash di contenuti senza stile
Un potenziale problema nei browser che non supportano ancora il DOM dichiarativo è quello di evitare il flash di contenuti senza stile (FOUC), in cui vengono mostrati i contenuti non elaborati per gli elementi personalizzati di cui non è stato ancora eseguito l'upgrade. Prima dello Shadow DOM dichiarativo, una tecnica comune per evitare FOUC era applicare una regola di stile display:none
agli elementi personalizzati che non erano ancora stati caricati, poiché non avevano la radice shadow collegata e compilata. 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 dichiarativo shadow, è possibile eseguire il rendering o creare elementi personalizzati in HTML in modo che i contenuti shadow siano presenti 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, il "FOUC" di display:none
impedisce la visualizzazione dei contenuti della radice ombra dichiarativa. Tuttavia, la rimozione di questa regola potrebbe far sì che i browser che non supportano lo Shadow DOM dichiarativo mostrino contenuti errati o senza stile fino a quando il polyfill dichiarativo del DOM shadow non viene caricato e converte il modello principale shadow in una radice shadow reale.
Fortunatamente, questo problema può essere risolto in CSS modificando la regola di stile FOUC. Nei browser che supportano Shadow DOM dichiarativo, l'elemento <template shadowrootmode>
viene immediatamente convertito in una radice shadow, senza lasciare alcun elemento <template>
nell'albero DOM. I browser che non supportano il DOM dichiarativo shadow conservano l'elemento <template>
, che possiamo utilizzare per evitare FOUC:
<style>
x-foo:not(:defined) > template[shadowrootmode] ~ * {
display: none;
}
</style>
Invece di nascondere l'elemento personalizzato non ancora definito, il "FOUC" rivisto nasconde i relativi figli 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 dichiarativo shadow perché il figlio <template shadowrootmode>
viene rimosso durante l'analisi HTML.
Rilevamento delle funzionalità e supporto del browser
Lo shadow DOM dichiarativo è disponibile da Chrome 90 ed Edge 91, ma utilizzava un attributo non standard meno recente chiamato shadowroot
anziché l'attributo shadowrootmode
standardizzato. L'attributo shadowrootmode
e il comportamento di streaming più recenti sono disponibili in Chrome 111 ed Edge 111.
Essendo una nuova API della piattaforma web, il dichiarativo Shadow DOM non dispone ancora di un supporto diffuso in tutti i browser. Il supporto del browser può essere rilevato controllando l'esistenza di una proprietà shadowRootMode
sul prototipo di HTMLTemplateElement
:
function supportsDeclarativeShadowDOM() {
return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}
Polyfill
Creare un polyfill semplificato per il DOM dichiarativo di shadow è relativamente semplice, dal momento che un polyfill non deve replicare perfettamente la semantica dei tempi o le caratteristiche solo dell'analizzatore sintattico che interessano l'implementazione del browser. Per eseguire il polyfill del DOM dichiarativo shadow, possiamo eseguire la scansione del DOM per trovare tutti gli elementi <template shadowrootmode>
, quindi convertirli in root shadow collegate sull'elemento principale. Questo processo può essere eseguito una volta che il documento è pronto o attivato 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);