Handlebars.js un regard en coulisses

Handlebars gagne en popularité avec son adoption dans des cadres tels que Meteor et Ember.js, mais ce qui se passe réellement dans les coulisses de ce moteur de templates passionnant?

Dans cet article, nous examinerons en profondeur le processus sous-jacent à Handlebars pour compiler vos modèles..

Cet article s'attend à ce que vous ayez lu mon introduction précédente à Guidon et suppose donc que vous connaissez les bases de la création de modèles de guidon.

Lorsque vous utilisez un modèle de guidon, vous savez probablement que vous commencez par compiler la source du modèle en une fonction utilisant Guidon.compile () et ensuite vous utilisez cette fonction pour générer le code HTML final, en transmettant des valeurs pour les propriétés et les espaces réservés.

Mais cette fonction de compilation apparemment simple est en train de faire pas mal d’étapes en coulisse, et c’est le but de cet article; Jetons un coup d'oeil à une ventilation rapide du processus:

  • Tokenize la source en composants.
  • Traiter chaque jeton dans un ensemble d'opérations.
  • Convertir la pile de processus en une fonction.
  • Exécutez la fonction avec le contexte et les aides pour générer du HTML.

La mise en place

Dans cet article, nous allons créer un outil pour analyser les modèles de guidons à chacune de ces étapes. Par conséquent, pour afficher les résultats un peu mieux à l’écran, j’utiliserai le Surligneur de syntaxe prism.js créé par la seule et unique Lea Verou. Téléchargez la source réduite en vérifiant que JavaScript est activé dans la section des langues.

L'étape suivante consiste à créer un fichier HTML vierge et à le remplir comme suit:

   Guidon.js 

Jetons:

Opérations:

Sortie:

Une fonction:

C'est juste un code standard qui inclut un guidon et un prisme, puis des divsions pour les différentes étapes. En bas, vous pouvez voir deux blocs de script: le premier pour le modèle et le second pour notre code JS..

J'ai aussi écrit un peu de CSS pour arranger un peu mieux tout ce que vous êtes libre d'ajouter:

 corps marge: 0; rembourrage: 0; font-family: "opensans", Arial, sans-serif; arrière-plan: # F5F2F0; taille de police: 13px;  #analysis top: 0; gauche: 0; position: absolue; largeur: 100%; hauteur: 100%; marge: 0; rembourrage: 0;  #analysis div width: 33.33%; hauteur: 50%; float: gauche; rembourrage: 10px 20px; taille de la boîte: boîte-frontière; débordement: auto;  #function width: 100%! important; 

Ensuite, nous avons besoin d’un modèle. Commençons par le modèle le plus simple possible, avec juste un texte statique:

 

Si vous ouvrez cette page dans votre navigateur, le modèle sera affiché dans la zone de sortie comme prévu. Rien d’autre, nous devons maintenant écrire le code pour analyser le processus à chacune des trois autres étapes..


Jetons

La première étape que le guidon effectue sur votre modèle consiste à segmenter la source, ce qui signifie que nous devons diviser la source en composants individuels de manière à pouvoir traiter chaque élément de manière appropriée. Ainsi, par exemple, s'il y avait du texte avec un espace réservé au milieu, alors le guidon séparerait le texte avant que celui-ci ne le place dans un seul jeton. serait placé dans un troisième jeton. En effet, ces pièces doivent à la fois conserver l'ordre du modèle mais également être traitées différemment..

Ce processus est fait en utilisant le Guidon.parse () fonction, et ce que vous récupérez est un objet qui contient tous les segments ou "déclarations".

Pour mieux illustrer ce dont je parle, créons une liste de paragraphes pour chacun des jetons sortis:

 // Afficher les jetons var tokenizer = Handlebars.parse (src); var tokenStr = ""; for (var i dans tokenizer.statements) var token = tokenizer.statements [i]; tokenStr + = "

"+ (parseInt (i) +1) +") "; commutateur (token.type) case" content ": tokenStr + =" [chaîne] - \ "" + token.string + "\" "; break; case "moustache": tokenStr + = "[espace réservé] -" + token.id.string; break; case "block": tokenStr + = "[block] -" + token.mustache.id.string; document. getElementById ("jetons"). innerHTML + = tokenStr;

Nous commençons donc par exécuter la source des modèles dans Guidon.parse pour obtenir la liste des jetons. Nous passons ensuite en revue tous les composants individuels et construisons un ensemble de chaînes lisibles par l'homme en fonction du type de segment. Le texte brut aura un type de "contenu" sur lequel nous pourrons alors simplement sortir la chaîne entourée de guillemets pour montrer ce qu'elle est égale. Les espaces réservés auront un type de "moustache" que nous pourrons ensuite afficher avec leur "id" (nom d'espace réservé). Et enfin, les assistants de bloc auront un type de "bloc" dans lequel on peut alors simplement afficher les blocs internes "id" (nom du bloc).

En rafraîchissant cela maintenant dans le navigateur, vous devriez voir juste un seul jeton 'chaîne', avec le texte de notre template.


Des opérations

Une fois que le guidon a la collection de jetons, il les passe en revue et "génère" une liste d'opérations prédéfinies devant être effectuées pour que le modèle soit compilé. Ce processus est fait en utilisant le Handlebars.Compiler () objet, en passant l'objet jeton de l'étape 1:

 // Opérations d'affichage var opSequence = new Handlebars.Compiler (). Compile (tokenizer, ); var opStr = ""; for (var i dans opSequence.opcodes) var op = opSequence.opcodes [i]; opStr + = "

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

Ici, nous compilons les jetons dans la séquence d'opérations dont j'ai parlé, puis nous parcourons chacun d'eux pour créer une liste similaire à celle de la première étape, sauf qu'ici, nous devons simplement imprimer le code d'opération. Le code d'opération est le "nom" de "l'opération" ou le nom de la fonction qui doit être exécuté pour chaque élément de la séquence..

De retour dans le navigateur, vous ne devriez maintenant voir qu'une seule opération appelée "appendContent" qui ajoutera la valeur au "tampon" actuel ou à la "chaîne de texte". Il existe de nombreux opcodes différents et je ne pense pas être qualifié pour en expliquer quelques-uns, mais une recherche rapide dans le code source d'un opcode donné vous indiquera la fonction qui sera exécutée..


La fonction

La dernière étape consiste à extraire la liste des opcodes et à les convertir en une fonction, en lisant la liste des opérations et en concaténant intelligemment le code correspondant à chacune d’elles. Voici le code requis pour accéder à la fonction pour cette étape:

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

La première ligne crée le compilateur en passant dans la séquence op, et cette ligne renverra la fonction finale utilisée pour générer le modèle. Nous convertissons ensuite la fonction en chaîne et disons à Prism de mettre en évidence sa syntaxe.

Avec ce code final, votre page devrait ressembler à ceci:

Cette fonction est incroyablement simple, puisqu'il n'y a qu'une seule opération, elle ne fait que renvoyer la chaîne donnée; Voyons maintenant comment modifier le modèle et voir comment ces étapes simples et individuelles se regroupent pour former une abstraction très puissante..


Modèles d'examen

Commençons par quelque chose de simple et remplaçons simplement le mot "Monde" par un espace réservé; votre nouveau modèle devrait ressembler à ceci:

 

Et n'oubliez pas de transmettre la variable afin que la sortie ait l'air OK:

 // Affichage de la sortie var t = Handlebars.compile (src); document.getElementById ("sortie"). innerHTML + = t (name: "Gabriel");

En exécutant cela, vous constaterez qu’en ajoutant un simple espace réservé, cela complique un peu le processus..

La section compliquée si / else est parce qu'elle ne sait pas si l'espace réservé est en fait un espace réservé ou une méthode d'assistance

Si vous ne saviez toujours pas ce que sont les jetons, vous devriez avoir une meilleure idée maintenant. comme vous pouvez le voir sur l'image, il sépare l'espace réservé des chaînes et crée trois composants individuels.

Ensuite, dans la section des opérations, il y a quelques ajouts. Si vous vous souvenez d’avant, pour produire du texte, Handlebars utilise l’opération 'appendContent', ce que vous pouvez voir en haut et en bas de la liste (pour les deux mots "Hello" et "!"). Le reste du milieu regroupe toutes les opérations nécessaires au traitement de l'espace réservé et à l'ajout du contenu échappé..

Enfin, dans la fenêtre du bas, au lieu de simplement renvoyer une chaîne, cette fois-ci crée une variable de tampon et gère un jeton à la fois. La section compliquée si / else est parce qu'elle ne sait pas si le paramètre fictif est en réalité un paramètre fictif ou une méthode auxiliaire. Il essaie donc de voir s'il existe une méthode d'assistance portant le même nom, auquel cas il appellera la méthode d'assistance et affectera la valeur "stack1". S'il s'agit d'un espace réservé, il affectera la valeur du contexte passé (ici appelé «profondeur0») et, si une fonction est passée, le résultat de la fonction sera placé dans la variable «pile1». Une fois que tout cela est fait, il s'échappe comme nous l'avons vu dans les opérations et l'ajoute à la mémoire tampon..

Pour notre prochain changement, essayons simplement le même modèle, sauf que cette fois-ci sans échapper aux résultats (pour ce faire, ajoutez une autre accolade "prénom")

En rafraîchissant la page, vous verrez maintenant que l'opération supprimée pour échapper à la variable a été supprimée. Au lieu de cela, elle l'a simplement ajoutée. Cette action est insérée dans la fonction qui vérifie simplement que la valeur n'est pas falsifiée (à part 0), puis l'ajoute sans y échapper.

Donc, je pense que les espaces réservés sont assez simples, jetons maintenant un coup d’œil à l’utilisation des fonctions d’aide.


Fonctions d'assistance

Il est inutile de rendre cela plus compliqué que nécessaire. Créons simplement une fonction qui renverra le duplicata d’un nombre transmis. Remplacez donc le modèle et ajoutez un nouveau bloc de script pour l’aide (avant l’autre code). ):

 

J'ai décidé de ne pas y échapper, car la fonction finale est un peu plus simple à lire, mais vous pouvez essayer les deux si vous le souhaitez. Quoi qu'il en soit, cela devrait produire les éléments suivants:

Ici, vous pouvez voir qu'il sait qu'il s'agit d'une aide. Ainsi, au lieu de dire 'invokeAmbiguous', on dit maintenant 'invokeHelper' et, par conséquent, la fonction ne contient plus de bloc if / else. Cependant, il vérifie toujours que l’assistant existe et essaie de revenir au contexte d’une fonction portant le même nom dans le cas contraire..

Une autre chose à noter est que vous pouvez voir que les paramètres des assistants sont transmis directement et sont en fait codés en dur, si possible, lorsque la fonction est générée (le nombre 3 dans la fonction doublée)..

Le dernier exemple que je veux couvrir concerne les aides en bloc..


Bloquer les assistants

Les assistants de blocage vous permettent d’envelopper d’autres jetons dans une fonction capable de définir son propre contexte et ses propres options. Jetons un coup d'oeil à un exemple utilisant l'aide par défaut du bloc 'if':

Nous vérifions ici si "name" est défini dans le contexte actuel, auquel cas nous allons l'afficher, sinon nous produisons "World!". En exécutant ceci dans notre analyseur, vous ne verrez que deux jetons, même s’il y en a plus; ceci est dû au fait que chaque bloc est exécuté comme son propre "modèle", de sorte que tous les jetons qu'il contient (comme prénom) ne fera pas partie de l'appel externe et vous devrez l'extraire du noeud du bloc lui-même.

En plus de cela, si vous regardez la fonction:

Vous pouvez voir qu'il compile réellement les fonctions de l'aide de bloc dans la fonction du modèle. Il y en a deux car l'une est la fonction principale et l'autre est la fonction inverse (lorsque le paramètre n'existe pas ou est faux). La fonction principale: "programme1" correspond exactement à ce que nous avions auparavant lorsque nous avions juste un texte et un seul espace réservé, car, comme je l’ai mentionné plus haut, chacune des fonctions d’aide aux blocs est construite et traitée exactement comme un modèle standard. Ils sont ensuite exécutés dans l’assistant "if" pour recevoir la fonction appropriée qu’il ajoutera ensuite à la mémoire tampon externe..

Comme auparavant, il convient de mentionner que le premier paramètre d'un assistant de bloc est la clé elle-même, tandis que le paramètre 'this' est défini sur l'ensemble du contexte passé, ce qui peut s'avérer utile lors de la création de vos propres assistants de bloc..


Conclusion

Dans cet article, nous n’avons peut-être pas jeté un regard pratique sur la manière d’accomplir quelque chose dans le guidon, mais j’espère que vous aurez une meilleure compréhension de ce qui se passe dans les coulisses, ce qui devrait vous permettre de créer de meilleurs modèles et assistants avec ce nouveau connaissance.

J'espère que vous avez aimé lire, comme toujours si vous avez des questions, n'hésitez pas à me contacter sur Twitter (@GabrielManricks) ou sur le Nettuts + IRC (#nettuts sur freenode).