Nuovo tag modello HTML

Standardizzazione dei modelli lato client

Introduzione

Il concetto di modello non è nuovo nello sviluppo web. Infatti, lingue/motori dei modelli 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 tutte leggermente diverse, ma la maggior parte condivide un meccanismo comune per il rendering dei modelli del livello di presentazione (noto anche come view).

Ammettiamolo. I modelli sono fantastici. Vai avanti, chiedi in giro. Anche la sua definizione ti fa sentire caldo e confortevole:

"...non deve essere ricreato ogni volta..." Non so di te, ma adoro evitare il lavoro extra. Perché la piattaforma web non supporta un supporto nativo per gli sviluppatori?

La specifica dei modelli HTML WhatWG è la risposta. Definisce un nuovo elemento <template> che descrive un approccio standard basato su DOM per i modelli lato client. I modelli ti consentono di dichiarare i frammenti di markup che vengono analizzati come HTML, che diventano inutilizzati al caricamento della pagina, ma che possono essere creati in un secondo momento durante l'esecuzione. Per citare Rafael Weinstein:

Sono uno spazio in cui inserire grosse porzioni di codice HTML che non vuoi che il browser rovini... per nessun motivo.

Rafael Weinstein (autore delle specifiche)

Rilevamento delle funzionalità

Per il rilevamento di <template> da parte della funzionalità, 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 modello

L'elemento HTML <template> rappresenta un modello nel tuo markup. Contiene "contenuti del modello"; essenzialmente blocchi inerti di DOM clonato. Pensa ai modelli come a elementi di struttura che puoi utilizzare (e riutilizzare) per tutto il ciclo di vita della tua app.

Per creare contenuti basati su modelli, dichiara il markup e includilo nell'elemento <template>:

<template id="mytemplate">
    <img src="" alt="great image">
    <div class="comment"></div>
</template>

I pilastri

L'aggregazione di contenuti in un <template> ci dà alcune proprietà importanti.

  1. I suoi contenuti sono di fatto inerti fino all'attivazione. In pratica, il markup è DOM nascosto e non viene visualizzato.

  2. I contenuti all'interno di un modello 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.

  3. I contenuti sono considerati non presenti nel documento. L'uso di document.getElementById() o querySelector() nella pagina principale non restituirà nodi secondari di un modello.

  4. I modelli possono essere posizionati ovunque 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 sintattico HTML... tutti tranne i modelli di contenuti secondari. 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 relativi contenuti non verranno mai visualizzati. Il modo più semplice per farlo è creare una copia approfondita del suo .content utilizzando document.importNode(). La proprietà .content è un elemento DocumentFragment di sola lettura che contiene le parti del modello.

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

Una volta rimosso il modello, i relativi contenuti "vengono pubblicati". In questo esempio specifico, i contenuti vengono clonati, viene effettuata la richiesta dell'immagine e viene eseguito il rendering del markup finale.

Demo

Esempio: script inerte

Questo esempio mostra l'inertezza dei contenuti del modello. L'<script> viene eseguito solo quando il pulsante viene premuto, escludendo 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 modello

La maggior parte delle persone collega 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ù il DOM shadow diventa complesso, maggiore è la concatenazione di stringhe. Non si adatta, le cose si fanno caotici in poco tempo e i bambini iniziano a piangere. È così che è nata anche l'XSS. <template> in soccorso.

Una cosa più logica sarebbe lavorare direttamente con il DOM aggiungendo i contenuti del modello a una radice shadow:

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

Ottieni

Ecco alcuni suggerimenti che ho riscontrato utilizzando <template> all'aperto:

  • Se utilizzi modpagespeed, fai attenzione a questo bug. I modelli che definiscono <style scoped> in linea, molti vengono spostati in alto con le regole di riscrittura CSS di PageSpeed.
  • Non è possibile eseguire il "prerendering" di un modello, ovvero non è possibile precaricare gli asset, elaborare JS, scaricare il codice CSS iniziale e così via. Questo vale sia per il server che per il client. Il modello viene visualizzato solo nel momento in cui viene pubblicato.
  • Fai attenzione ai modelli nidificati. Non si comportano come previsto. Ad esempio:

    <template>
      <ul>
        <template>
          <li>Stuff</li>
        </template>
      </ul>
    </template>
    

    L'attivazione del modello esterno non attiverà i modelli interni. I modelli nidificati richiedono che anche i relativi elementi secondari vengano attivati manualmente.

Il percorso verso un

Non dimentichiamo da dove siamo venuti. Il percorso verso i modelli HTML basati su standard è stato lungo. Nel corso degli anni, abbiamo ideato dei trucchi piuttosto intelligenti per creare modelli riutilizzabili. Ecco due delle domande più comuni che ho trovato. Li includerò in questo articolo per fare un confronto.

Metodo 1: DOM fuori schermo

Un approccio che gli utenti usano da molto tempo è creare un DOM "off-screen" e nasconderlo dalla visualizzazione utilizzando l'attributo hidden o display:none.

<div id="mytemplate" hidden>
  <img src="logo.png">
  <div class="comment"></div>
</div>

Sebbene questa tecnica funzioni, ci sono diversi svantaggi. Riepilogo di questa tecnica:

  • Utilizzando DOM: il browser conosce il DOM. Ci riesce. Possiamo clonarlo facilmente.
  • Non viene visualizzato nulla: l'aggiunta di hidden impedisce la visualizzazione del blocco.
  • Non inerte: anche se i nostri contenuti sono nascosti, viene comunque effettuata una richiesta di rete per l'immagine.
  • Stili e temi problematici: una pagina di incorporamento deve far precedere tutte le regole CSS dal prefisso #mytemplate per limitare l'ambito degli stili al modello. Si tratta di una procedura complessa e non c'è alcuna garanzia che non riscontreremo conflitti di denominazione in futuro. Ad esempio, siamo in regola se la pagina di incorporamento contiene già un elemento con quell'ID.

Metodo 2: script di sovraccarico

Un'altra tecnica consiste nel sovraccaricare <script> e manipolarne i contenuti come stringa. John Resig è stato probabilmente il primo a mostrarlo nel 2008 con la sua utilità di Micro Templating. Ora ce ne sono molti altri, inclusi alcuni nuovi bambini come handlebars.js.

Ad esempio:

<script id="mytemplate" type="text/x-handlebars-template">
  <img src="logo.png">
  <div class="comment"></div>
</script>

Riepilogo di questa tecnica:

  • Non viene visualizzato nulla: il browser non esegue il rendering di questo blocco perché <script> è display:none per impostazione predefinita.
  • Inerte: il browser non analizza i contenuti dello script come JS perché il tipo è impostato su qualcosa di diverso da "text/JavaScript".
  • Problemi di sicurezza: incoraggia l'utilizzo di .innerHTML. L'analisi delle stringhe in fase di runtime dei dati forniti dall'utente può causare facilmente vulnerabilità XSS.

Conclusione

Ricordi quando jQuery ha reso il lavoro estremamente semplice con DOM? Il risultato è stato l'aggiunta di querySelector()/querySelectorAll() alla piattaforma. Vittoria ovvia, vero? Una libreria ha reso popolare il DOM di recupero con i selettori e gli standard CSS. Non sempre funziona in questo modo, ma adoro quando sì.

Penso che <template> sia un caso simile. Standardizza il modo in cui eseguiamo i modelli lato client ma, soprattutto, elimina la necessità dei nostri attacchi del 2008. Rendere l'intero processo di authoring web più sano, più gestibile e più completo è sempre un aspetto positivo del mio libro.

Altre risorse