6 模板引擎——Template
6.1 表达式
表达式与Java是直接打通!你也可以先无视这些用法,而是直接当成是 java 表达式去使用,则可以免除掉上面的学习成本。
6.1.1 与java规则基本相同的表达式
算术运算: + - * / % ++ --
比较运算: > >= < <= == != (基本用法相同,后面会介绍增强部分)
逻辑运算: ! && ||
三元表达式: ? :
Null 值常量: null
字符串常量: "jfinal club"
布尔常量:true false
数字常量: 123 456F 789L 0.1D 0.2E10
数组存取:array[i](Map被增强为额外支持 map[key]的方式取值)
属性取值:object.field(Map被增强为额外支持map.key 的方式取值)
方法调用:object.method(p1, p2…, pn) (支持可变参数)
逗号表达式:123, 1>2, null, "abc", 3+6 (逗号表达式的值为最后一个表达式的值)
小技巧:如果从java端往map中传入一个key为中文的值,可以通过map["中文"] 的方式去访问到,而不能用 "map.中文" 访问。因为引擎会将之优先当成是object.field的访问形式,而目前引擎暂时还不支持中文作为变量名标识符。
6.1.2 属性访问
由于模板引擎的属性取值表达式极为常用,所以对其在用户体验上进行了符合直觉的扩展,field 表达式取值优先次序,以 user.name 为例:
如果 user.getName() 存在,则优先调用
如果 user 具有 public 修饰过的name 属性,则取 user.name 属性值(注意:jfinal 4.0 之前这条规则的优先级最低)
如果 user 为 Model 子类,则调用 user.get("name")
如果 user 为 Record,则调用 user.get("name")
如果 user 为 Map,则调用 user.get("name")
此外,还支持数组的length长度访问:array.length,与java语言一样
最后,属性访问表达式还可以通过 FieldGetter 抽象类扩展,具体方法参考 com.jfinal.template.expr.ast.FieldGetters,这个类中已经给出了多个默认实现类;
6.1.3 方法调用
模板引擎被设计成与 java 直接打通,可以在模板中直接调用对象上的任何public方法,使用规则与java中调用方式保持一致,以下代码示例:
#("ABCDE".substring(0, 3))
#(girl.getAge())
#(list.size())
#(map.get(key))
6.1.4 静态属性访问
示例
#if(x.status == com.demo.common.model.Account::STATUS_LOCK_ID)
<span>(账号已锁定)</span>
#end
通过类名加双冒号再加静态属性名即为静态属性访问表达式,上例中静态属性在java代码中是一个int数值,通过这种方式可以避免在模板中使用具体的常量值,从而有利于代码重构。
如果某个静态属性要被经常使用,建议通过 addSharedObject(...) 将其配置成共享对象,然后通过 field 表达式来引用,从而节省代码,例如先配置 shared object:
public void configEngine(Engine me) {
me.addSharedObejct("Account", new Account());
}
然后在模板中就可以使用 field 表达式来代替原有的静态属性访问表达式了:
#if(x.status == Account.STATUS_LOCK_ID)
<span>(账号已锁定)</span>
#end
6.1.5 静态方法调用
示例
#if(com.jfinal.kit.StrKit::isBlank(title))
....
#end
使用方式与前面的静态属性访问保持一致,仅仅是将静态属性名换成静态方法名,并且后面多一对小括号与参数:类名 + :: + 方法名(参数)。静态方法调用支持可变参数。与静态属性相同,被调用的方法需要使用public static 修饰才可访问。
如果觉得类名前方的包名书写很麻烦,可以使用后续即将介绍的me.addSharedMethod(…)方法将类中的方法添加为共享方法,调用的时候直接使用方法名即可,连类名都不再需要。
此外,还可以调用静态属性上的方法,以下是代码示例:
(com.jfinal.MyKit::me).method(paras)
上面代码中需要先用一对小扩号将静态属性取值表达式扩起来,然后再去调用它的方法,小括号在此仅是为了改变表达式的优先级。
6.1.6 空合并安全取值调用操作符
JFinal Template Engine 引入了swift与C#语言中的空合操作符,并在其基础之上进行了极为自然的扩展,该表达式符号为两个紧靠的问号:??。代码示例:
seoTitle ?? "JFinal 社区" // 空合并,seoTitle为空则取"JFinal 社区"
object.field ?? // 安全取值,object为null时候不要报异常,返回null;
object.method() ?? // 安全取值,跟上面一样;
object.field ?? "默认值" // 空合并+安全取值
特别注意:?? 操作符的优先级高于数学计算运算符:+、-、*、/、%,低于单目运算符:!、++、--。强制改变优先级使用小括号即可。
6.1.7 单引号字符串支持
针对Template Engine 经常用于html的应用场景,添加了单引号字符串支持;
6.1.8 字符串比较
相等不等表达式 == 与 != 会对左右表达式进行left.equals(right)比较操作,所以可以对字符串进行直接比较;
6.1.9 尔表达式增强
布尔表达式在原有java基础之下进行了增强,可以减少代码输入量,具体规则自上而下优先应用如下列表:
null 返回 false
boolean 类型,原值返回
String、StringBuilder等一切继承自 CharSequence 类的对象,返回 length > 0
其它返回 true
以上规则可以减少模板中的代码量,以下是示例:
#if(user && user.id == x.userId)
...
#end
以上代码中的 user 表达式实质上代替了java表达式的 user != null 这种写法,减少了代码量。当然,上述表达式如果使用 ?? 运算符,还可以更加简单顺滑:if (user.id ?? == x.userId);
6.1.10 Map 定义表达式
示例
#set(map = {k1:123, "k2":"abc", "k3":object})
#(map.k1)
#(map.k2)
#(map["k1"])
#(map["k2"])
#(map.get("k1"))
map的定义使用一对大括号,每个元素以key : value的形式定义,多个元素之间用逗号分隔。
key 只允许是合法的 java 变量名标识符或者 String 常量值(jfinal 3.4 起将支持 int、long、float、double、boolean、null 等等常量值),注意:上例中使用了标识符 k1 而非 String 常量值 "k1" 只是为了书写时的便利,与字符串是等价的,并不会对标识符 k1 进行表达式求值。
上例中通过#set指令将定义的变量赋值给了map变量,第二与第三行中以object.field的方式进行取值,第四第五行以 map[key] 的方式进行取值,第六行则是与 java 表达式打通式的用法。
特别注意:上例代码如果使用 map[k1] 来取值,则会对 k1 标识符先求值,得到的是 null,也即map[k1] 相当于 map[null],因此上述代码中使用了 map["k1"] 这样的形式来取值。
此外,map 取值还支持在定义的同时来取值,如下所示:
#({1:'自买', 2:'跟买'}.get(1))
#({1:'自买', 2:'跟买'}[2])
### 与双问号符联合使用支持默认值
#({1:'自买', 2:'跟买'}.get(999) ?? '其它')
6.1.11 数组定义表达式
示例
// 定义数组 array,并为元素赋默认值
#set(array = [123, "abc", true])
// 获取下标为 1 的值,输出为: "abc"
#(array[1])
// 将下标为 1 的元素赋值为 false,并输出
#(array[1] = false, array[1])
数组定义表达式的初始化元素除了可以使用常量值以外,还可以使用任意的表达式,包括变量、方法调用返回值等等:
#set(array = [ 123, "abc", true, a && b || c, 1 + 2, obj.doIt(x) ])
范围数组定义示例
#for(x : [1..10])
#(x)
#end
6.1.12 逗号表达式
将多个表达式使用逗号分隔开来组合而成的表达式称为逗号表达式,逗号表达式整体求值的结果为最后一个表达式的值。例如:1+2, 3*4 这个逗号表达式的值为12。
6.1.13 从java中去除的运算符
针对模板引擎的应用场景,去除了位运算符,避免开发者在模板引擎中表述过于复杂,保持模板引擎的应用初衷,同时也可以提升性能。
6.2 指令
指令有 #if、#for、#switch、#set、#include、#define、#(…) 这七个指令,便实现了传统模板引擎几乎所有的功能,用户如果有任意一门程序语言基础,学习成本几乎为零。
自定义指令可以继承Directive,可以在com.jfinal.template.ext.directive 包下面就有五个扩展指令,Active Record 的 sql 模块也针对sql管理功能扩展了三个指令,参考这些扩展指令的代码;
6.2.1 输出指令#( )
示例
#(value)
#(object.field)
#(object.field ??)
#(a > b ? x : y)
#(seoTitle ?? "JFinal 俱乐部")
#(object.method(), null)
6.2.2 #if 指令
示例
#if(c1)
...
#else if(c2)
...
#else if (c3)
...
#else
...
#end
6.2.3 #for 指令
for指令的设计非常到位,强悍!
示例
// 对 List、数组、Set 这类结构进行迭代
#for(x : list)
#(x.field)
#end
// 对 Map 进行迭代
#for(x : map)
#(x.key)
#(x.value)
#end
注意:当被迭代的目标为 null 时,不需要做 null 值判断,for 指令会自动跳过,不进行迭代。从而可以避免 if 判断,节省代码提高效率。
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
注意:嵌套的for循环,内层for循环获取外层for循环的状态,可以使用#(for.outer.index)的方式;
除了 Map、List 以外,for指令还支持 Collection、Iterator、array 普通数组、Iterable、Enumeration、null 值的迭代,用法在形式上与前面的List迭代完全相同,都是 #for(id : target) 的形式,对于 null 值,for指令会直接跳过不迭代。
此外,for指令还支持对任意类型进行迭代,此时仅仅是对该对象进行一次性迭代,如下所示:
#for(x : article) ### article是Java对象
#(x.title)
#end
for 指令还支持 #else 分支语句,在for指令迭代次数为0时,将执行 #else 分支内部的语句,如下是示例:
#for(blog : blogList)
#(blog.title)
#else
您还没有写过博客,点击此处开博。
#end
最后,除了上面介绍的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完全一致;
6.2.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")
6.2.5 #set 指令
set指令只接受赋值表达式,以及用逗号分隔的赋值表达式列表,如下是代码示例:
#set(x = 123)
#set(a = 1, b = 2, c = a + b)
#set(array[0] = 123)
#set(map["key"] = 456)
重要:由于赋值表达式本质也是表达式,而其它指令本质上支持任意表达式,所以 #set 指令对于赋值来说并不是必须的,例如可以在 #() 输出指令中使用赋值表达式:
#(x = 123, y = "abc", array = [1, "a", true], map = {k1:v1}, null)
以上代码在输出指令中使用了多个赋值表达式,可以实现 #set 的功能,在最后通过一个 null 值来避免输出表达式输出任何东西。类似的,别的指令内部也可以这么来使用赋值表达式。
6.2.6 #include 指令
include指令用于将外部模板内容包含进来,被包含的内容会被解析成为当前模板中的一部分进行使用,如下是代码示例:
#include("sidebar.html")
#include还支持带参数的方式
#include("_hot_list.html", title="热门项目", list=projectList, url="/project")
6.2.7 #render 指令
render指令在使用上与include指令几乎一样,同样也支持无限量传入赋值表达式参数,主要有两点不同:
render指令支持动态化模板参数,例如:#render(temp),这里的temp可以是任意表达式,而#include指令只能使用字符串常量:#include(“abc.html”)
render指令中#define定义的模板函数只在其子模板中有效,在父模板中无效,这样设计非常有利于模块化;
引入 #render 指令的核心目的在于支持动态模板参数。
#render("_hot_list.html", title="热门项目", list=projectList, url="/project")
6.2.8 #define 指令
#define指令是模板引擎主要的扩展方式之一,define指令可以定义模板函数(Template Function)。通过define指令,可以将需要被重用的模板片段定义成一个一个的 template function,在调用的时候可以通过传入参数实现千变万化的功能。
6.2.9 #call 指令
#call 指令是 jfinal 3.6 版本新增指令,使用 #call 指令,模板函数的名称与参数都可以动态指定,提升模板函数调用的灵活性,用法如下:
#call(funcName, p1, p2, ..., pn)
上述代码中的 funcName 为函数名,p1、p2、pn 为被调用函数所使用的参数。如果希望模板函数不存在时忽略其调用,添加常量值 true 在第一个参数位置即可:
#call(true, funcName, p1, p2, ..., pn)
等价于
#@funcName?(p1, p2, ..., pn)
6.2.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");
6.2.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,可以找到非常多的资料。
6.2.12 #escape 指令
escape 指令用于 html 安全转义输出,可以消除 XSS 攻击。escape 将类似于 html 形式的数据中的大于号、小于号这样的字符进行转义,例如将小于号转义成:< 将空格转义成
使用方式与输出指令类似:
#escape(blog.content)
6.3 注释
### 这里是单行注释
#--
这里是多行注释的第一行
这里是多行注释的第二行
--#
6.4 原样输出
#[[
#(value)
#for(x : list)
#(x.name)
#end
]]#
6.5 共享模板函数
可以在configEngine(Engine me)方法中,通过me.addSharedFunction("layout.html")方法,将该模板中定义的所有模板函数设置为共享的,那么就可以省掉#include(…),通过此方法可以将所有常用的模板函数全部定义成类似于共享库这样的集合,极大提高重用度、减少代码量、提升开发效率。
6.6 共享方法
示例
public void configEngine(Engine me) {
me.addSharedMethod(new com.jfinal.kit.StrKit());
}
#if(isBlank(nickName))
...
#end
#if(notBlank(title))
...
#end
默认 Shared Method 配置扩展
Enjoy 模板引擎默认配置添加了 com.jfinal.template.ext.sharedmethod.SharedMethodLib 为 Shared Method,所以其中的方法可以直接使用不需要配置。里头有 isEmpty(...) 与 notEmpty(...) 两个方法可以使用。
isEmpty(...) 用来判断 Collection、Map、数组、Iterator、Iterable 类型对象中的元素个数是否为 0,其规如下:
null 返回 true
List、Set 等一切继承自 Collection 的,返回 isEmpty()
Map 返回 isEmpty()
数组返回 length == 0
Iterator 返回 ! hasNext()
Iterable 返回 ! iterator().hasNext()
6.7 共享对象
示例
public void configEngine(Engine me) {
me.addSharedObject("RESOURCE_HOST", "http://res.jfinal.com");
me.addSharedObject("sk", new com.jfinal.kit.StrKit());
}
<img src="#(RESOURCE_HOST)/img/girl.jpg" />
#if(sk.isBlank(title))
...
#end
注意:由于对象被全局共享,所以需要注意线程安全问题,尽量只共享常量以及无状态对象。
6.8 扩展方法
Extension Method 用于对已存在的类在其外部添加扩展方法,该功能类似于ruby语言中的mixin特性。
JFinal Template Engine 默认已经为String、Integer、Long、Float、Double、Short、Byte 这七个基本的 java 类型,添加了toInt()、toLong()、toFloat()、toDouble()、toBoolean()、toShort()、toByte() 七个extension method。
一般用不到,不详细介绍;
6.9 任意环境下使用Engine
Enjoy Template Engine 的使用不限于 web,可以使用在任何 java 开发环境中。Enjoy 常被用于代码生成、email 生成、模板消息生成等具有模板特征数据的应用场景,使用方式极为简单。
Engine 是使用 Enjoy 的配置入口和使用入口,主要功能之一是配置 Enjoy 各种参数,其二是通过 getTemplate、getTemplateByString 方法获取到 Template 对象;
Template 代表对模板的抽象,可以调用其 render 系方法对模板进行渲染,支持字节流、字符流;
Engine对象管理
Engine对象的创建方式有两种,一种是通过 Engine.create(name) 方法,另一种是直接使用 new Engine() 语句,前者创建的对象是在 Engine 模块管辖之内,可以通过 Engine.use(name) 获取到,而后者创建的对象脱离了 Engine 模块管辖,无法通过 Engine.use(name) 获取到,开发者需要自行管理。
JFinal 的 render 模块以及 activerecord 模块使用 new Engine() 创建实例,无法通过 Engine.use(name) 获取到,前者可以通过RenderManager.me().getEngine() 获取到,后者可以通过 activeRecordPlugin.getEngine() 获取到。
Engine对象管理的设计,允许在同一个应用程序中使用多个 Engine 实例用于不同的用途,JFinal 自身的 render、activerecord 模块对 Engine 的使用就是典型的例子。