Maison >interface Web >js tutoriel >Apprenez à écrire simplement un moteur de modèle de page en utilisant les compétences javascript_javascript

Apprenez à écrire simplement un moteur de modèle de page en utilisant les compétences javascript_javascript

WBOY
WBOYoriginal
2016-05-16 16:00:581074parcourir

J'ai donc réfléchi à la possibilité d'écrire du code simple pour améliorer ce moteur de modèles et de travailler avec d'autres logiques existantes. AbsurdJS lui-même est principalement publié en tant que module pour NodeJS, mais il publie également une version client. Dans cette optique, je ne peux pas utiliser directement les moteurs existants car la plupart d'entre eux fonctionnent sur NodeJS et ne peuvent pas fonctionner sur le navigateur. Ce dont j'ai besoin, c'est de quelque chose de petit, écrit uniquement en Javascript, qui puisse s'exécuter directement dans le navigateur. Quand je suis tombé un jour sur ce blog de John Resig, j'ai été agréablement surpris de constater que c'était exactement ce que je cherchais ! J'ai apporté quelques légères modifications, et le nombre de lignes de code est d'environ 20. La logique est très intéressante. Dans cet article, je vais reproduire étape par étape le processus d'écriture de ce moteur. Si vous parvenez à lire, vous comprendrez à quel point l'idée de John est pointue !

Mes premières pensées étaient les suivantes :

var TemplateEngine = function(tpl, data) {
  // magic here ...
}
var template = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>';
console.log(TemplateEngine(template, {
  name: "Krasimir",
  age: 29
}));

Une fonction simple, l'entrée est notre modèle et notre objet de données, la sortie est probablement facile à imaginer, comme ce qui suit :

e388a4556c0f65e1904146cc1a846beeBonjour, je m'appelle Krasimir, j'ai 29 ans.94b3e26ee717c64999d7867364b1b4a3
La première étape consiste à trouver les paramètres du modèle à l’intérieur et à les remplacer par les données spécifiques transmises au moteur. J'ai décidé d'utiliser des expressions régulières pour accomplir cette étape. Mais je ne suis pas le meilleur dans ce domaine, alors n'hésitez pas à commenter si votre écriture n'est pas bonne.

var re = /<%([^%>]+)&#63;%>/g;

Cette expression régulière capturera tous les fragments commençant par d01c9b8657475002c4bafa3b514f86c2. Le paramètre g (global) à la fin signifie que non seulement un fragment correspond, mais que tous les fragments correspondants le sont. Il existe de nombreuses façons d'utiliser les expressions régulières en Javascript. Ce dont nous avons besoin est de générer un tableau contenant toutes les chaînes basées sur l'expression régulière. C'est exactement ce que fait exec.

var re = /<%([^%>]+)&#63;%>/g;
var match = re.exec(tpl);

Si nous utilisons console.log pour imprimer la correspondance de variable, nous verrons :

[
  "<%name%>",
  " name ", 
  index: 21,
  input: 
  "<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>"
]

Mais on peut voir que le tableau renvoyé ne contient que la première correspondance. Nous devons envelopper la logique ci-dessus avec une boucle while afin que nous puissions obtenir toutes les correspondances.

var re = /<%([^%>]+)&#63;%>/g;
while(match = re.exec(tpl)) {
  console.log(match);
}

Si vous exécutez le code ci-dessus, vous verrez que db185f51abe1d02301f57d0c9e8c9eb5 et 5775174bcf4feadc65f033b84eb1e107

Vient maintenant la partie intéressante. Après avoir identifié les correspondances dans le modèle, nous devons les remplacer par les données réelles transmises à la fonction. Le moyen le plus simple consiste à utiliser la fonction de remplacement. On peut l'écrire ainsi :

var TemplateEngine = function(tpl, data) {
  var re = /<%([^%>]+)&#63;%>/g;
  while(match = re.exec(tpl)) {
    tpl = tpl.replace(match[0], data[match[1]])
  }
  return tpl;
}

D'accord, donc je peux courir, mais ce n'est pas suffisant. Ici, nous utilisons un objet simple pour transmettre des données sous la forme de données["propriété"], mais dans les situations réelles, nous aurons probablement besoin d'objets imbriqués plus complexes. Nous avons donc légèrement modifié l'objet de données :

{
  name: "Krasimir Tsonev",
  profile: { age: 29 }
}

Cependant, si vous l'écrivez directement ainsi, il ne fonctionnera pas, car si vous utilisez 913f7752eb512a3f068e4113f2294efd dans le modèle, le code sera remplacé par data['profile.age'], et le résultat sera indéfini. De cette façon, nous ne pouvons pas simplement utiliser la fonction replace, mais devons utiliser d’autres méthodes. Il serait préférable que vous puissiez utiliser du code Javascript directement entre 28bcf2047c89371241fb5d13477c798a, afin que les données entrantes puissent être évaluées directement, comme ceci :

Copier le code Le code est le suivant :

var template = 'e388a4556c0f65e1904146cc1a846beeBonjour, je m'appelle 9951915acf0f83f8d48a7a5aa3e0943c J'ai c3c740eb268a72a41cd4ae63499ebd77 ans.94b3e26ee717c64999d7867364b1b4a3';

Vous pourriez être curieux, comment cela est-il réalisé ? Ici, John utilise la nouvelle syntaxe Function pour créer une fonction basée sur une chaîne. Prenons un exemple :

var fn = new Function("arg", "console.log(arg + 1);");
fn(2); // outputs 3

 fn est une véritable fonction. Il accepte un paramètre et le corps de la fonction est console.log(arg 1);. Le code ci-dessus est équivalent au code suivant :

var fn = function(arg) {
  console.log(arg + 1);
}
fn(2); // outputs 3

Avec cette méthode, nous pouvons construire une fonction à partir d'une chaîne, y compris ses paramètres et son corps de fonction. N’est-ce pas exactement ce que nous voulons ! Mais ne vous inquiétez pas, avant de construire la fonction, regardons à quoi ressemble le corps de la fonction. Selon l'idée précédente, le retour final de ce moteur de modèle devrait être un modèle compilé. Toujours en utilisant la chaîne de modèle précédente comme exemple, le contenu renvoyé devrait être similaire à :

return
"<p>Hello, my name is " + 
this.name + 
". I\'m " + 
this.profile.age + 
" years old.</p>";

Bien sûr, dans le moteur de modèle actuel, nous diviserons le modèle en petits morceaux de texte et en code Javascript significatif. Plus tôt, vous m'avez peut-être vu utiliser une simple concaténation de chaînes pour obtenir l'effet souhaité, mais cela n'est pas conforme à 100 % à nos exigences. Étant donné que l'utilisateur est susceptible de transmettre du code Javascript plus complexe, nous avons besoin ici d'une autre boucle, comme suit :

var template = 
'My skills:' + 
'<%for(var index in this.skills) {%>' + 
'<a href=""><%this.skills[index]%></a>' +
'<%}%>';

  如果使用字符串拼接的话,代码就应该是下面的样子:

return
'My skills:' + 
for(var index in this.skills) { +
'<a href="">' + 
this.skills[index] +
'</a>' +
}

  当然,这个代码不能直接跑,跑了会出错。于是我用了John的文章里写的逻辑,把所有的字符串放在一个数组里,在程序的最后把它们拼接起来。

var r = [];
r.push('My skills:'); 
for(var index in this.skills) {
r.push('<a href="">');
r.push(this.skills[index]);
r.push('</a>');
}
return r.join('');

  下一步就是收集模板里面不同的代码行,用于生成函数。通过前面介绍的方法,我们可以知道模板中有哪些占位符(译者注:或者说正则表达式的匹配项)以及它们的位置。所以,依靠一个辅助变量(cursor,游标),我们就能得到想要的结果。

var TemplateEngine = function(tpl, data) {
  var re = /<%([^%>]+)&#63;%>/g,
    code = 'var r=[];\n',
    cursor = 0;
  var add = function(line) {
    code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
  }
  while(match = re.exec(tpl)) {
    add(tpl.slice(cursor, match.index));
    add(match[1]);
    cursor = match.index + match[0].length;
  }
  add(tpl.substr(cursor, tpl.length - cursor));
  code += 'return r.join("");'; // <-- return the result
  console.log(code);
  return tpl;
}
var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
console.log(TemplateEngine(template, {
  name: "Krasimir Tsonev",
  profile: { age: 29 }
}));

  上述代码中的变量code保存了函数体。开头的部分定义了一个数组。游标cursor告诉我们当前解析到了模板中的哪个位置。我们需要依靠它来遍历整个模板字符串。此外还有个函数add,它负责把解析出来的代码行添加到变量code中去。有一个地方需要特别注意,那就是需要把code包含的双引号字符进行转义(escape)。否则生成的函数代码会出错。如果我们运行上面的代码,我们会在控制台里面看见如下的内容:

var r=[];
r.push("<p>Hello, my name is ");
r.push("this.name");
r.push(". I'm ");
r.push("this.profile.age");
return r.join("");

  等等,貌似不太对啊,this.name和this.profile.age不应该有引号啊,再来改改。

var add = function(line, js) {
  js&#63; code += 'r.push(' + line + ');\n' :
    code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
}
while(match = re.exec(tpl)) {
  add(tpl.slice(cursor, match.index));
  add(match[1], true); // <-- say that this is actually valid js
  cursor = match.index + match[0].length;
}

  占位符的内容和一个布尔值一起作为参数传给add函数,用作区分。这样就能生成我们想要的函数体了。

var r=[];
r.push("<p>Hello, my name is ");
r.push(this.name);
r.push(". I'm ");
r.push(this.profile.age);
return r.join("");

  剩下来要做的就是创建函数并且执行它。因此,在模板引擎的最后,把原本返回模板字符串的语句替换成如下的内容:

复制代码 代码如下:
return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);

  我们甚至不需要显式地传参数给这个函数。我们使用apply方法来调用它。它会自动设定函数执行的上下文。这就是为什么我们能在函数里面使用this.name。这里this指向data对象。

  模板引擎接近完成了,不过还有一点,我们需要支持更多复杂的语句,比如条件判断和循环。我们接着上面的例子继续写。

var template = 
'My skills:' + 
'<%for(var index in this.skills) {%>' + 
'<a href="#"><%this.skills[index]%></a>' +
'<%}%>';
console.log(TemplateEngine(template, {
  skills: ["js", "html", "css"]
}));

  这里会产生一个异常,Uncaught SyntaxError: Unexpected token for。如果我们调试一下,把code变量打印出来,我们就能发现问题所在。

var r=[];
r.push("My skills:");
r.push(for(var index in this.skills) {);
r.push("<a href=\"\">");
r.push(this.skills[index]);
r.push("</a>");
r.push(});
r.push("");
return r.join("");

  带有for循环的那一行不应该被直接放到数组里面,而是应该作为脚本的一部分直接运行。所以我们在把内容添加到code变量之前还要多做一个判断。

var re = /<%([^%>]+)&#63;%>/g,
  reExp = /(^( )&#63;(if|for|else|switch|case|break|{|}))(.*)&#63;/g,
  code = 'var r=[];\n',
  cursor = 0;
var add = function(line, js) {
  js&#63; code += line.match(reExp) &#63; line + '\n' : 'r.push(' + line + ');\n' :
    code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
}

  这里我们新增加了一个正则表达式。它会判断代码中是否包含if、for、else等等关键字。如果有的话就直接添加到脚本代码中去,否则就添加到数组中去。运行结果如下:

var r=[];
r.push("My skills:");
for(var index in this.skills) {
r.push("<a href=\"#\">");
r.push(this.skills[index]);
r.push("</a>");
}
r.push("");
return r.join("");

  当然,编译出来的结果也是对的。

复制代码 代码如下:

My skills:ee7959cc8dd4be16ef633321c03dac32js5db79b134e9f6b82c0b36e0489ee08edee7959cc8dd4be16ef633321c03dac32html5db79b134e9f6b82c0b36e0489ee08edee7959cc8dd4be16ef633321c03dac32css5db79b134e9f6b82c0b36e0489ee08ed

  最后一个改进可以使我们的模板引擎更为强大。我们可以直接在模板中使用复杂逻辑,例如:

var template = 
'My skills:' + 
'<%if(this.showSkills) {%>' +
  '<%for(var index in this.skills) {%>' + 
  '<a href="#"><%this.skills[index]%></a>' +
  '<%}%>' +
'<%} else {%>' +
  '<p>none</p>' +
'<%}%>';
console.log(TemplateEngine(template, {
  skills: ["js", "html", "css"],
  showSkills: true
}));

  除了上面说的改进,我还对代码本身做了些优化,最终版本如下:

var TemplateEngine = function(html, options) {
  var re = /<%([^%>]+)&#63;%>/g, reExp = /(^( )&#63;(if|for|else|switch|case|break|{|}))(.*)&#63;/g, code = 'var r=[];\n', cursor = 0;
  var add = function(line, js) {
    js&#63; (code += line.match(reExp) &#63; line + '\n' : 'r.push(' + line + ');\n') :
      (code += line != '' &#63; 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
    return add;
  }
  while(match = re.exec(html)) {
    add(html.slice(cursor, match.index))(match[1], true);
    cursor = match.index + match[0].length;
  }
  add(html.substr(cursor, html.length - cursor));
  code += 'return r.join("");';
  return new Function(code.replace(/[\r\t\n]/g, '')).apply(options);
}

  代码比我预想的还要少,只有区区15行!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn