6.4 指令

优质
小牛编辑
130浏览
2023-12-01

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

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

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

1、输出指令#( )

与几乎所有 java 模板引擎不同,Enjoy 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语法完全一样,在此不在赘述。(注意:jfinal 3.3 之前的版本 #else if 之间不能有空格字符需要写成:#elseif,否则会报异常:Can not match the #end of directive #if )

3、#for 指令

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

// 对 List、数组、Set 这类结构进行迭代
#for(x : list)
  #(x.field)
#end

// 对 Map 进行迭代
#for(x : map)
  #(x.key)
  #(x.value)
#end

上例代码中的第一个 for 指令是对 list 进行迭代,用法与 java 语法完全一样。

第二个 for 指令是对 map 进行迭代,取值方式为 item.key 与 item.value。该取值方式是 enjoy 对 map 迭代的增强功能,可以节省代码量。仍然也可以使用传统的 java map 迭代方式:#for( x : map.entrySet() ) #(x.key) #(x.value) #end

注意当被迭代的目标为 null 时,不需要做 null 值判断,for 指令会自动跳过,不进行迭代。从而可以避免 if 判断,节省代码提高效率。

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语法基本一样,唯一的不同是变量声明不需要类型,直接用赋值语句即可,Enjoy 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、#switch 指令(3.6 版本新增指令)

#switch 指令对标 java 语言的 switch 语句。基本用法一致,但做了少许提升用户体验的改进,用法如下:

#switch (month)
  #case (1, 3, 5, 7, 8, 10, 12)
    #(month) 月有 31 天
  #case (2)
    #(month) 月平年有28天,闰年有29天
  #default
    月份错误: #(month ?? "null")
#end

如上代码所示,#case 分支指令支持以逗号分隔的多个参数,这个功能就消解掉了 #break 指令的必要性,所以 enjoy 模板引擎是不需要 #break 指令的。

#case 指令参数还可以是任意表达式,例如:

#case (a, b, x + y, "abc", "123")

上述代码中用逗号分隔的表达式先会被求值,然后再逐一与 #switch(value) 指令中的 value 进行比较,只要有一个值与其相等则该 case 分支会被执行。

#case 支持逗号分隔的多参数,从而无需引入 #break 指令,不仅减少了代码量,而且避免了忘写 #break 指令时带来的错误隐患。还有一个与 java 语法有区别的地方是 #case、#default 指令都未使用冒号字符。

5、#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指令之内,用于实现模块化,从而希望其中的变量名不会与上层作用域发生命名上的冲突。

重要:由于赋值表达式本质也是表达式,而其它指令本质上支持任意表达式,所以 #set 指令对于赋值来说并不是必须的,例如可以在 #() 输出指令中使用赋值表达式:

#(x = 123, y = "abc", array = [1, "a", true], map = {k1:v1}, null)

以上代码在输出指令中使用了多个赋值表达式,可以实现 #set 的功能,在最后通过一个 null 值来避免输出表达式输出任何东西。类似的,别的指令内部也可以这么来使用赋值表达式。

6、#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”的模块化重用。

7、#render 指令

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

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

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

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

8、#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(…),通过此方法可以将所有常用的模板函数全部定义成类似于共享库这样的集合,极大提高重用度、减少代码量、提升开发效率。

Enjoy 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 指令,可以在模板函数中返回,但不支持返回值。

9、模板函数调用与 #call 指令

调用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文件的模板,则不需要定义这两个方法,安全调用在调用不存在的模板函数时会直接跳过。

#call 指令是 jfinal 3.6 版本新增指令,使用 #call 指令,模板函数的名称与参数都可以动态指定,提升模板函数调用的灵活性,用法如下:

#call(funcName, p1, p2, ..., pn)

上述代码中的 funcName 为函数名,p1、p2、pn 为被调用函数所使用的参数。如果希望模板函数不存在时忽略其调用,添加常量值 true 在第一个参数位置即可:

#call(true, funcName, p1, p2, ..., pn)

10、#date 指令

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

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

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

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

keepPara 问题:如果日期型表单域提交到后端,而后端调用了 Controller 的 keepPara() 方法,会将这个日期型数据转成 String 类型,那么 #date(...) 指令在输出这个 keepPara 过来的 String 时就会抛出异常,对于这种情况可以指令 keep 住其类型:

// keepPara() 用来 keep 住所有表单提交数据,全部转换成 String 类型
keepPara();

// 再用一次带参的 keepPara,指定 createAt 域 keep 成 Date 类型
keepPara(Date.class, "createAt");

如上所示,第二行代码用 Date.class 参数额外指定了 createAt 域 keep 成 Date 类型,那么在页面 #date(createAt) 指令就不会抛出异常了。keepModel(...)、keepBean(...) 会保持原有类型,无需做上述处理。

11、#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指令的两个参数可以是变量或者复杂表达式,上例参数中使用常量仅为了方便演示。

12、#escape 指令

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

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

#escape(blog.content)

13、指令扩展

由于采用独创的 DKFF 和 DLRD 算法,Enjoy 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的指令外,Enjoy 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) {
    write(writer, "body 执行前");
    stat.exec(env, scope, writer);  // 执行 body
    write(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产生的结果生成了最终的结果。

重要:指令中声明的属性是全局共享的,所以要保障指令中的属性是线程安全的。如下代码以 com.jfinal.template.ext.directive.DateDirective 的代码片段为例:

public class DateDirective extends Directive {
	
   private Expr valueExpr;
   private Expr datePatternExpr;
   private int paraNum;

   ...
}

以上代码中有三个属性,类型是 Expr、Expr、int,其中 Expr 是线程安全的,而 int paraNum 虽然表面上看不是线程安全的,但在整个 DateDirective 类中只有构造方法对该值初始化的时候有写入操作,其它所有地方都是读操作,所以该 int 属性在这里是线程安全的。

14、常见错误

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

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

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

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

#directiveName( user.name )

最常见错误的用法如下:

#directiveName ( #(user.name) )

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