只有 20 行的 JavaScript 模板引擎实例详解
本文实例讲述了JavaScript模板引擎。分享给大家供大家参考,具体如下:
原文链接:JavaScripttemplateengineinjust20lines
(译者吐槽:只收藏不点赞都是耍流氓)
前言
我仍旧在为我的JS预处理器AbsurdJS进行开发工作。它原本是一个CSS预处理器,但之后它扩展成为了CSS/HTML预处理器,很快它将支持JS到CSS/HTML的转换。它就像一个模板引擎一样能够生成HTML代码,也就是说它能够用数据填充模板当中的标识片段。
因此,我希望去写一个可以满足我当前需求的模板引擎。AbsurdJS主要作为NodeJS的模块使用,但同时它也可以在客户端使用。为了这个目的,我无法使用市面上已经存在的模板引擎,因为它们几乎全都依赖于NodeJS,并且难以在浏览器中使用。我需要一个更小,纯JS写成的模板引擎。我浏览了这篇由JohnResig写的博客,似乎这正是我需要的东西。我把当中的代码稍作修改,并且浓缩到了20行。
这段代码的运行原理非常有趣,我将在这篇文章中一步一步为大家展示John的wonderfulidea。
1、提取标识片段
这是我们在开始的时候将要获得的东西:
varTemplateEngine=function(tpl,data){
//magichere...
}
vartemplate='Hello,mynameis<%name%>.I\'m<%age%>yearsold.
';
console.log(TemplateEngine(template,{
name:"Krasimir",
age:29
}));
一个简单的函数,传入模板和数据作为参数,正如你所想象的,我们想要得到以下的结果:
Hello,mynameisKrasimir.I'm29yearsold.
我们要做的第一件事就是获取模板中的标识片段<%...%>,然后用传入引擎中的数据去填充它们。我决定用正则表达式去完成这些功能。正则不是我的强项,所以大家将就一下,如果有更好的正则也欢迎向我提出。
varre=/<%([^%>]+)?%>/g;
我们将会匹配所有以<%开头以%>结尾的代码块,末尾的g(global)表示我们将匹配多个。有许多的方法能够用于匹配正则,但是我们只需要一个能够装载字符串的数组就够了,这正是exec所做的工作:
varre=/<%([^%>]+)?%>/g; varmatch=re.exec(tpl);
在控制台console.log(match)可以看到:
[ "<%name%>", "name", index:21, input: "Hello,mynameis<%name%>.I\'m<%age%>yearsold.
" ]
我们取得了正确的匹配结果,但正如你所看到的,只匹配到了一个标识片段<%name%>,所以我们需要一个while循环去取得所有的标识片段。
varre=/<%([^%>]+)?%>/g,match;
while(match=re.exec(tpl)){
console.log(match);
}
运行,发现所有的标识片段已经被我们获取到了。
2、数据填充与逻辑处理
在获取了标识片段以后,我们就要对它们进行数据的填充。使用.replace方法就是最简单的方式:
varTemplateEngine=function(tpl,data){
varre=/<%([^%>]+)?%>/g,match;
while(match=re.exec(tpl)){
tpl=tpl.replace(match[0],data[match[1]])
}
returntpl;
}
data={
name:"KrasimirTsonev",
age:29
}
OK,正常运行。但很明显这并不足够,我们当前的数据结构非常简单,但实际开发中我们将面临更复杂的数据结构:
{
name:"KrasimirTsonev",
profile:{age:29}
}
出现错误的原因,是当我们在模板中输入<%profile.age%>的时候,我们得到的data["profile.age"]是undefined的。显然.replace方法是行不通的,我们需要一些别的方法把真正的JS代码插入到<%和%>当中,就像以下栗子:
vartemplate='Hello,mynameis<%this.name%>.I\'m<%this.profile.age%>yearsold.
';
这看似不可能完成?John使用了newFunction,即通过字符串去创建一个函数的方法去完成这个功能。举个栗子:
varfn=newFunction("arg","console.log(arg+1);");
fn(2);//输出3
fn是个真正的函数,它包含一个参数,其函数体为console.log(arg+1)。以上代码等价于下列代码:
varfn=function(arg){
console.log(arg+1);
}
fn(2);//输出3
通过newFunction,我们得以通过字符串去创建一个函数,这正是我们所需要的。在创建这么一个函数之前,我们需要去构造这个它的函数体。该函数体应当返回一个最终拼接好了的模板。沿用前文的模板字符串,想象一下这个函数应当返回的结果:
return "Hello,mynameis"+ this.name+ ".I\'m"+ this.profile.age+ "yearsold.
";
显然,我们把模板分成了文本和JS代码。正如上述代码,我们使用了简单的字符串拼接的方式去获取最终结果,但是这个方法无法100%实现我们的需求,因为之后我们还要处理诸如循环之类的JS逻辑,像这样:
vartemplate=
'Myskills:'+
'<%for(varindexinthis.skills){%>'+
'<%this.skills[index]%>'+
'<%}%>';
如果使用字符串拼接,结果将会变成这样:
return
'Myskills:'+
for(varindexinthis.skills){+
''+
this.skills[index]+
''+
}
理所当然这会报错。这也是我决定参照John的文章去写逻辑的原因——我把所有的字符串都push到一个数组中,在最后才把它们拼接起来:
varr=[];
r.push('Myskills:');
for(varindexinthis.skills){
r.push('');
r.push(this.skills[index]);
r.push('');
}
returnr.join('');
下一步逻辑就是整理得到的每一行代码以便生成函数。我们已经从模板中提取出了一些信息,知道了标识片段的内容和位置,所以我们可以通过一个指针变量(cursor)去帮助我们取得最终的结果:
varTemplateEngine=function(tpl,data){
varre=/<%([^%>]+)?%>/g,
code='varr=[];\n',
cursor=0,match;
varadd=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+='returnr.join("");';//<--returntheresult
console.log(code);
returntpl;
}
vartemplate='Hello,mynameis<%this.name%>.I\'m<%this.profile.age%>yearsold.
';
console.log(TemplateEngine(template,{
name:"KrasimirTsonev",
profile:{age:29}
}));
变量code以声明一个数组为开头,作为整个函数的函数体。正如我所说的,指针变量cursor表示我们正处于模板的哪个位置,我们需要它去遍历所有的字符串,跳过填充数据的片段。另外,add函数的任务是把字符串插入到code变量中,作为构建函数体的过程方法。这里有一个棘手的地方,我们需要跳过标识符<%%>,否则当中的JS脚本将会失效。如果我们直接运行上述代码,结果将会是下面的情况:
varr=[];
r.push("Hello,mynameis");
r.push("this.name");
r.push(".I'm");
r.push("this.profile.age");
returnr.join("");
呃……这不是我们想要的。this.name和this.profile.age不应该带引号。我们改进一下add函数:
varadd=function(line,js){
js?code+='r.push('+line+');\n':
code+='r.push("'+line.replace(/"/g,'\\"')+'");\n';
}
varmatch;
while(match=re.exec(tpl)){
add(tpl.slice(cursor,match.index));
add(match[1],true);//<--saythatthisisactuallyvalidjs
cursor=match.index+match[0].length;
}
标识片段中的内容将通过一个boolean值进行控制。现在我们得到了一个正确的函数体:
varr=[];
r.push("Hello,mynameis");
r.push(this.name);
r.push(".I'm");
r.push(this.profile.age);
returnr.join("");
接下来我们要做的就是生成这个函数并且运行它。在这个模板引擎的末尾,我们用以下代码去代替直接返回一个tpl对象:
returnnewFunction(code.replace(/[\r\t\n]/g,'')).apply(data);
我们甚至不需要向函数传递任何的参数,因为apply方法已经为我们完整了这一步工作。它自动设置了作用域,这也是为什么this.name可以运行,this指向了我们的data。
3、代码优化
大致上已经完成了。最后一件事情,我们需要支持更多复杂的表达式,像if/else表达式和循环等。让我们用同样的例子去尝试运行下列代码:
vartemplate=
'Myskills:'+
'<%for(varindexinthis.skills){%>'+
'<%this.skills[index]%>'+
'<%}%>';
console.log(TemplateEngine(template,{
skills:["js","html","css"]
}));
结果将会报错,错误为UncaughtSyntaxError:Unexpectedtokenfor。仔细观察,通过code变量我们可以找出问题所在:
varr=[];
r.push("Myskills:");
r.push(for(varindexinthis.skills){);
r.push("");
r.push(this.skills[index]);
r.push("");
r.push(});
r.push("");
returnr.join("");
包含着for循环的代码不应该被push到数组当中,而是直接放在脚本里面。为了解决这个问题,在把代码push到code变量之前我们需要多一步的判断:
varre=/<%([^%>]+)?%>/g,
reExp=/(^()?(if|for|else|switch|case|break|{|}))(.*)?/g,
code='varr=[];\n',
cursor=0;
varadd=function(line,js){
js?code+=line.match(reExp)?line+'\n':'r.push('+line+');\n':
code+='r.push("'+line.replace(/"/g,'\\"')+'");\n';
}
我们添加了一个新的正则。这个正则的作用是,如果一段JS代码以if,for,else,switch,case,break,|开头,那它们将会直接添加到函数体中;如果不是,则会被push到code变量中。下面是修改后的结果:
varr=[];
r.push("Myskills:");
for(varindexinthis.skills){
r.push("");
r.push(this.skills[index]);
r.push("");
}
r.push("");
returnr.join("");
理所当然的正确执行啦:
Myskills:js html css
接下来的修改会给予我们更强大的功能。我们可能会有更加复杂的逻辑会放进模板中,像这样:
vartemplate=
'Myskills:'+
'<%if(this.showSkills){%>'+
'<%for(varindexinthis.skills){%>'+
'<%this.skills[index]%>'+
'<%}%>'+
'<%}else{%>'+
'none
'+
'<%}%>';
console.log(TemplateEngine(template,{
skills:["js","html","css"],
showSkills:true
}));
进行过一些细微的优化之后,最终的版本如下:
varTemplateEngine=function(html,options){
varre=/<%([^%>]+)?%>/g,reExp=/(^()?(if|for|else|switch|case|break|{|}))(.*)?/g,code='varr=[];\n',cursor=0,match;
varadd=function(line,js){
js?(code+=line.match(reExp)?line+'\n':'r.push('+line+');\n'):
(code+=line!=''?'r.push("'+line.replace(/"/g,'\\"')+'");\n':'');
returnadd;
}
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+='returnr.join("");';
returnnewFunction(code.replace(/[\r\t\n]/g,'')).apply(options);
}
优化后的代码甚至少于15行。
后记(译者注)
这是我第一次完整地翻译文章,语句多有错漏还请多多谅解,今后将继续努力,争取把更多优质的文章翻译分享。
由于对前端的框架、模板引擎一类的工具特别感兴趣,非常希望能够学习当中的原理,于是乎找了个相对简单的模板引擎开刀进行研究,google后看到了这篇文章觉得非常优秀,一步步讲解生动且深入,代码经过本人测试均能正确得到文章描述的结果。
模板引擎有多种设计思路,本文仅仅为其中的一种,其性能等参数还有待测试和提高,仅供学习使用。
谢谢大家~
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。