Handlebars.js uno sguardo dietro le quinte

Handlebars sta guadagnando popolarità con la sua adozione in framework come Meteor ed Ember.js, ma cosa sta succedendo davvero dietro le quinte di questo eccitante motore di template?

In questo articolo daremo uno sguardo approfondito attraverso il processo sottostante che attraversa Handlebars per compilare i tuoi modelli.

Questo articolo si aspetta che tu abbia letto la mia precedente introduzione a Manubri e come tale presume che tu conosca le basi della creazione di modelli di manubrio.

Quando si utilizza un modello di Handlebars probabilmente si sa che si inizia compilando il sorgente del template in una funzione usando Handlebars.compile () e quindi si utilizza quella funzione per generare l'HTML finale, passando in valori per proprietà e segnaposto.

Ma quella funzione di compilazione apparentemente semplice sta facendo davvero alcuni passi dietro le quinte, e questo è il vero argomento di questo articolo; diamo un'occhiata a una rapida interruzione del processo:

  • Tokenizzare la fonte in componenti.
  • Elabora ogni token in un insieme di operazioni.
  • Converti lo stack di processo in una funzione.
  • Esegui la funzione con il contesto e gli helper per generare dell'HTML.

Il set up

In questo articolo creeremo uno strumento per analizzare i modelli di Handlebars in ognuno di questi passaggi, quindi per visualizzare i risultati un po 'meglio sullo schermo, userò il evidenziatore di sintassi prism.js creato dall'unica e unica Lea Verou. Scarica la fonte minificata ricordando di selezionare JavaScript nella sezione lingue.

Il prossimo passo è creare un file HTML vuoto e riempirlo con il seguente:

   Handlebars.js 

gettoni:

operazioni:

Produzione:

Funzione:

È solo un codice che include manubri e prismi e quindi imposta alcuni div per i diversi passaggi. In basso, puoi vedere due blocchi di script: il primo è per il modello e il secondo è per il nostro codice JS.

Ho anche scritto un piccolo CSS per organizzare tutto un po 'meglio, che sei libero di aggiungere:

 body margin: 0; padding: 0; font-family: "opensans", Arial, sans-serif; sfondo: # F5F2F0; dimensione del font: 13px;  #analysis top: 0; a sinistra: 0; posizione: assoluta; larghezza: 100%; altezza: 100%; margine: 0; padding: 0;  #analysis div width: 33.33%; altezza: 50%; fluttuare: a sinistra; imbottitura: 10px 20px; dimensionamento della scatola: border-box; overflow: auto;  #funzione larghezza: 100%! importante; 

Successivamente abbiamo bisogno di un modello, quindi iniziamo con il modello più semplice possibile, solo un po 'di testo statico:

 

L'apertura di questa pagina nel browser dovrebbe comportare la visualizzazione del modello nella casella di output come previsto, non è ancora diverso, ora dobbiamo scrivere il codice per analizzare il processo in ognuna delle altre tre fasi.


gettoni

Il primo passo che i manubri eseguono sul tuo modello è quello di tokenizzare la sorgente, il che significa che è necessario suddividere la sorgente nei suoi singoli componenti in modo da poter gestire ogni pezzo in modo appropriato. Quindi, ad esempio, se ci fosse del testo con un segnaposto nel mezzo, allora Handlebars separerebbe il testo prima che il segnaposto lo collochi in un token, quindi il segnaposto stesso sarebbe posto in un altro token, e infine tutto il testo dopo il segnaposto verrebbe inserito in un terzo token. Questo perché quei pezzi hanno bisogno sia di mantenere l'ordine del modello, ma devono anche essere elaborati in modo diverso.

Questo processo è fatto usando il Handlebars.parse () funzione, e ciò che si ottiene è un oggetto che contiene tutti i segmenti o 'istruzioni'.

Per illustrare meglio di cosa sto parlando, creiamo un elenco di paragrafi per ciascun token estratto:

 // Visualizza token var tokenizer = Handlebars.parse (src); var tokenStr = ""; for (var i in tokenizer.statements) var token = tokenizer.statements [i]; tokenStr + = "

"+ (parseInt (i) +1) +") "; switch (token.type) case" content ": tokenStr + =" [stringa] - \ "" + token.string + "\" "; break; case "mustache": tokenStr + = "[segnaposto] -" + token.id.string; break; case "block": tokenStr + = "[block] -" + token.mustache.id.string; documento. getElementById ("token"). innerHTML + = tokenStr;

Quindi iniziamo eseguendo l'origine dei modelli in Handlebars.parse per ottenere l'elenco dei token. Quindi ciclichiamo tutti i singoli componenti e creiamo una serie di stringhe leggibili dall'uomo in base al tipo di segmento. Il testo normale avrà un tipo di "contenuto" che sarà quindi in grado di generare la stringa racchiusa tra virgolette per mostrare cosa equivale. I segnaposto avranno un tipo di "baffi" che possiamo quindi mostrare insieme al loro "id" (nome del segnaposto). E, ultimo ma non meno importante, gli helper di blocchi avranno un tipo di "blocco" che possiamo anche solo visualizzare i blocchi interni "id" (nome del blocco).

Aggiungendo ora questo nel browser, dovresti vedere solo un singolo token 'stringa', con il testo del nostro modello.


operazioni

Una volta che il manubrio ha la raccolta di token, passa ciclicamente a ciascuno di essi e "genera" un elenco di operazioni predefinite che devono essere eseguite per il modello da compilare. Questo processo è fatto usando il Handlebars.Compiler () oggetto, passando l'oggetto token dal punto 1:

 // Visualizza operazioni var opSequence = new Handlebars.Compiler (). Compile (tokenizer, ); var opStr = ""; for (var i in opSequence.opcodes) var op = opSequence.opcodes [i]; opStr + = "

"+ (parseInt (i) +1) +") - "+ op.opcode; document.getElementById (" operations "). innerHTML + = opStr;

Qui stiamo compilando i token nella sequenza delle operazioni di cui ho parlato, e quindi stiamo scorrendo attraverso ciascuno di essi e creando un elenco simile come nel primo passaggio, tranne che qui abbiamo solo bisogno di stampare l'opcode. L'opcode è "l'operazione" o il "nome" della funzione che deve essere eseguito per ogni elemento nella sequenza.

Di nuovo nel browser, ora dovresti vedere solo una singola operazione chiamata 'appendContent' che aggiungerà il valore al "buffer" o "stringa di testo" corrente. Ci sono molti opcode diversi e non penso di essere qualificato per spiegarne alcuni, ma facendo una ricerca veloce nel codice sorgente per un determinato codice operativo ti mostreremo la funzione che verrà eseguita per questo.


La funzione

L'ultimo passo è quello di prendere l'elenco degli opcode e convertirli in una funzione, lo fa leggendo l'elenco delle operazioni e il codice di concatenazione intelligente per ognuno di essi. Ecco il codice necessario per ottenere la funzione per questo passaggio:

 // Visualizza Function var outputFunction = new Handlebars.JavaScriptCompiler (). Compile (opSequence, , undefined, true); document.getElementById ("source"). innerHTML = outputFunction.toString (); Prism.highlightAll ();

La prima riga crea il passaggio del compilatore nella sequenza op, e questa linea restituirà la funzione finale utilizzata per generare il modello. Quindi convertiamo la funzione in una stringa e diciamo a Prisma che la sintassi lo evidenzia.

Con questo codice finale, la tua pagina dovrebbe apparire in questo modo:

Questa funzione è incredibilmente semplice, poiché c'era solo un'operazione, restituisce solo la stringa data; diamo ora un'occhiata alla modifica del modello e vedendo come questi passi individualmente semplici, si raggruppano per formare un'astrazione molto potente.


Esaminando i modelli

Iniziamo con qualcosa di semplice e sostituiamo semplicemente la parola "Mondo" con un segnaposto; il tuo nuovo modello dovrebbe apparire come il seguente:

 

E non dimenticare di passare la variabile in modo che l'output appaia OK:

 // Visualizza Output var t = Handlebars.compile (src); document.getElementById ("output"). innerHTML + = t (nome: "Gabriel");

Eseguendo ciò, scoprirai che aggiungendo solo un semplice segnaposto, questo complica il processo un po '.

La complicata if / else sezione è perché non sa se il segnaposto è in realtà un segnaposto o un metodo di supporto

Se non sapevi ancora quali sono i token, dovresti avere un'idea migliore ora; come puoi vedere nell'immagine, ha diviso il segnaposto dalle stringhe e creato tre singoli componenti.

Successivamente, nella sezione delle operazioni, ci sono alcune aggiunte. Se ti ricordi di prima, per emettere semplicemente del testo, Handlebars usa l'operazione 'appendContent', che è ciò che ora puoi vedere nella parte superiore e inferiore dell'elenco (sia per "Hello" che per "!"). Il resto nel mezzo sono tutte le operazioni necessarie per elaborare il segnaposto e aggiungere il contenuto di escape.

Infine, nella finestra in basso, invece di restituire una stringa, questa volta crea una variabile di buffer e gestisce un token alla volta. La complicata if / else sezione è perché non sa se il segnaposto è in realtà un segnaposto o un metodo di supporto. Quindi prova a vedere se esiste un metodo helper con il nome specificato, nel qual caso chiamerà il metodo helper e imposterà 'stack1' sul valore. Nel caso in cui sia un segnaposto, assegnerà il valore dal contesto passato (qui denominato 'depth0') e se una funzione è stata passata in esso verrà inserito il risultato della funzione nella variabile 'stack1'. Una volta fatto, lo sfugge come abbiamo visto nelle operazioni e lo aggiunge al buffer.

Per il nostro prossimo cambiamento, proviamo semplicemente lo stesso modello, tranne questa volta senza sfuggire ai risultati (per fare ciò, aggiungi un altro rinforzo "nome")

Rinfrescando la pagina, ora vedrai che è stata rimossa l'operazione per sfuggire alla variabile e invece la aggiunge semplicemente, questa esplode nella funzione che ora controlla semplicemente per assicurarsi che il valore non sia un valore falsy (oltre a 0) e quindi lo appende senza sfuggirlo.

Quindi penso che i segnaposto siano piuttosto semplici, ora diamo un'occhiata all'utilizzo delle funzioni di supporto.


Funzioni di supporto

Non ha senso renderlo più complicato di quanto deve essere, creiamo una semplice funzione che restituirà il duplicato di un numero passato, quindi sostituisci il modello e aggiungi un nuovo blocco di script per l'helper (prima dell'altro codice ):

 

Ho deciso di non scappare, poiché rende la funzione finale leggermente più semplice da leggere, ma puoi provare entrambi, se vuoi. In ogni caso, eseguendo questo dovrebbe produrre quanto segue:

Qui puoi vedere che sa che è un aiuto, quindi invece di dire 'invokeAmbiguous' ora dice 'invokeHelper' e quindi anche nella funzione non c'è più un blocco if / else. Tuttavia, fa comunque in modo che l'helper esista e tenti di tornare al contesto per una funzione con lo stesso nome nel caso in cui non lo faccia.

Un'altra cosa che vale la pena di menzionare è che puoi vedere i parametri per gli helper che vengono passati direttamente, e sono effettivamente codificati, se possibile, quando viene generata la funzione get (il numero 3 nella funzione raddoppiata).

L'ultimo esempio che voglio trattare riguarda gli helper dei blocchi.


Blocca gli aiutanti

Blocca gli helper che ti consentono di racchiudere altri token all'interno di una funzione in grado di impostare il proprio contesto e opzioni. Diamo un'occhiata a un esempio usando l'helper di blocco predefinito "if":

Qui stiamo controllando se "name" è impostato nel contesto corrente, nel qual caso lo mostreremo, altrimenti usciremo "World!". Eseguendo questo nel nostro analizzatore, vedrete solo due token anche se ce ne sono altri; questo perché ogni blocco viene eseguito come un proprio 'modello' così tutti i token al suo interno (come nome) non farà parte della chiamata esterna e sarà necessario estrarlo dal nodo stesso del blocco.

Oltre a ciò, se dai un'occhiata alla funzione:

Puoi vedere che in realtà compila le funzioni dell'helper di blocco nella funzione del modello. Ce ne sono due perché uno è la funzione principale e l'altro è la funzione inversa (per quando il parametro non esiste o è falso). La funzione principale: "program1" è esattamente ciò che avevamo prima quando avevamo solo del testo e un singolo segnaposto, perché come ho detto, ognuna delle funzioni di blocco dei blocchi è costruita e trattata esattamente come un modello normale. Vengono quindi eseguiti attraverso l'helper "if" per ricevere la funzione corretta che verrà quindi aggiunta al buffer esterno.

Come prima, vale la pena ricordare che il primo parametro di un blocco helper è la chiave stessa, mentre il parametro "this" è impostato sull'intero contesto passato, che può tornare utile quando si costruiscono i propri helper di blocco.


Conclusione

In questo articolo potremmo non aver dato un'occhiata pratica a come realizzare qualcosa in Handlebars, ma spero che tu abbia una migliore comprensione di cosa sta succedendo dietro le quinte che dovrebbe permetterti di costruire template e helper migliori con questo nuovo ritrovato conoscenza.

Spero ti sia piaciuto leggere, come sempre se hai qualche domanda non esitate a contattarmi su Twitter (@GabrielManricks) o su Nettuts + IRC (#nettuts su freenode).