模板引擎随谈

模板引擎随谈

模板引擎是为了解耦而产生的,从编程范型的角度来说,写模板属于“声明式(Imperative)编程”。JSP大概是最早接触也是最基础的模板引擎,本来写Servlet嘛,一大堆一大堆的print,实在是没有任何结构性可言,然后JSP出现,先被处理成实质为Servlet的Java文件,编译以后变成class,接着一样执行。所以本质是编译型的模板引擎,当然模板引擎也有解释型或者二者混合的。通常说来编译型的执行效率要高得多。只要是和显示相关的编程语言,都会发展出一套或者N套模板引擎,用得多了觉得很多情况下都大同小异。

几年前我在工作中折腾过一段时间的服务端模板引擎,最早遗留系统使用的Velocity,后来我们实现的时候用了FreeMarker,因为后者功能更强大,IDE支持也更好,对于后者的macro(宏),实在是不知怎么讲,功能上它当然是一个强大的武器,但是没控制好就会让代码写得功能不清,或者干脆很难看懂。在搞性能调优的时候,到后来不动大刀已经没有什么可以值得改进的地方了。遂眼光瞄到了FreeMarker上面,我们拿profiler的工具检查出来模板引擎的解释执行耗费了大量的时间,而且其中的模板缓存命中率很低,公司里面有一个团队为此专门改了FreeMarker的代码,性能好像有20%的提高。

很多人搞web开始阶段都是自由生长的,或者说野蛮生长,完全没有章法,凭借着搜索引擎加试错大法,因此方法往往都不正统。我也一样。在我知道专门的模板以前,我已经在粗暴地实现类似的事情了,让一个DIV不可见(display=none),然后里面变化的地方用占位符标识,在Ajax获得数据以后把占位符替换成真正的文字,然后显示出来——这不就是一最土鳖的模板么?后来开始接触到一些前端模板引擎,Mustache是最早接触的,我不知道 {{ }} 这样的记号是不是从它开始的,然后是Handlebar.js,其实它用的也是Mustache的引擎。Underscore.js是值得推荐的模板引擎,性能非常出色,而且语法和JSP差不多。AngularJS的模板是我最喜欢的形式(下面我列出了一个官网上面的例子),因为直接融合进HTML里面了,减少了生硬的特殊格式标签,可以给既有DOM对象增加属性,也可以通过directive方式自定义DOM。模板引擎怎么演进而来的,又是怎么从后端移到前端来的,其实都因一个“解耦”,这个过程我在《MVC框架的映射和解耦》以及《Web页面的聚合技术》里面都有部分介绍。

<ul>
  <li ng-repeat="phone in phones">
    {{phone.name}}
    <p>{{phone.snippet}}</p>
  </li>
</ul>

关于常见几款前端模板的比较,这里有一篇文章。HTML5用新标签的方式收录了模板,这里有一篇文章介绍。另外,这里有一个有趣的帖子,作者在入门Node.js的时候选模板,很多人在讨论Jade,它最有意思的地方是如果打开普通的没有代码辅助的记事本文件,它的编写效率真得高出好多,而且没有烦人的括号、尖括号之类的标记符号,不知道你怎么看。对于性能的横向比较,在JSPerf上面有人做了一个完整的列表,可以打开页面后立即测试

关于模板引擎的原理解析,推荐一篇文章《高性能JavaScript模板引擎原理解析》,里面提到了“高性能”模板引擎的原理,这也是现在越来越多的JavaScript模板引擎的设计思路,尽量把工作放到预编译阶段去,生成函数以后,原始的模板就不再使用了,后面每次需要渲染的时候调用这个函数传入参数就可以了。

通过一个小小的例子,可以看到模板引擎的工作原理,这里拿Handlerbar.js举例:

<table>
    {{#each users}}
    <tr>
        <td>
            {{this.name}}
        </td>
        <td>
            {{this.age}}
        </td>
    </tr>
    {{/each}}
</table>

对于这样一段简单的模板,调用语句是:

var func = Handlebars.compile(document.getElementById("template").innerHTML);
var result = func({
	users : [
		{
			name : "A",
			age : 10
		},
		{
			name : "B",
			age : 20
		}
	]
});
console.log(result);

接着动态生成了这样的Function:

this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
  var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;

function program1(depth0,data) {
  
  var buffer = "", stack1;
  buffer += "\n	<tr>\n		<td>"
    + escapeExpression(((stack1 = depth0.name),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
    + "</td>\n		<td>"
    + escapeExpression(((stack1 = depth0.age),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
    + "</td>\n	</tr>\n	";
  return buffer;
  }

  buffer += "\n<table>\n	";
  stack1 = helpers.each.call(depth0, depth0.users, {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
  if(stack1 || stack1 === 0) { buffer += stack1; }
  buffer += "\n</table>\n";
  return buffer;

其实代码并不难理解,这里的each就是通过内置的工具方法helpers.each来实现的,执行总的来说就是递归调用(第9、11行),如果stack1还是方法就继续调用,否则就直接转码(escapeExpression)显示。最终拼接成字符串输出。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:

发表评论

电子邮件地址不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Preview on Feedage: