模板引擎是为了解耦而产生的,从编程范型的角度来说,写模板属于 “声明式(Imperative)编程”。JSP 大概是最早接触也是最基础的模板引擎,本来写 Servlet 嘛,一大堆一大堆的 print,实在是没有任何结构性可言,然后 JSP 出现,先被处理成实质为 Servlet 的 Java 文件,编译以后变成 class,接着一样执行。所以本质是编译型的模板引擎,当然模板引擎也有解释型或者二者混合的。通常说来编译型的执行效率要高得多。只要是和显示相关的编程语言,都会发展出一套或者 N 套模板引擎,用得多了觉得很多情况下都大同小异。
几年前我在工作中折腾过一段时间的服务端模板引擎,最早遗留系统使用的 Velocity,后来我们实现的时候用了 FreeMarker,因为后者功能更强大,IDE 支持也更好,对于后者的 macro(宏),实在是不知怎么讲,功能上它当然是一个强大的武器,但是没控制好就会让代码写得功能不清,或者干脆很难看懂。在搞性能调优的时候,到后来不动大刀已经没有什么可以值得改进的地方了。遂眼光瞄到了 FreeMarker 上面,我们拿 profiler 的工具检查出来模板引擎的解释执行耗费了大量的时间,而且其中的模板缓存命中率很低,公司里面有一个团队为此专门改了 FreeMarker 的代码,性能好像有 20% 的提高。
很多人搞 web 开始阶段都是自由生长的,或者说野蛮生长,完全没有章法,凭借着搜索引擎加试错大法,因此方法往往都不正统。我也一样。在我知道专门的模板以前,我已经在粗暴地实现类似的事情了,让一个 DIV 不可见(display=none),然后里面变化的地方用占位符标识,在 Ajax 获得数据以后把占位符替换成真正的文字,然后显示出来——这不就是一最土鳖的模板么?后来开始接触到一些前端模板引擎,Mustache 是最早接触的,我不知道 {{ }} 这样的记号是不是从它开始的,然后是 Handlebars,其实它用的也是 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 模板引擎的设计思路,尽量把工作放到预编译阶段去,生成函数以后,原始的模板就不再使用了,后面每次需要渲染的时候调用这个函数传入参数就可以了。
通过一个小小的例子,可以看到模板引擎的工作原理,这里拿 Handlerbars 举例:
<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)显示。最终拼接成字符串输出。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》