JFinal

登录 注册

1 快速上手

2 JFinalConfig

3 Controller

4 AOP

5 ActiveRecord

6 Enjoy 模板引擎

7 EhCachePlugin

8 RedisPlugin

9 Cron4jPlugin

10 Validator

11 国际化

12 Json 转换

13 JFinal架构及扩展

14 升级到 3.5

6.4 指令

    JFinal Template Engine一如既往地坚持极简设计,核心只有#if、#for、#set、#include、#define、#(…)这六个指令,便实现了传统模板引擎几乎所有的功能,用户如果有任意一门程序语言基础,学习成本几乎为零。

    如果官方提供的指令无法满足需求,还可以极其简单地在模板语言的层面对指令进行扩展,在com.jfinal.template.ext.directive 包下面就有五个扩展指令,Active Record 的 sql 模块也针对sql管理功能扩展了三个指令,参考这些扩展指令的代码,便可无师自通,极为简单。

    注意,JFinal模板引擎指令的扩展是在词法分析、语法分析的层面进行扩展,与传统模板引擎的自定义标签类的扩展完全不是一个级别,前者可以极为全面和自由的利用模板引擎的基础设施,在更加基础的层面以极为简单直接的代码实现千变万化的功能。参考 Active Record的 sql 管理模块,则可知其强大与便利。

1、输出指令#( )

    与几乎所有 java 模板引擎不同,JFinal Template Engine消灭了插值指令这个原本独立的概念,而是将其当成是所有指令中的一员,仅仅是指令名称省略了而已。因此,该指令的定界符与普通指令一样为小括号,从而不必像其它模板引擎一样引入额外的如大括号般的定界符。

    #(…)输出指令的使用极为简单,只需要为该指令传入前面6.4节中介绍的任何表达式即可,指令会将这些表达式的求值结果进行输出,特别注意,当表达式的值为null时没有任何输出,更不会报异常。所以,对于 #(value) 这类输出不需要对value进行null值判断,如下是代码示例:

#(value)
#(object.field)
#(object.field ??)
#(a > b ? x : y)
#(seoTitle ?? "JFinal 俱乐部")
#(object.method(), null)

    如上图所示,只需要对输出指令传入表达式即可。注意上例中第一行代码value参数可以为null,而第二行代码中的object为null时将会报异常,此时需要使用第三行代码中的空合安全取值调用运算符:object.field ??

    此外,注意上图最后一行代码中的输出指令参数为一个逗号表达式,逗号表达式的整体求值结果为最后一个表达式的值,而输出指令对于null值不做输出,所以这行代码相当于是仅仅调用了object.method()方法去实现某些操作。

    输出指令可以自由定制,只需要继承 OutputDirectiveFactory 类并覆盖其中的 getOutputDirective 方法,然后在configEngine(Engine me)方法中,通过 me. setOutputDirectiveFactory(…) 切换即可。

2、#if 指令

    直接举例:

#if(cond)
  ...
#end

    如上图所示,if指令需要一个cond表达式作为参数,并且以#end为结尾符,cond可以为6.3章节中介绍的所有表达式,包括逗号表达式,当cond求值为true时,执行if分支之中的代码。

    if 指令必然支持#else if 与#else分支块结构,以下是示例:

#if(c1)
  ...
#else if(c2)
  ...
#else if (c3)
  ...
#else
  ...
#end

    由于#else if、#else用法与java语法完全一样,在此不在赘述。(jf 3.3版添加了对# else if 风格的支持,也即else与if之间可以有空白字符)

3、#for 指令

    JFinal Template Engine 对for 指令进行了极为人性化的扩展,可以对任意类型数据进行迭代输出,包括支持null值迭代。以下是代码示例:

#for(x : list)
  #(x.field)
#end

#for(x : map)
  #(x.key)
  #(x.value)
#end

    上图代码中展示了for指令迭代输出。第一个for指令是对list进行迭代输出,用法与java语法完全一样,第二个for指令是对map进行迭代,取值方式为item.key与item.value。

注意:当被迭代的目标为null时,不需要做null值判断,for指令会直接跳过null值,不进行迭代。

     for指令还支持对其状态进行获取,代码示例:

#for(x : listAaa)
  #(for.index)
  #(x.field)
  
  #for(x : listBbb)
     #(for.outer.index)
     #(for.index)
     #(x.field)
  #end
#end

     以上代码中的#(for.index)、#(for.outer.index)是对for指令当前状态值进行获取,前者是获取当前for指令迭代的下标值(从0开始的整数),后者是内层for指令获取上一层for指令的状态。这里注意for.outer这个固定的用法,专门用于在内层for指令中引用上层for指令状态。

     注意:for指令嵌套时,各自拥有自己的变量名作用域,规则与java语言一致,例如上例中的两个#(x.field)处在不同的for指令作用域内,会正确获取到所属作用域的变量值。

    for指令支持的所有状态值如下示例:

#for(x : listAaa)
   #(for.size)    被迭代对象的 size 值
   #(for.index)   从 0 开始的下标值
   #(for.count)   从 1 开始的记数值
   #(for.first)   是否为第一次迭代
   #(for.last)    是否为最后一次迭代
   #(for.odd)     是否为奇数次迭代
   #(for.even)    是否为偶数次迭代
   #(for.outer)   引用上层 #for 指令状态
#end

     具体用法在上面代码中用中文进行了说明,在此不再赘述。

     除了Map、List以外,for指令还支持Collection、Iterator、array普通数组、Iterable、Enumeration、null值的迭代,用法在形式上与前面的List迭代完全相同,都是#for(id : target)的形式,对于null值,for指令会直接跳过不迭代。

     此外,for指令还支持对任意类型进行迭代,此时仅仅是对该对象进行一次性迭代,如下所示:

#for(x : article)
   #(x.title)
#end

    上例中的article为一个普通的java对象,而非集合类型对象,for循环会对该对象进行一次性迭代操作,for表达式中的x即为article对象本身,所以可以使用#(x.title)进行输出。

    for 指令还支持#else分支语句,在for指令迭代次数为0时,将执行#else分支内部的语句,如下是示例:

#for(blog : blogList)
   #(blog.title)
#else
   您还没有写过博客,点击此处<a href="/blog/add">开博</a>
#end

     以上代码中,当blogList.size() 为0或者blogList为null值时,也即迭代次数为0时,会执行#else分支,这种场景在web项目中极为常见。

    最后,除了上面介绍的for指令迭代用法以外,还支持更常规的for语句形式,以下是代码示例:

#for(i = 0; i < 100; i++)
   #(i)
#end

    与java语法基本一样,唯一的不同是变量声明不需要类型,直接用赋值语句即可,JFinal Template Engine中的变量是动态弱类型。

    注意:以上这种形式的for语句,比前面的for迭代少了for.size与for.last两个状态,只支持如下几个状态:for.index、for.count、for.first、for.odd、for.even、for.outer

    #for 指令还支持 #continue、#break 指令,用法与java完全一致,在此不再赘述。

4、#set 指令

    set指令用于声明变量同时对其赋值,也可以是为已存在的变量进行赋值操作。set指令只接受赋值表达式,以及用逗号分隔的赋值表达式列表,如下是代码示例:

#set(x = 123)
#set(a = 1, b = 2, c = a + b)
#set(array[0] = 123)
#set(map["key"] = 456)

#(x)  #(c)  #(array[0])  #(map.key)  #(map["key"])

    以上代码中,第一行代码最为简单为x赋值为123,第二行代码是一个赋值表达式列表,会从左到右依次执行赋值操作,如果等号右边出现表达式,将会对表达式求值以后再赋值。最后一行代码是输出上述赋值以后各变量的值,其她所有指令也可以像输出指令一样进行变量的访问。

    请注意,#for、#include、#define这三个指令会开启新的变量名作用域,#set指令会首先在本作用域中查找变量是否存在,如果存在则对本作用域中的变量进行操作,否则继续向上层作用域查找,找到则操作,如果找不到,则将变量定义在顶层作用域中,这样设计非常有利于在模板中传递变量的值。

    当需要明确指定在本层作用域赋值时,可以使用#setLocal指令,该指令所需参数与用法与#set指令完全一样,只不过作用域被指定为当前作用域。#setLocal 指令通常用于#define、#include指令之内,用于实现模块化,从而希望其中的变量名不会与上层作用域发生命名上的冲突。

5、#include 指令

    include指令用于将外部模板内容包含进来,被包含的内容会被解析成为当前模板中的一部分进行使用,如下是代码示例:

#include("sidebar.html")

    #include 指令第一个参数必须为 String 常量,当以 ”/” 打头时将以 baseTemplatePath 为相对路径去找文件,否则将以使用 #include 指令的当前模板的路径为相对路径去找文件。

    baseTemplatePath 可以在 configEngine(Engine me) 中通过 me.setBaseTemplatePath(…) 进行配置。

    此外,include指令支持传入无限数量的赋值表达式,十分有利于模块化,例如:如下名为 ”_hot_list.html” 的模板文件用于展示热门项目、热门新闻等等列表:

<div class="hot-list">
  <h3>#(title)</h3>
  <ul>
    #for(x : list)
    <li>
      <a href="#(url)/#(x.id)">#(x.title)</a>
    </li>
    #end
  </ul>
</div>

    上图中的 title、list、url 是该html片段需要的变量,使用include指令分别渲染“热门项目”与“热门新闻”的用法如下:

#include("_hot_list.html", title="热门项目", list=projectList, url="/project")
#include("_hot_list.html", title="热门新闻", list=newsList, url="/news")

    上面两行代码中,为“_hot_list.html”中用到的三个变量title、list、url分别传入了不同的值,实现了对“_hot_list.html”的模块化重用。

6、#render 指令

    render指令在使用上与include指令几乎一样,同样也支持无限量传入赋值表达式参数,主要有两点不同:

  • render指令支持动态化模板参数,例如:#render(temp),这里的temp可以是任意表达式,而#include指令只能使用字符串常量:#include(“abc.html”)

  • render指令中#define定义的模板函数只在其子模板中有效,在父模板中无效,这样设计非常有利于模块化

    引入 #render 指令的核心目的在于支持动态模板参数。

7、#define 指令

    #define指令是模板引擎主要的扩展方式之一,define指令可以定义模板函数(Template Function)。通过define指令,可以将需要被重用的模板片段定义成一个个的 template function,在调用的时候可以通过传入参数实现千变万化的功能。

    在此给出使用define指令实现的layout功能,首先创建一个layout.html文件,其中的代码如下:

#define layout()
<html>
  <head>
    <title>JFinal俱乐部</title>
  </head>
  <body>
    #@content()
  </body>
</html>
#end

     以上代码中通过#define layout()定义了一个名称为layout的模板函数,定义以#end结尾,其中的#@content()表示调用一个名为content的模板函数。

    特别注意:模板函数的调用比指令调用多一个@字符,是为了与指令调用区分开来。

    接下来再创建一个模板文件,如下所示:

#include("layout.html")
#@layout()

#define content()
<div>
   这里是模板内容部分,相当于传统模板引擎的 nested 的部分
</div>
#end

    上图中的第一行代码表示将前面创建的模板文件layout.html包含进来,第二行代码表示调用layout.html中定义的layout模板函数,而这个模板函数中又调用了content这个模板函数,该content函数已被定义在当前文件中,简单将这个过程理解为函数定义与函数调用就可以了。注意,上例实现layout功能的模板函数、模板文件名称可以任意取,不必像velocity、freemarker需要记住 nested、layoutContent这样无聊的概念。

    通常作为layout的模板文件会在很多模板中被使用,那么每次使用时都需要#include指令进行包含,本质上是一种代码冗余,可以在configEngine(Engine me)方法中,通过me.addSharedFunction("layout.html")方法,将该模板中定义的所有模板函数设置为共享的,那么就可以省掉#include(…),通过此方法可以将所有常用的模板函数全部定义成类似于共享库这样的集合,极大提高重用度、减少代码量、提升开发效率。

    JFinal Template Engine彻底消灭掉了layout、nested、macro这些无需有的概念,极大降低了学习成本,并且极大提升了扩展能力。模板引擎本质是一门程序语言,任何可用于生产环境的语言可以像呼吸空气一样自由地去实现layout这类功能。

    此外,模板函数必然支持形参,用法与java规则基本相同,唯一不同的是不需要指定参数类型,只需要参数名称即可,如下是代码示例:

#define test(a, b, c)
   #(a)
   #(b)
   #(c)
#end

    以上代码中的模板函数test,有a、b、c三个形参,在函数体内仅简单对这三个变量进行了输出,注意形参必须是合法的java标识符,形参的作用域为该模板函数之内符合绝大多数程序语言习惯,以下是调用该模板函数的例子代码:

#@test(123, "abc", user.name)

    以上代码中,第一个参数传入的整型123,第二个是字符串,第三个是一个field取值表达式,从例子可以看出,实参可以是任意表达式,在调用时模板引擎会对表达式求值,并逐一赋值给模板函数的形参。

    注意:形参与实参数量要相同,如果实参偶尔有更多不确定的参数要传递进去,可以在调用模板函数代码之前使用#set指令将值传递进去,在模板函数内部可用空合安全取值调用表达式进行适当控制,具体用法参考jfinal-club项目中的_paginate.html中的append变量的用法。

    define还支持return指令,可以在模板函数中返回,但不支持返回值。

8、模板函数调用

    调用define定义的模板函数的格式为:#@name(p1, p2…, pn),模板函数调用比指令调用多一个@字符,多出的@字符用来与指令调用区别开来。

    此外,模板函数还支持安全调用,格式为:#@name?(p1, p2…, pn),安全调用只需在模板函数名后面添加一个问号即可。安全调用是指当模板函数未定义时不做任何操作。

    安全调用适合用于一些模板中可有可无的内容部分,以下是一个典型应用示例:

#define layout()
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="/assets/css/jfinal.css">
    #@css?()
  </head>
 
  <body>
    <div class="content">
      #@main()
    </div>
   
    <script type="text/javascript" src="/assets/js/jfinal.js"></script>
    #@js?()
  </body>
 </html>
#end

    以上代码示例定义了一个web应用的layout模板,注意看其中的两处:#@css?() 与 #@js?() 就是模板函数安全调用。

    上述模板中引入的 jfinal.css 与 jfinal.js 是两个必须的资源文件,对大部分模块已经满足需要,但对于有些模块,除了需要这两个必须的资源文件以外,还需要额外的资源文件,那么就可以通过#define css() 与 #define js() 来提供,如下是代码示例:

#@layout()   ### 调用 layout.html 中定义的模板函数 layout() 

#define main()
   这里是 body 中的内容块
#end

#define css()
   这里可以引入额外的 css 内容
#end

#define js()
   这里可以引入额外的 js 内容
#end

    以上代码中先是通过#@layout()调用了前面定义过的layout()这个模板函数,而这个模板函数中又分别调用了#@main()、#@css?()、#@js?()这三个模板函数,其中后两个是安全调用,所以对于不需要额外的css、js文件的模板,则不需要定义这两个方法,安全调用在调用不存在的模板函数时会直接跳过。

9、#date 指令

    date指令用于格式化输出日期型数据,包括Date、Timestamp等一切继承自Date类的对象的输出,使用方式极其简单:

#date(account.createAt)
#date(account.createAt, "yyyy-MM-dd HH:mm:ss")

    上面的第一行代码只有一个参数,那么会按照默认日期格式进行输出,默认日期格式为:“yyyy-MM-dd HH:mm”。上面第二行代码则会按第二个参数指定的格式进行输出。

    如果希望改变默认输出格式,只需要通过engine.setDatePattern()进行配置即可。

10、#number 指令

    number指令用于格式化输出数字型数据,包括Double、Float、Integer、Long、BigDecimal等一切继承自Number类的对象的输出,使用方式依然极其简单:

#number(3.1415926, "#.##")
#number(0.9518, "#.##%")
#number(300000, "光速为每秒,### 公里。")

    上面的 #number指令第一个参数为数字类型,第二个参数为String类型的pattern。Pattern参数的用法与JDK中DecimalFormat中pattern的用法完全一样。当不知道如何使用pattern时可以在搜索引擎中搜索关键字DecimalFormat,可以找到非常多的资料。

    #number指令的两个参数可以是变量或者复杂表达式,上例参数中使用常量仅为了方便演示。

11、#escape 指令

    escape 指令用于 html 安全转义输出,可以消除 XSS 攻击。escape 将类似于 html 形式的数据中的大于号、小于号这样的字符进行转义,例如将小于号转义成:&lt;  将空格转义成 &nbsp;

    使用方式与输出指令类似:

#escape(blog.content)

12、指令扩展

    由于采用独创的DKFF和DLRD算法,JFinal Template Engine可以极其便利地在语言层面对指令进行扩展,而代码量少到不可想象的地步,学习成本无限逼近于0。以下是一个代码示例:

public class NowDirective extends Directive {
  public void exec(Env env, Scope scope, Writer writer) {
    write(writer, new Date().toString());
  }
}

    以上代码中,通过继承Directive并实现exec方法,三行代码即实现一个#now指令,可以向模板中输出当前日期,在使用前只需通过me.addDirective(“now”, NowDirective.class) 添加到模板引擎中即可。以下是在模板中使用该指令的例子:

今天的日期是: #now()

    除了支持上述无#end块,也即无指令body的指令外,JFinal Template Engine还直接支持包含#end与body的指令,以下是示例:

public class Demo extends Directive {

  // ExprList 代表指令参数表达式列表
  public void setExprList(ExprList exprList) {
    // 在这里可以对 exprList 进行个性化控制
    super.setExprList(exprList);
  }
  
  public void exec(Env env, Scope scope, Writer writer) {
    wirte(writer, "body 执行前");
    stat.exec(env, scope, writer);  // 执行 body
    wirte(writer, "body 执行后");
  }
  
  public boolean hasEnd() {
    return true;  // 返回 true 则该指令拥有 #end 结束标记
  }
}

    如上所示,Demo继承Directive覆盖掉父类中的hasEnd方法,并返回true,表示该扩展指令具有#end结尾符。上例中public void exec 方法中的三行代码,其中stat.exec(…)表示执行指令body中的代码,而该方法前后的write(…)方法分别输出一个字符串,最终的输出结果详见后面的使用示例。此外通过覆盖父类的setExprList(…)方法可以对指令的参数进行控制,该方法并不是必须的。

    通过me.addDirective(“demo”, Demo.class)添加到引擎以后,就可以像如下代码示例中使用:

#demo()
 这里是 demo body 的内容
#end

    最后的输出结果如下:

body 执行前
 这里是 demo body 的内容
body 执行后

   上例中的#demo指令body中包含一串字符,将被Demo.exec(…)方法中的stat.exec(…)所执行,而stat.exec(…)前后的write(…)两个方法调用产生的结果与body产生的结果生成了最终的结果。


13、常见错误

    enjoy 模板引擎的使用过程中最常见的错误就是分不清 “表达式” 与 “非表达式”,所谓表达式是指模板函数调用、指令调用时小括号里面的所有东西,例如:

#directiveName(这里所有东西是表达式)

#@functionName(这里所有东西是表达式)

     上例中的两行代码分别是调用指令与调用模板函数,小括号内的东西是表达式,而表达式的用法与 Java 几乎一样,该这么来用:

#directiveName( user.name )

    最常见错误的用法如下:

#directiveName ( #(user.name) )

    简单来说这种错误就是在该使用表达式的地方使用指令,在表达式中永远不要出现字符 '#',而是直接使用 java 表达式