现代模板引擎应具有的7大特征

  Java模板引擎已经发展了10余年,使用模板引擎者和模板引擎开发者都不思进取,得过且过,变化一直不大。毕竟,有技术含量值得屌丝追求的还是在后台。譬如早期的EJB,后来的WebService,SSH,还有现在的云计算,NOSQL等。但是,模板引擎没有变化并不代表已经非常成熟,它跟其他技术一样,也需要更新换代。程序员越来越认识到,模板开发在工作中占了较大工时,模板在渲染时占了CPU较大份额。现在国内外,已经有聪明的程序员意识到现在流行的模板引擎有些陈旧,不能满足更好的模板应用,纷纷提出新的模板引擎需求,甚至有些有才智的程序员已经开发出更好的模板引擎供大家使用。

这博文总结了本人长期实践以及来自其一线程序员的建议,列出了现代模板引擎应该具有的7大特征,并使用新一代Beetl模板语言作为说明。

  特征一,模板语言应该有较少的侵入性,以提高模板维护性

   由于模板语言总是“嵌入”到原来的文件中,减少对原有文件的侵入性对模板的维护是非常有利的。有的模板引擎的占位符号是“$变量名”,这就是一个典型侵入性过强的例子。因为对于html文件来说,一些js变量是以$开头(如Jquery),因此这样的模板即不好维护,而且模板引擎会误认为某些js变量也是占位符而导致解析混乱。

var userId = $userId;
 

    流行的Freemarker没有此问题,但更大的问题是它的控制语句定界符是"<#" ">",这样,进过它更改的HTML模板到处充满了<#符号,你再无法使用IE浏览器,Dreamweaver编辑工具进行编辑了。因为它不在是”HTML“了。     还有些以HTML为主要对象的模板引擎,把控制语句作为DOM一个节点属性,这样看似对原有文件侵入性低,使用IE或者Dreamweaver仍然能打开编辑,但仍然混淆了模板语言和HTML文件。我以为仍然不利于模板的维护。

<tr item="user" foreach="usersList" >
</tr>

    为了降低模板语言对原有的文件侵入性,Beetl是如何做的呢?

    Beetl允许自定义占位符号和控制语句,譬如在HTML中,可以设定控制语句定界符是"<!--#" "-->",这在HTML中看似一个注释片段,因此侵入性就非常低了

<!--# for(user in usersList){ -->
<tr>
<td>${user.name}</td>
</tr>
<!--# } -->
 

你也可以设置自己喜欢的占位符,如最通用的"${" "}",或者我曾经喜爱的"~" "~"。

关于倾入性,还有个这样的有趣问题,就是自己开发一个界面生成工具,通常采用模板生成jsp代码,就是很少有人用”模板生成模板代码“,这是因为对一些陈旧的模板引擎来说,会产生符号冲突。Beetl中则不会出现此问题,其他模板语会有这个问题。

特征二,性能良好才能省钱

   模板引擎渲染页面过程会占相当大部分的CPU,特别是一些后台只有少量计算的web应用,比如以数据库为中心的Web应用。选用高性能的模板引擎,是相当划算的选择,特别是对那些大的互联网公司,动不动就数百,数千台web服务器。省下一些CPU,就剩下一些钱。而且,高性能的模板引擎会给用户带来很好的Web使用体验。

  现在有才智的程序员已经把越来越多的提升模板引擎性能的方法应用于现代模板引擎,性能已经成倍的超过了原有的Freemarker,Velcoity。这些提升性能方法有:

  •   模板引擎将模板文件编译成class运行。
  •   模板中的静态部分采用二进制输出,不需要CPU运行的时候再转码
  •   合并模板中的静态部分一起输出,而不是每一行每一行输出

Beetl是为数不多同时具备上面三个优化策略的现代模板引擎.对于第一条编译成class,beetl比其他现代模板引擎走的更远一点,它是带类型的编译,尽量避免反射调用。而且,beetl不需求在模板中申明模板变量的Java类型,是通过运行时刻推测出来的,编译引擎非常先进。如下是一个模板

<html>
<body>
${user.name}/${user.role}<br/>
<%if(user.role == "admin"){%>
<table>
  <tr>
    <th>NO.</th>
    <th>Title</th>
    <th>Author</th>
    <th>Publisher</th>
    <th>PublicationDate</th>
    <th>Price</th>
    <th>DiscountPercent</th>
    <th>DiscountPrice</th>
  </tr>
  <%for(book in books){%>
  <%if(book.price > 0){%>
  <tr>
    <td>${book_index + 1}</td>
    <td>${book.title}</td>
    <td>${book.author}</td>
    <td>${book.publisher}</td>
    <td>${book.publication,dateFormat="yyyy-MM-dd HH:mm:ss"}</td>
    <td>${book.price}</td>
    <td>${book.discount}%</td>
    <td>${book.price * book.discount / 100}</td>
  </tr>
  <%}%>
  <%}%>
</table>
 

 编译后的代码

final User user;
        final ArrayList<Book> books;
        try{
            user = (User)ctx.getVarWithoutException("user");
            books = (ArrayList<Book>)ctx.getVarWithoutException("books");
        }catch(ClassCastException ex){
            //转入解释模式执行
            throw new VaribaleCastException(ex);
        }
        try{
            out.write(__V0);
            out.write(user.getName());
            out.write(__V1);
            out.write(user.getRole());
            out.write(__V2);
            out.write(__VCR);
            if(user.getRole().equals("admin")){
                out.write(__V3);
                int book_index = 0;
                int book_size = books.size();
                for(Book book : books){
                    if((new BeeNumber(book.getPrice()).compareTo(new BeeNumber(0))>0)){
                        out.write(__V6);
                        out.write(__V7);
                        out.write((new BeeNumber(book_index).add(new BeeNumber(1))));
                        out.write(__V8);
                        out.write(__VCR);
                        out.write(__V9);
                        out.write(book.getTitle());

//// 忽略其他代码
 private static final byte[] __V13 = new byte[]{0x20,0x20,0x20,0x20,0x3c,0x74,0x64,0x3e};
    private static final byte[] __V14 = new byte[]{0x3c,0x2f,0x74,0x64,0x3e};
    private static final byte[] __V1 = new byte[]{0x2f};
    private static final byte[] __V15 = new byte[]{0x20,0x20,0x20,0x20,0x3c,0x74,0x64,0x3e};
    private static final byte[] __V0 = new byte[]{0x3c,0x68,0x74,0x6d,0x6c,0x3e,0xd,0xa,0x3c,0x62,0x6f,0x64,0x79,0x3e,0xd,0xa};
    private static final byte[] __V16 = new byte[]{0x3c,0x2f,0x74,0x64,0x3e};
    private static final byte[] __V3 = new byte[]

如代码4行所示,模板变量user即使User对象,这是模板运行时推测出来,无需再模板中申明

如代码11行,原有文件中的<html><body>俩行合并成一行输出。不像传统JSP,编译成class是多行输出,会影响性能的

代码32以后是实现将静态文本转化为二进制,这样,IO输出非常快!

Beetl通过采用上面三个策略,性能是以传统模板引擎的2-4倍。一些现代模板引擎,如HTTL,以及我的好友Green写的rythm也采用了上面三个优化策略,从它们的测试报告中,某些测试性能超过了5倍Freemarker。

特征三, 良好的错误提示

早期在使用JSP过程中,由于JSP运行出错,但找不到具体出错原因,这让我们老一批的开发人员非常痛苦。后来的模板引擎改善了这点,具有良好的错误提示,能精确的提示道错误的行数,和可能的错误原因。现在模板引擎在这点上做的更好,以Beetl为例,能显示错误的行数,错误的符号,错误的原因,以及错误所在的文件,和错误上下三行。(并且都是可定制的),譬如如下模板有某处错误:

<html>
<body>
${user.name}/${user.role}<br/>
<%if(user.role == "admin"{%>
<table>
  <tr>
    <th>NO.</th>

 在编译运行时,会报如下错误

>>语法错:缺少符号')',4 行 文件 \beetl\book.txt
1|<html>
2|<body>
3|${user.name}/${user.role}<br/>
4|<%if(user.role == "admin"{%>
5|<table>
6|  <tr>

这样,开发者就很容易改正问题

JSP之所以连错误行数都无法准确报告出来,主要是JSP会编译成Class而失去了原有行数信息,在现代模板引擎中,此信息不会丢失,譬如作者的好友Green的模板引擎能将行数保存到生成的源代码里,可以根据错误间接查询出模板错误行数,Beetl在这方面走的更棒,能自动在错误时打印出错误行数。这是因为beetl生成class代码的时候,将模板中可能出错的行数与生成的Class行数关联起来

/* 行映射*/
    protected String lineMap = "-69=26-33=4-38=17-65=25-36=16-37=16-78=31-41=19-45=20-49=21-21=0-20=0-53=22-57=23-28=3-61=24-30=3-";

 以69=23为例子,表示class的69行出错,代表了模板中的26行出错

特征四, 支持渲染结果的再处理,实现灵活的模板功能

JSP标签库就是一种渲染结果再处理,譬如可以用JSP标签完成一个Cache标签。此标签可以缓存标签体的内容而不需要每次都去渲染。传统的模板引擎,如Freemarker也有这个功能。但某些速度很快的模板引擎却不具备此功能,这是很遗憾的。这样的模板除了作为代码生成器或者内容生成器外,并不适合作为动态Web输出。

Beetl 可以通过俩种方式支持渲染结果再处理,一种是类似JSP,Freemarker的标签库,以模板中的布局为例,beetl支持layout标签函数,可以轻易完成简单的布局功能,如下例子:

这是child页面
<%layout('/ext/layout_template.html'){%>
hello,我是${name}
<%}%>
 

 如代码所示,layout标签将使用/ext/layout_template.html作为布局页面,将”{“  ”}“的内容渲染后插入到模板页面

Beetl还有一种更为先进的渲染结果再处理的方式,即独一无二的模板变量功能,可以将模板渲染的结果作为模板变量保存起来以备用。如在复杂的布局要求中,通常要求模板页面的js部分插入到布局页面头部,动态html部分插入到布局页面的某处,采用别的模板引擎,这是几乎不可能实现的,使用Beetl,则很简单

<%
var jsPart = {
%>
<script>
//这是js部分,将放在布局页面的头部
</script>
<%};%>

<%var htmlPart={%>
<div>
这是html部分,将放在布局页面的底部
</div>
<%};%>

<%
var layoutParas = {"jsPart":jsPart,"htmlPart":htmlPart};
includeFileTemplate('/ext/complex_layout_template.html',layoutParas){}

%>
 

如上定义了俩个模板变量jsPart,和 htmlPart,渲染结果暂时保存到这俩个变量,然后再模板布局页面使用这俩个变量

特征五, 安全输出

   早期的JSP功能,经常因为空指针或者变量不存在而报错,这是缺少安全输出功能,Freemarker就做的特别好,可以用!表示如果变量为空或者不存在则输出!后面的值。Beetl也提供了同样的功能,如下模板中

<span>${user.wife.name!"单身"}</span>
<span>${[email protected]}</span>

上面例子第一行表示如果user为空,或者user.wife为空,或者user.wife.name为空,这输出”单身“。

第二行同样是安全输出,只是输出为java class的一个常量(在beetl中,直接调用class方法和属性是很简单的,只要加一个@符号即可,Beetl提供安全管理,架构师可以指定那些class能被调用,哪些class是不能调用,如Runtime类就不能被调用)

   遗憾的是,现代有些模板引擎还是不支持安全输出,只能说要么这些模板引擎的开发者还没有意识到安全输出的重要性,要么模板引擎还需要更长一段时间的完善

特征六,可测试的模板

   通常程序员测试模板是否正确,在MVC架构中,必须同时具备M V C 三个,只有很少模板引擎可以说只需要M 和 V,不需要C,现代模板引擎应该能做的更好,以Beetl为例,它支持仅有V的情况也能测试模板的正确性,这特别适合水平开发,即模板开发者,和后台开发者不是同一人的情况。

   Beetl是怎么做到的呢? 作为Beetl的作者,我花了很大精力才完成仅有V的情况下也能测试模板功能。譬如,如下简单模板

<span>this is template,${user.name},${sessions['userId']}</span>

 现在即没有User对象,也没有Web容器提供sessions,更没有控制层往session里设置一个userId属性,如何测试此模板呢?

   在Beetl中,提供SimpleTemplateTestUtil类用来完成模板测试,他提供俩个输入参数,第一个是模板,第二个是一个字符串,字符串包含了以json格式定义的变量,输出就是模板是否渲染无误,以及渲染结果

String input = "this is template,${user.name},${sessions['userId']}";
		String json = "var user = {'name':'joel'},sessions={'userId':'12345'};";
		Writer w = new StringWriter();
		SimpleTemplateTestUtil util = new SimpleTemplateTestUtil(input, json, w);
		util.run();
		System.out.println(util.isOk());
		System.out.println(w);

 输出是

true
this is template,joel,12345

Beetl的参与者之一 ”一颗草“正在做一个beetl模板在线体验网站,即将完成,有兴趣的人可以再网站上在线写模板,测试模板效果,这正是基于Beetl提供的可测试模板,这是独一无二的。

特征七,简洁的指令,良好的扩展性:易学易用

   现代模板引擎语言,我以为应该以更简单的语法,丰富的函数调用为主。去掉那些花哨的语言特性为好。以循环为例子,最为简单的语法莫过于大家都熟悉的形式,如for(xxx in xxxList), 有的现代模板引擎则是for( xxx << xxxList),<<符号在Java中意为这移位操作,这很容易让人误解,其实真的无须这样语法,看似迷人,其实一点不好用。

   还有的模板引擎如Freemarker,"?" ,"!" 满天飞,一个小小的模板,充满了?! 符号,是非常别扭的。

   Beetl提供的语法非常简单,类似JS。能很快的上手,我采用Beetl的项目的同事们,基本上能做到不看Guild文档而能完成大部分模板的开发。Beetl同时又具备当今流行模板的所有功能(甚至在功能上是超过了这些模板),是通过提供了丰富的函数功能来完成的,以将日期格式转化输出为例子,Freemarker提供了如下多的选择

${openingTime?string.short}
${openingTime?string.medium}
${openingTime?string.long}
${openingTime?string.full}
 

而Beetl仅仅提供一个dateFormat格式化函数

${lastUpdated,dateFormat='yyyy-MM-dd'}

如果你不满意此日期格式函数,你也可以非常方便自定义格式化日期函数,如下代码

group.registerFormat("shortDate", new Format(){

			@Override
			public Object format(Object data, String pattern)
			{
				Date d = (Date)data;
				return new SimpleDateFormat("yyyy-MM-dd").format(d);
			}
			
		}
		);
 

 你在模板中,可以调用shortDate格式化函数

${lastUpdated,shortDate}

像Freemarker这样已经很容易学的模板引擎,我在群里,经常能看到很多人问起Freemarker各种使用问题,对于各位使用者来说,初学的时候仍然是花了很长时间,各种使用潜在的问题让初学者猝不及防。这都是因为Freemarker过多的语法,以及新手对一门新的语法产生的语法习俗不适应导致的,我强烈介意那些头一次使用Freemarker的的程序员,谨慎考虑使用Freemarker带来的问题,尽管它是最流行的,但不代表对你的项目来说,是最理想的

    总结

十几年来,企业应用,互联网应用后端技术发生了很多变革,从分布式到Without EJB,从Database到Nosql,公司成本得到了很大降低。然而,对于架构师来说,同样至关重要前端技术,仍然没有变化,无非还是JSP技术,或者采用模板引擎,如Freemarker,Velocity。前端对于与产品以及和作为产品实现者开发人员来来说,仍然是费时费力一块,君不见,错误的使用前端技术,将抵消后台优化带来的用户体验,不适当的使用前端技术,将成倍增加开发和维护时间,尽管架构师们意识到到此问题,但可选方案几乎没有。这种现象长时间未曾变化!

可喜的是,越来越多的现代模板引擎正在涌现出来,在国内,有我闲.大赋的Beetl,有梁飞的HTTL,有走向国际化的我的好友Green的rythm,更多的我就不一一列出来。这些模板引擎,或多或少的合乎现代模板引擎7大特征。必将在发展使用的几年后,取代陈旧,不思进取的老的模板引擎。也希望国人在选用模板引擎的时候,可以多多考虑国内的模板引擎。

相关推荐