Standardizzazione dei modelli lato client
Introduzione
Il concetto di templating non è nuovo nello sviluppo web. In effetti, i linguaggi/motori di template lato server come Django (Python), ERB/Haml (Ruby) e Smarty (PHP) esistono da molto tempo. Negli ultimi due anni, tuttavia, abbiamo assistito a un'esplosione di framework MVC. Sono tutti leggermente diversi, ma la maggior parte condivide un meccanismo comune per il rendering del livello di presentazione (ovvero la visualizzazione): i modelli.
Ammettiamolo. I modelli sono fantastici. Chiedi pure in giro. Anche la sua definizione trasmette una sensazione di calore e intimità:
"…non deve essere ricreato ogni volta…" Non so voi, ma io adoro evitare il lavoro extra. Perché allora la piattaforma web non supporta in modo nativo qualcosa che interessa chiaramente agli sviluppatori?
La specifica dei modelli HTML di WhatWG è la risposta. Definisce un nuovo elemento <template> che descrive un approccio standard basato su DOM per la creazione di modelli lato client. I modelli consentono di dichiarare frammenti di markup che vengono analizzati come HTML, non vengono utilizzati al caricamento pagina, ma possono essere istanziati in un secondo momento in fase di runtime. Per citare Rafael Weinstein:
Sono un posto dove inserire un grande blocco di HTML che non vuoi che il browser tocchi in alcun modo… per nessun motivo.
Rafael Weinstein (autore delle specifiche)
Rilevamento delle caratteristiche
Per rilevare la funzionalità <template>, crea l'elemento DOM e verifica che la proprietà .content esista:
function supportsTemplate() {
return 'content' in document.createElement('template');
}
if (supportsTemplate()) {
// Good to go!
} else {
// Use old templating techniques or libraries.
}
Dichiarazione dei contenuti del template
L'elemento HTML <template> rappresenta un modello nel tuo markup. Contiene "contenuti del modello", essenzialmente blocchi inerti di DOM clonabile.
Pensa ai modelli come a elementi di un'impalcatura che puoi utilizzare (e riutilizzare) per tutta la durata della tua app.
Per creare contenuti basati su modelli, dichiara un markup e inseriscilo nell'elemento <template>:
<template id="mytemplate">
<img src="" alt="great image">
<div class="comment"></div>
</template>
I pilastri
Se inserisci i contenuti in un <template>, ottieni alcune proprietà importanti.
I contenuti sono effettivamente inerti fino all'attivazione. In sostanza, il markup è un DOM nascosto e non viene visualizzato.
I contenuti di un template non avranno effetti collaterali. Lo script non viene eseguito, le immagini non vengono caricate, l'audio non viene riprodotto… finché non viene utilizzato il modello.
I contenuti non sono considerati presenti nel documento. L'utilizzo di
document.getElementById()oquerySelector()nella pagina principale non restituirà i nodi secondari di un modello.I modelli possono essere posizionati in qualsiasi punto all'interno di
<head>,<body>o<frameset>e possono contenere qualsiasi tipo di contenuto consentito in questi elementi. Tieni presente che "ovunque" significa che<template>può essere utilizzato in sicurezza in posizioni non consentite dall'analizzatore HTML… tutte tranne i nodi secondari del modello di contenuto. Può anche essere posizionato come elemento secondario di<table>o<select>:
<table>
<tr>
<template id="cells-to-repeat">
<td>some content</td>
</template>
</tr>
</table>
Attivazione di un modello
Per utilizzare un modello, devi attivarlo. In caso contrario, i suoi contenuti non verranno mai visualizzati.
Il modo più semplice per farlo è creare una copia completa del suo .content utilizzando document.importNode(). La proprietà .content è un DocumentFragment di sola lettura contenente i dettagli del template.
var t = document.querySelector('#mytemplate');
// Populate the src at runtime.
t.content.querySelector('img').src = 'logo.png';
var clone = document.importNode(t.content, true);
document.body.appendChild(clone);
Dopo aver creato un modello, i suoi contenuti "vengono pubblicati". In questo esempio specifico, i contenuti vengono clonati, viene effettuata la richiesta di immagine e viene eseguito il rendering del markup finale.
Demo
Esempio: script inerte
Questo esempio mostra l'inerzia dei contenuti del template. <script> viene eseguito solo quando viene premuto il pulsante, stampando il modello.
<button onclick="useIt()">Use me</button>
<div id="container"></div>
<script>
function useIt() {
var content = document.querySelector('template').content;
// Update something in the template DOM.
var span = content.querySelector('span');
span.textContent = parseInt(span.textContent) + 1;
document.querySelector('#container').appendChild(
document.importNode(content, true)
);
}
</script>
<template>
<div>Template used: <span>0</span></div>
<script>alert('Thanks!')</script>
</template>
Esempio: creazione di un DOM shadow da un template
La maggior parte delle persone associa lo shadow DOM a un host impostando una stringa di markup su .innerHTML:
<div id="host"></div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<span>Host node</span>';
</script>
Il problema di questo approccio è che più lo Shadow DOM diventa complesso, più concatenazioni di stringhe devi eseguire. Non è scalabile, le cose si complicano rapidamente e i bambini iniziano a piangere. Questo approccio è anche il modo in cui è nato l'XSS. <template> in soccorso.
Un approccio più sensato sarebbe lavorare direttamente con il DOM aggiungendo il contenuto del template a una shadow root:
<template>
<style>
:host {
background: #f8f8f8;
padding: 10px;
transition: all 400ms ease-in-out;
box-sizing: border-box;
border-radius: 5px;
width: 450px;
max-width: 100%;
}
:host(:hover) {
background: #ccc;
}
div {
position: relative;
}
header {
padding: 5px;
border-bottom: 1px solid #aaa;
}
h3 {
margin: 0 !important;
}
textarea {
font-family: inherit;
width: 100%;
height: 100px;
box-sizing: border-box;
border: 1px solid #aaa;
}
footer {
position: absolute;
bottom: 10px;
right: 5px;
}
</style>
<div>
<header>
<h3>Add a Comment
</header>
<content select="p"></content>
<textarea></textarea>
<footer>
<button>Post</button>
</footer>
</div>
</template>
<div id="host">
<p>Instructions go here</p>
</div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.appendChild(document.querySelector('template').content);
</script>
Gotchas
Ecco alcuni problemi che ho riscontrato durante l'utilizzo di <template>:
- Se utilizzi modpagespeed, fai attenzione a questo bug. I modelli che definiscono
<style scoped>in linea possono essere spostati nell'intestazione con le regole di riscrittura CSS di PageSpeed. - Non è possibile eseguire il "prerendering" di un template, il che significa che non puoi precaricare gli asset, elaborare JS, scaricare il CSS iniziale e così via. Questo vale sia per il server che per il client. Un modello viene visualizzato solo quando viene pubblicato.
Fai attenzione ai template nidificati. Non si comportano come potresti aspettarti. Ad esempio:
<template> <ul> <template> <li>Stuff</li> </template> </ul> </template>L'attivazione del modello esterno non attiverà i modelli interni. Ciò significa che i modelli nidificati richiedono che anche i relativi modelli secondari vengano attivati manualmente.
La strada verso uno standard
Non dimentichiamo da dove veniamo. La strada verso i modelli HTML basati su standard è stata lunga. Nel corso degli anni, abbiamo ideato alcuni trucchi piuttosto intelligenti per creare modelli riutilizzabili. Di seguito sono riportati due problemi comuni che ho riscontrato. Li includo in questo articolo per un confronto.
Metodo 1: DOM fuori schermo
Un approccio utilizzato da molto tempo consiste nel creare un DOM "fuori schermo" e nasconderlo alla vista utilizzando l'attributo hidden o display:none.
<div id="mytemplate" hidden>
<img src="logo.png">
<div class="comment"></div>
</div>
Sebbene questa tecnica funzioni, presenta una serie di svantaggi. Ecco un riepilogo di questa tecnica:
- Utilizzo del DOM: il browser conosce il DOM. È molto bravo. Possiamo clonarlo facilmente.
- Non viene eseguito il rendering di nulla: l'aggiunta di
hiddenimpedisce la visualizzazione del blocco. - Non inerte: anche se i nostri contenuti sono nascosti, viene comunque effettuata una richiesta di rete per l'immagine.
- Stile e temi complessi: una pagina di incorporamento deve anteporre
#mytemplatea tutte le sue regole CSS per limitare gli stili al modello. Questo approccio è fragile e non garantisce che non si verificheranno conflitti di denominazione in futuro. Ad esempio, se la pagina di incorporamento ha già un elemento con quell'ID, non possiamo fare nulla.
Metodo 2: script di sovraccarico
Un'altra tecnica consiste nell'eseguire l'overload di <script> e manipolarne i contenuti come stringa. John Resig è stato probabilmente il primo a dimostrarlo nel 2008 con la sua utilità di micro-templating.
Ora ce ne sono molti altri, tra cui alcuni nuovi arrivati come handlebars.js.
Ad esempio:
<script id="mytemplate" type="text/x-handlebars-template">
<img src="logo.png">
<div class="comment"></div>
</script>
Ecco un riepilogo di questa tecnica:
- Non viene eseguito il rendering di nulla: il browser non esegue il rendering di questo blocco perché
<script>èdisplay:noneper impostazione predefinita. - Inert: il browser non analizza i contenuti dello script come JS perché il tipo è impostato su un valore diverso da "text/javascript".
- Problemi di sicurezza: incoraggia l'uso di
.innerHTML. L'analisi delle stringhe in fase di esecuzione dei dati forniti dagli utenti può facilmente portare a vulnerabilità XSS.
Conclusione
Ricordi quando jQuery rendeva semplicissimo lavorare con il DOM? Il risultato è stato l'aggiunta di querySelector()/querySelectorAll() alla piattaforma. Un successo evidente, giusto? Una libreria ha reso popolare il recupero del DOM con i selettori CSS e gli standard lo hanno adottato in seguito. Non funziona sempre così, ma adoro quando succede.
Penso che <template> sia un caso simile. Standardizza il modo in cui eseguiamo i modelli lato client, ma soprattutto elimina la necessità dei nostri hack del 2008.
Rendere l'intero processo di creazione web più sano, più gestibile e più completo è sempre una buona cosa secondo me.
Risorse aggiuntive
- WhatWG Specification
- Introduzione ai componenti web
- <web>components</web> (video): una presentazione incredibilmente completa di me stesso.