模板引擎随谈

template engine

模板引擎是为了解耦而产生的,从编程范型的角度来说,写模板属于“声明式(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)显示。最终拼接成字符串输出。

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

13,696 次阅读

发表评论

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

back to top