模板一般是指嵌入了某种动态编程语言代码的文本,数据和模板经过某种形式的结合,能够变化出不一样的结果。模板一般用来定义显示的形式,可以使得数据展示更为丰富,并且容易维护。例如,下面是一个模板的例子:javascript
<ul> <% for(var i in items){ %> <li class='<%= items[i].status %>'><%= items[i].text %></li> <% } %> </ul>
若是有以下items
数据:html
items:[ { text: 'text1' ,status:'done' }, { text: 'text2' ,status:'pending' }, { text: 'text3' ,status:'pending' }, { text: 'text4' ,status:'processing' } ]
经过某种方式的结合,能够产生下面的Html代码:java
<ul> <li class='done'>text1<li> <li class='pending'>text2<li> <li class='pending'>text3<li> <li class='processing'>text4<li> </ul>
若是不使用模板,想要达到一样的效果,即将上面的数据展示成结果的样子,须要像下面这样作:web
var temp = '<ul>'; for(var i in items){ temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>"; } temp += '</ul>';
能够看出使用模板有以下好处:编程
经过分析模板,将数据和模板结合在一块儿输出最后的结果的程序称为模板引擎,模板有不少种,相对应的模板引擎也有不少种。一种比较古老的模板称为ERB
,在不少的web框架中被采用,好比:ASP.NET
、 Rails
... 上面的例子就是ERB的例子。在ERB中两个核心的概念:evaluate
和interpolate
。表面上evaluate
是指包含在<% %>
中的部分,interpolate
是指包含在<%= %>
中的部分。从模板引擎的角度,evaluate
中的部分不会直接输出到结果中,通常用于过程控制;而interpolate
中的部分将直接输出到结果中。缓存
从模板引擎的实现上看,须要依赖编程语言的动态编译或者动态解释的特性,以简化实现和提升性能。例如:ASP.NET
利用.NET的动态编译,将模板编译成动态的类,并利用反射动态执行类中的代码。这种实现其实是比较复杂的,由于C#是一门静态的编程语言,可是使用javascript能够利用Function,以极少的代码实现一个简易的模板引擎。本文就来实现一个简易的ERB模板引擎,以展示javascript的强大之处。框架
针对上面的例子,回顾一下使用模板和不使用模板的差异:编程语言
模板写法:函数
<ul> <% for(var i in items){ %> <li class='<%= items[i].status %>'><%= items[i].text %></li> <% } %> </ul>
非模板写法:性能
var temp = '<ul>'; for(var i in items){ temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>"; } temp += '</ul>';
仔细观察,实际上这两种方法十分“类似”,可以找到某种意义上的一一对应。若是可以将模板的文本变成代码执行,那么就能实现模板转化。在转化过程当中有两个原则:
interpolate
(即<%= %>
),将其中的内容当成变量拼接在字符串中evaluate
(即<% %>
),直接当成代码将上面的例子按照上述原则进行变换,再添加一个总的函数:
var template = function(items){ var temp = ''; //开始变换 temp += '<ul>'; for(var i in items){ temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>"; } temp += '</ul>'; }
最后执行这个函数,传入数据参数便可:
var result = template(items);
可见上面的转化逻辑其实十分简单,可是关键的问题是,模板是变化的,这意味着生成的程序代码也必须是在运行时生成并执行的。好在javascript有许多动态特性,其中一个强大的特性就是Function。
咱们一般使用function
关键字在js中声明函数,不多用Function
。在js中function
是字面语法,js的运行时会将字面的function
转化成Function
对象,因此实际上Function
提供了更为底层和灵活的机制。
用 Function 类直接建立函数的语法以下:
var function_name = new Function(arg1, arg2, ..., argN, function_body)
例如:
//建立动态函数 var sayHi = new Function("sName", "sMessage", "alert(\"Hello \" + sName + sMessage);"); //执行 sayHi('Hello','World');
函数体和参数都可以经过字符串来建立!So cool!有了这个特性,能够将模板文本转化成函数体的字符串,这样就能够建立动态的函数来动态的调用了。
首先利用正则式来描述interpolate
和evaluate
,括号用来分组捕获:
var interpolate_reg = /<%=([\s\S]+?)%>/g; var evaluate_reg = /<%([\s\S]+?)%>/g;
为了对整个模板进行连续的匹配将这两个正则式合并在一块儿,可是注意,全部可以匹配interpolate的字符串都能匹配evaluate,因此interpolate须要有较高的优先级:
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g
设计一个函数用于转化模板,输入参数为模板文本字串和数据对象
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g //text: 传入的模板文本字串 //data: 数据对象 var template = function(text,data){ ... }
使用replace方法,进行正则的匹配和“替换”,实际上咱们的目的不是要替换interpolate
或evaluate
,而是在匹配的过程当中构建出“方法体":
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g //text: 传入的模板文本字串 //data: 数据对象 var template = function(text,data){ var index = 0;//记录当前扫描到哪里了 var function_body = "var temp = '';"; function_body += "temp += '"; text.replace(matcher,function(match,interpolate,evaluate,offset){ //找到第一个匹配后,将前面部分做为普通字符串拼接的表达式 function_body += text.slice(index,offset); //若是是<% ... %>直接做为代码片断,evaluate就是捕获的分组 if(evaluate){ function_body += "';" + evaluate + "temp += '"; } //若是是<%= ... %>拼接字符串,interpolate就是捕获的分组 if(interpolate){ function_body += "' + " + interpolate + " + '"; } //递增index,跳过evaluate或者interpolate index = offset + match.length; //这里的return没有什么意义,由于关键不是替换text,而是构建function_body return match; }); //最后的代码应该是返回temp function_body += "';return temp;"; }
至此,function_body
虽然是个字符串,但里面的内容其实是一段函数代码,能够用这个变量来动态建立一个函数对象,并经过data参数调用:
var render = new Function('obj', function_body); return render(data);
这样render
就是一个方法,能够调用,方法内部的代码由模板的内容构造,可是大体的框架应该是这样的:
function render(obj){ var temp = ''; temp += ... ... return temp; }
注意到,方法的形参是obj
,因此模板内部引用的变量应该是obj
:
<script id='template' type='javascript/template'> <ul> <% for(var i in obj){ %> <li class="<%= obj[i].status %>"><%= obj[i].text %></li> <% } %> </ul> </script>
看似到这里就OK了,可是有个必须解决的问题。模板文本中可能包含\r
\n
\u2028
\u2029
等字符,这些字符若是出如今代码中,会出错,好比下面的代码是错误的:
temp += ' <ul> ' + ... ;
咱们但愿看到的应该是这样的代码:
temp += '\n \t\t<ul>\n' + ...;
这样须要把\n
前面的\
转义成\\
便可,最终变成字面的\\n
。
另外,还有一个问题是,上面的代码没法将最后一个evaluate
或者interpolate
后面的部分拼接进来,解决这个问题的办法也很简单,只须要在正则式中添加一个行尾的匹配便可:
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g;
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g //模板文本中的特殊字符转义处理 var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\t': 't', '\u2028': 'u2028', '\u2029': 'u2029' }; //text: 传入的模板文本字串 //data: 数据对象 var template = function(text,data){ var index = 0;//记录当前扫描到哪里了 var function_body = "var temp = '';"; function_body += "temp += '"; text.replace(matcher,function(match,interpolate,evaluate,offset){ //找到第一个匹配后,将前面部分做为普通字符串拼接的表达式 //添加了处理转义字符 function_body += text.slice(index,offset) .replace(escaper, function(match) { return '\\' + escapes[match]; }); //若是是<% ... %>直接做为代码片断,evaluate就是捕获的分组 if(evaluate){ function_body += "';" + evaluate + "temp += '"; } //若是是<%= ... %>拼接字符串,interpolate就是捕获的分组 if(interpolate){ function_body += "' + " + interpolate + " + '"; } //递增index,跳过evaluate或者interpolate index = offset + match.length; //这里的return没有什么意义,由于关键不是替换text,而是构建function_body return match; }); //最后的代码应该是返回temp function_body += "';return temp;"; var render = new Function('obj', function_body); return render(data); }
调用代码能够是这样:
<script id='template' type='javascript/template'> <ul> <% for(var i in obj){ %> <li class="<%= obj[i].status %>"><%= obj[i].text %></li> <% } %> </ul> </script> ... var text = document.getElementById('template').innerHTML; var items = [ { text: 'text1' ,status:'done' }, { text: 'text2' ,status:'pending' }, { text: 'text3' ,status:'pending' }, { text: 'text4' ,status:'processing' } ]; console.log(template(text,items));
可见,咱们只用了不多的代码就实现了一个简易的模板。
还有几个细节的问题须要注意:
<%
或者%>
都是模板的边界字符,若是模板须要输出<%
或者%>
,那么须要设计转义的办法null
,显然不但愿最后输出'null'
,因此须要在function_body
的代码中考虑null
的状况obj
的形参引用数据,可能不太方便,能够在function_body
添加with(obj||{}){...}
,这样模板中能够直接使用obj
的属性render
返回出去,而不是返回转化的结果,这样外部能够缓存生成的函数,以提升性能