AbsurdJS这篇教程的作者一步步教你用Javascript实现一个纯客户端的模板引擎。整个引擎实现只需要不到20行代码。如果能从头看到尾,还是有很多收获的。你甚至可以跟随大牛的脚步,自己写一个引擎。以下为全文。不知道大家有没有听说过一个基于Javascript的网页预处理器,叫做AbsurdJS。我是它的作者,我仍在努力。一开始只是打算写一个CSS预处理器,后来扩展到CSS和HTML,可以用来将Javascript代码转换成CSS和HTML代码。当然,既然可以生成HTML代码,你也可以用它作为模板引擎,用标记语言填充数据。所以我想知道我是否可以编写一些简单的代码来改进模板引擎并与其他现有逻辑一起工作。AbsurdJS本身主要作为NodeJS模块分发,但它也发布客户端版本。考虑到这一点,我不能只使用现有的引擎,因为它们中的大多数都在NodeJS上运行,而不是在浏览器上运行。我需要的是一个直接在浏览器上运行的小型纯Javascript。有一天,当我偶然发现JohnResig的这个博客时,我惊喜地发现这正是我要找的东西!我稍微修改了一下,代码行数大概20行。这个逻辑非常有趣。在这篇文章中,我将一步步重现编写这个引擎的过程。如果你能一路看完,你就会明白约翰的想法是多么的犀利!起初我的想法是这样的:varTemplateEngine=function(tpl,data){//magichere...}vartemplate='
Hello,mynameis<%name%>.I\'m<%age%>岁。
';console.log(TemplateEngine(模板,{name:"Krasimir",age:29}));一个简单的函数,输入是我们的模板和数据对象,输出大概你很容易想象,像这样:你好,mynameisKrasimir.I'm29yearsold。
第一步是找到模板参数并用传递给引擎的特定数据替换它们。我决定在此步骤中使用正则表达式。不过这方面我不是最擅长的,所以如果写的不好,欢迎大家喷。varre=/<%([^%>]+)?%>/g;此正则表达式将捕获所有以<%开头并以%>结尾的片段。末尾的参数g(global)表示不只匹配一个,而是匹配所有匹配的片段。在Javascript中有很多使用正则表达式的方法。我们需要的是根据正则表达式输出一个包含所有字符串的数组,这正是exec所做的。varre=/<%([^%>]+)?%>/g;varmatch=re.exec(tpl);如果我们使用console.log打印出变量match,我们将看到:["<%name%>","name",index:21,input:"Hello,mynameis<%name%>.I\'m<%age%>yearsold.
"]但是我们可以看到返回的数组只包含第一个匹配项。我们需要用一个while循环将上面的逻辑包裹起来,这样我们就可以得到所有的匹配项。varre=/<%([^%>]+)?%>/g;while(match=re.exec(tpl)){console.log(match);}如果你再次运行上面的代码,你会看到<%name%>和<%age%>都被打印出来了。现在,有趣的部分来了。在模板中识别匹配项后,我们将它们替换为传递给函数的实际数据。最简单的方法是使用替换功能。我们可以这样写:varTemplateEngine=function(tpl,data){varre=/<%([^%>]+)?%>/g;while(match=re.exec(tpl)){tpl=tpl.replace(match[0],data[match[1]])}returntpl;}好的,这行得通,但还不够好。这里我们使用一个简单的对象以data["property"]的形式传递数据,但在现实中我们可能需要更复杂的嵌套对象。所以我们稍微修改了数据对象:{name:"KrasimirTsonev",profile:{age:29}}但是这样写的话是跑不起来的,因为如果在<%profile.age%>中使用模板,代码将替换为data['profile.age'],结果未定义。这样我们就不能简单的使用replace函数,而是使用其他的方法。如果能在<%和%>之间直接使用Javascript代码就好了,这样就可以直接计算传入的数据,像这样:vartemplate='Hello,mynameis<%this.name%>.I\'m<%this.profile.age%>岁。
';你可能很好奇,这是如何实现的?这里John使用新的Function语法创建一个基于字符串的函数。我们来看一个例子:varfn=newFunction("arg","console.log(arg+1);");fn(2);//outputs3fn是一个真正的函数。它接受一个参数,函数体是console.log(arg+1);.上面的代码相当于下面的代码:varfn=function(arg){console.log(arg+1);}fn(2);//outputs3通过这个方法,我们可以构造基于字符串的函数,包括它的参数和函数体。这不正是我们想要的吗!不过别着急,在构造函数之前,我们先来看看函数体长什么样。按照之前的思路,模板引擎最终应该返回一个编译好的模板。还是以之前的模板字符串为例,返回的内容应该类似:return"Hello,mynameis"+this.name+".I\'m"+this.profile.age+"yearsold.
";当然,在实际的模板引擎中,我们会将模板分成小块的文本和有意义的Javascript代码。早些时候你可能已经看到我使用简单的字符串连接来达到预期的效果,但这并不是我们100%想要的。由于用户可能会传递更复杂的Javascript代码,我们这里需要另一个循环,如下:vartemplate='Myskills:'+'<%for(varindexinthis.skills){%>'+'你好,我的名字是<%this.name%>.I\'m<%this.profile.age%>yearsold.
';console.log(TemplateEngine(template,{name:"KrasimirTsonev",profile:{age:29}}));上面代码中的变量code就是保存的函数体。第一部分定义了一个数组。cursor光标告诉我们当前解析到模板中的哪个位置。我们需要依靠它来遍历整个模板字符串。另外还有一个函数add,负责将解析后的代码行添加到可变代码中。有一个地方需要特别注意,就是代码中包含的双引号字符需要进行转义(escape)。否则生成的函数代码是错误的。如果我们运行上面的代码,我们将在控制台中看到以下内容: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是不应该有引号的,待会再改吧。varadd=function(line,js){js?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);//<--saythatthisisactuallyvalidjscursor=match.index+match[0].length;}占位符的内容和一个布尔值作为参数传递给add函数进行区分。这将生成我们想要的函数体。varr=[];r.push("
Hello,mynameis");r.push(this.name);r.push(".I'm");r.push(this.profile.age);returnr.join("");剩下要做的就是创建函数并执行它。因此,在模板引擎底层,将原来返回模板字符串的语句替换为如下内容:returnnewFunction(code.replace(/[\r\t\n]/g,'')).apply(data);我们甚至不需要明确地将参数传递给这个函数。我们使用apply方法来调用它。它会自动设置函数执行的上下文。这就是我们可以在函数内部使用this.name的原因。这里this指向数据对象。模板引擎已经差不多完成了,但是还有一点,我们需要支持更复杂的语句,比如条件判断和循环。让我们继续上面的例子。vartemplate='Myskills:'+'<%for(varindexinthis.skills){%>'+'
无
'+'<%}%>';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;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("");';返回newFunction(code.replace(/[\r\t\n]/g,'')).apply(options);}代码比我预想的少,只有15行!