FreeMarker 模板开发
封面来源:由博主个人绘制,如需使用请联系博主。
本文摘录自:FreeMarker 中文官方参考手册
0. 前言
公司的项目需要用到模板引擎 FreeMarker,因此,不会的我也就只有学习。
曾经在学习 SpringBoot 的时候,学习过另外一种模板引擎 Thymeleaf,这俩也有相似之处,但是不同点也挺多。
本文只作为一个备忘录,或者说知识梳理,不作为入门级教程。
入门级教程建议查看:FreeMarker 中文官方参考手册,本文大部分内容就是摘录此手册。
1. FreeMarker 入门
1.1 模板 + 数据模型 = 输出
模板引擎,模板引擎,那肯定是需要编写模板的。但是也不能只有一个光模板,还需要数据来填充。
为模板准备的数据整体被称作为 数据模型。 模板作者要关心的是,数据模型是树形结构(就像硬盘上的文件夹和文件),在视觉效果上, 数据模型就像是:
1 | (root) |
总的来说,模板和数据模型是 FreeMarker 来生成输出所必须的,因此就有:模板 + 数据模型 = 输出
1.2 数据模型
哈希
数据模型的基本结构是树状的。 这棵树可以很复杂,并且可以有很大的深度,比如:
1 | (root) |
上图中的变量扮演目录的角色(比如 root、animals
、mouse
、 elephant
、 python
、 misc
)被称为 hashes。
存储单值的变量(size
, price
, message
和 foo
)称为 scalars(标量)。
序列
还有一种很重要的变量是 sequences。 它们像哈希表那样存储子变量,但是子变量没有名字,它们只是列表中的项。 比如,在下面这个数据模型中, animals
和 misc.fruits
就是序列:
1 | (root) |
要访问序列的子变量,可以使用方括号形式的数字索引下标。 索引下标从 0 开始(懂的都懂 😂)。
比如,要得到第一个动物的名称的话,可以这么来写代码 animals[0].name
。要得到 misc.fruits
中的第二项(字符串"banana"
)可以这么来写 misc.fruits[1]
。
标量类型 可以分为如下的类别:
- 字符串
- 数字,需要注意 212 和 “212” 的区别
- 日期 / 时间:可以是日期 - 时间格式(存储某一天的日期和时间), 或者是日期(只有日期,没有时间),或者是时间(只有时间,没有日期)。
- 布尔值
还有一些其它更为高级的类型,比如方法和指令。
1.3 指令
能被 FreeMarker 所解析的特殊代码片段有:
1、${...}
:这样的表达式被称为 interpolation(插值)。
2、FTL 标签:这些标签的名字以 #
开头,用户自定义的 FTL 标签则需要使用 @
来代替 #
,FTL 标签也被成为 指令。
3、注释:使用 <#--
and -->
来标识。 与 HTML 注释不同,FTL 注释不出现在访问者的页面中,因为 FreeMarker 会跳过它们。
if 指令的基本使用
1、<#if condition>
condition为 true 执行内容,为 false就略过 </#if>
2、<#if>
标签体内可以存在标签 <# else>
3、<#if>
标签体内还可以存在标签 <# elseif>
list 指令的基本使用
<#list>
标签可以用来显示列表内容,基本语法:
1 | <#list sequence as loopVariable> repeatThis </#list> |
就我个人的理解来说,<#list>
标签就和循环一样。
<#list>
标签内还可以有 <#items>
标签、 <#sep>
标签、<# else>
标签。
1 | <#list misc.fruits> |
如果 <#list>
标签内有 <#items>
标签,那么list
指令将列表视为一个整体, 在 items
指令中的部分才会为每个水果重复。 如果我们有 0 个水果,那么在 list
中的所有东西都被略过了, 因此就不会有 <ul>
标签了。
还有一种场景,使用分隔符来列出一些水果,比如用逗号作为分隔符:
1 | <p>Fruits: <#list misc.fruits as fruit>${fruit}<#sep>, </#list> |
然后就会输出:
1 | <p>Fruits: orange, banana |
被 sep
覆盖的部分只有当还有下一项时才会被执行,因此最后一个水果后面不会有逗号。
list
指令也像 if
指令那样,可以有 else
部分,如果列表中有 0 个元素时就会被执行:
1 | <p>Fruits: <#list misc.fruits as fruit>${fruit}<#sep>, <#else>None</#list> |
当然,指令 list
、items
、sep
和 else
可以联合起来使用。
include 指令的基本使用
使用 include
指令, 就可以在模板中插入其他文件的内容。
比方说,我们可以将版权信息单独存放在页面文件 copyright_footer.html
中:
1 | <hr> |
当需要用到这个文件时,就可以使用 include
指令来插入:
1 | <html> |
当修改了 copyright_footer.html
文件, 那么访问者在所有页面都会看到版权声明的新内容。
补充: 重用代码片段的一个更有力的方式是使用宏。
在页面上也可以多次使用指令,而且指令间也可以很容易地相互嵌套。比如,在 list
指令中嵌套 if
指令:
1 | <#list animals as animal> |
Freemarker 不会解析 FTL 标签以外的文本,甚至可在 HTML 标签内写 FTL 标签,比如:
1 | <div <#if><#/if>> 内容 </div> |
注意: FTL 标签区分带小写。插值只能在文本中使用,FTL 标签不可以在其他 FTL 标签和插值中使用,注释可以放在 FTL 标签和插值中。
FreeMarker 参考指令:指令参考
1.4 内建函数
内建函数很像子变量(或者说 Java 中的方法),它们并不是数据模型中的东西,是 FreeMarker 在数值上添加的。 为了清晰子变量是哪部分,使用 ?
(问号)代替 .
(点)来访问它们。常用内建函数的示例:
user?html
给出user
的 HTML 转义版本user?upper_case
给出user
值的大写版本animal.name?cap_first
给出animal.name
的首字母大写版本user?length
给出user
值中字符的数量animals?size
给出animals
序列中项目的个数- 如果在
<#list animals as animal>
和对应的</#list>
标签中:animal?index
给出了在animals
中基于 0 开始的animal
的索引值animal?counter
也像index
, 但是给出的是基于 1 的索引值animal?item_parity
基于当前计数的奇偶性,给出字符串 “odd” 或 “even”。在给不同行着色时非常有用,比如在<td class="${animal?item_parity}Row">
中。
一些内建函数需要参数来指定行为,比如:
animal.protected?string("Y", "N")
基于animal.protected
的布尔值来返回字符串 “Y” 或 “N”。animal?item_cycle('lightRow','darkRow')
是之前介绍的item_parity
更为常用的变体形式。fruits?join(", ")
通过连接所有项,将列表转换为字符串, 在每个项之间插入参数分隔符user?starts_with("J")
根据user
的首字母是否是 “J” 返回布尔值 true 或 false。
一些内建函数可以对字符串进行转义:
- Java 语言规则的字符串转义:
content?j_string
- JavaScript 语言规则的字符串转义:
content?js_string
- JSON 规则的字符串转义:
content?json_string
内建函数应用可以链式操作,比如user?upper_case?html
会先转换用户名到大写形式,之后再进行 HTML 转义。
更多内建函数参考:内建函数参考 - FreeMarker 中文官方参考手册
哈希表内建函数
keys
获取一个包含哈希表中查找到的键的序列。 请注意,并不是所有哈希表都支持该内建函数。
1 | <#assign h = {"name":"mouse", "price":50}> |
输出:
name = mouse; price = 50;
哈希表的键通常是无序的,因此 keys
获取到的序列通常也是无序的。对于某些维持了键的顺序的哈希表,keys
获取到的序列中的元素也将按照那个维持的顺序排列。
values
获取一个包含哈希表中所有键的序列。请注意,并不是所有哈希表都支持该内建函数。values
获取到的序列的元素顺序与keys
的规则一致。
字符串内建函数中的大坑
cap_first
:字符串中的 首单词 的首字母大写。
capitalize
:字符串中 所有单词 的首字母大写。
为什么说这是一个大坑?
Apache 为 Java 提供了许多常用的工具包,其中有一个工具包叫做 common-lang3,在这个工具包中有一个名为 StringUtils
的工具类,可以使用这个工具类对 Java 中的字符串进行操作。
在 StringUtils
中有一个名为 capitalize()
的方法,此方法可以将传入字符串的首字母大写并返回, 不会改变其他字符串。
针对字符串 GreEN mouse
来说:
StringUtils.capitalize("GreEN mouse")
将返回GreEN mouse
;- 而在 FreeMarker 中,
${"GreEN mouse"?capitalize}
会返回Green Mouse
,原本的mouse
居然变成了Mouse
?!
这是因为 FreeMarker 中字符串内建函数 capitalize
会将字符串中 所有单词 的首字母大写。
这里的单词指的是不间断的字符序列,包含了任意字符,但是没有空白。FreeMarker 中的空白是指完全透明的字符,但是对文本的视觉呈现会有作用。 比如空格,制表符(水平和垂直),换行符(CR 和 LF),换页。
因此,在 FreeMarker 中,想让字符串的首字母大写,应该使用 cap_first
内建函数。
循环变量内建函数
上文中介绍了循环变量的 index
、counter
和 item_parity
内建函数,再补充一些:
has_next
:判断当前循环项后是否还有可用的循环项;is_even_item
:判断循环项是否是当前迭代间隔 1 的奇数项,比如:
1 | <#list ['a', 'b', 'c', 'd'] as i>${i?is_even_item?c} </#list> |
1 | false true false true |
is_first
:判断循环项是否是当前迭代的第一项;is_last
:判断循环项是否是当前迭代的最后一项;is_odd_item
:判断循环项是否是当前迭代间隔 1 的偶数项,比如:
1 | <#list ['a', 'b', 'c', 'd'] as i>${i?is_odd_item?c} </#list> |
1 | true false true false |
item_cycle
:与item_parity
类似,它更加通用,直接上例子:
1 | <#list ['a', 'b', 'c', 'd', 'e', 'f', 'g'] as i> |
1 | <tr class="row1">a</tr> |
内置函数 item_cycle
的使用细节:参数的个数至少是1个,没有上限,参数的类型是任意的,无需只是字符串。
item_parity
:该内置函数只会返回字符串值"odd"
或"even"
,常用于表格中行间的颜色变换:
1 | <#list ['a', 'b', 'c', 'd'] as i> |
1 | <tr class="oddRow">a</tr> |
item_parity_cap
:与item_parity
使用方式相同,只不过返回的是首字母大写的"Odd"
和"Even"
。
1.5 处理不存在的变量
除了一些典型的人为原因导致失误外,FreeMarker 绝不能容忍引用不存在的变量, 除非明确地告诉它当变量不存在时如何处理。这里来介绍两种典型的处理方法。
可以指定一个默认值来避免变量丢失这种情况, 通过在变量名后面跟着一个 !
和默认值。 就像下面的这个例子,当 user
不存在于数据模型时, 模板将会将 user
的值表示为字符串 "visitor"
, 而当 user
存在时, 模板就会表现出 ${user}
的值:
1 | <h1>Welcome ${user!"visitor"}!</h1> |
注意区分:${object!}
表示如果 object 为空则不执行
除此之外,还有 <#if object ??>${object}<#if>
表示如果 object 不为空,则执行里面语句,相当于:
1 | <#if object?if_exits>${object}</#if> |
关于多级访问的变量,animals.python.price!0
当且仅当 animals.python
永远存在, 而仅仅最后一个子变量 price
可能不存在时,这种情况下默认值是 0。 如果 animals
或 python
不存在, 那么模板处理过程将会以“未定义的变量”错误而停止。
为了防止这种情况的发生, 可以如下这样来编写代码 (animals.python.price)!0
来解决。 这种情况就是说当 animals
、python
或 price
任意一个不存在时, 表达式的结果是 0
。对于 ??
也是同样用来的处理这种逻辑的,可以将 animals.python.price??
对比 (animals.python.price)??
来看。
在
<#if>
标签中处理不存在的变量
假设需要判断 parent
中的 child
中的 str
是否等于 mofan
,但 parent
、child
和 str
都可能不存在,因此有这样“恶心”的判断:
1 | <#if parent?? && parent.child?? && parent.child.str?? && parent.child.str == "mofan"> |
或者是套多层 <#if>
标签。那是否有优雅点的写法呢?
1 | <#if (parent.child.str)!"" == "mofan"></#if> |
当 parent
、child
或 str
三者中的任意一个不存在时,返回空字符串,空字符与字符串 mofan
比较结果返回 false
,这与多次非空判断或多层 <#if>
标签嵌套返回的结果是一样的。
但在实际运行过程中,上述代码并不会按照预期执行,反而抛出异常,提示 <#if>
标签中的表达式 不是布尔值, 于是又进行如下尝试:
1 | <#if ((parent.child.str)!"" == "mofan")></#if> |
错误依旧没有得到结果,依旧显示同样的错误。
按照以下方式编写方可正常执行:
1 | <#if ((parent.child.str)!"") == "mofan"></#if> |
2. 数值与类型
2.1 基本内容
数值的概念很好理解,比如:
1 | (root) |
变量 user
的数值就是 “Big Joe”,它的类型是字符串。
lotteryNumbers
的数值是包含20,14的序列。它包含多个值,但是 lotteryNumbers
本身还是单值。它就像一个装有其它很多东西的盒子 (或者说容器),但盒子作为整体还是视作单独的。
对 mouse
来说也是这样,它的数值是一个哈希表(也是类似盒子一样的东西)。
所以说,数值就是存储在变量中的的那个东西。但不需要存储于变量之中的数值也可以称之为数值,比如下面的数字 100:
1 | <#if cargo.weight < 100>Light cargo</#if> |
类型也很好理解,比方说,类型有:字符串、日期、数字、布尔值、序列、哈希表等等。
数值也可以含有多种类型,尽管很少这么使用。比方说上面数据模型中的 mouse
它本身就又是字符串,又是哈希表。
从每个数据模型的例子可以发现,被“(root)”所标识的内容就是哈希表类型的值。
根哈希表包含更多的哈希表或序列。 哈希表包含其他变量,那些变量包含其它值,这些数值可以是字符串,数字等变量, 当然也可以是哈希表或序列变量(相当于可以一直套娃)。
2.2 类型
标量
标量是最基本,最简单的数值类型,它们可以是:
1、字符串:表示简单的文本。将文本内容写在引号内即可,单引号、双引号都可以。
2、数值:整数和非整数不区分
3、布尔值
4、日期:日期变量可以存储和日期 / 时间相关的数据。有三种格式:
- 日期:精确到天的日期,没有时间部分。比如April 4, 2003。
- 时间:精确到毫秒,没有日期部分。比如10:19:18 PM。
- 日期-时间(有时也被称为"时间戳"),比如April 4,2003 10:19:18 PM。 有日期和时间两部分,时间部分的存储精确到毫秒。
不幸的是,受到 Java 平台的限制,FreeMarker 有时是不能决定日期的部哪分被使用(也就是说,是日期-时间格式,日期格式还是时间格式无法确定)。
容器
这些值存在的目的是为了包含其他变量,它们只是容器。 它们包含的变量通常视为 subvariables (子变量)。容器的类型有:
1、哈希表
2、序列
3、集合:从模板设计者角度来看,集合是有限制的序列。不能获取集合的大小, 也不能通过索引取出集合中的子变量,但是它们仍然可以通过 list 指令来遍历。
请注意,一个值也可有多种类型。 对于一个值可以同时存在哈希表和序列这两种类型,这时该变量就支持索引和名称两种访问方式,但我们一般不会两者同时使用。
存储在哈希表,序列(集合)中的变量可以是任意类型的, 这些变量也可以是哈希表、序列(或集合),这样就可以构建任意深度的数据结构。
数据模型本身(最好说成是它的根 root)也是哈希表。
方法和函数
当一个值是方法或函数的时候,那么它就可以计算其他值,结果取决于传递给它的参数。
假设程序员在数据模型中放置了一个方法变量 avg
, 该变量用来计算数字的平均值。如果给定 3 和 5 作为参数,访问 avg
时就能得到结果 4。比如:
1 | The average of 3 and 5 is: ${avg(3, 5)} |
那么会输出:
1 | The average of 3 and 5 is: 4 |
用户自定义指令
这种类型的值可以作为用户自定义指令(用户可以自定义自己的 FreeMarker 的标签),用户自定义指令是一种子程序,一种可以复用的模板代码段。
自定义指令可以使用 <# macro>
指令来定义,使用自定义指令的时候要用 @
而不是 #
。
具体方式参看后文介绍。
函数 / 方法和用户自定义指令的比较
对于函数 / 方法和用户自定义指令的选择,按经验来说,如果能够实现需求, 请先用自定义指令而不要用函数 / 方法。对于自定义指令来说:
- 输出(返回值)的是标记(HTML、XML 等)。 主要原因是函数的返回结果可以自动进行 XML 转义(因为
${...}
的特性), 而用户自定义指令的输出则不是(因为<@...>
的特性所致,它的输出假定是标记,因此已经转义过了); - 没有返回值;
- 会进行流程的控制(就像
list
或if
指令那样),但是不能在函数 / 方法上这么做。
3. 模板总体结构
所谓模板,就是我们编写的 .ftl
文件,模板是由以下部分混合而成的:
-
文本:照着原样输出
-
插值:
${....}
-
FTL 标签
-
注释
FTL 标签是区分大小写的。 list
是指令的名称但 List
就不是,同样 ${name}
和 ${Name}
或 ${NAME}
也是不同的。
请注意非常重要的一点: 插值仅仅可以在文本中使用。
FTL 标签不可以在其他 FTL 标签和插值中使用。比如,下面这样就是错的:
1 | <#if <#include 'foo'>='bar'>...</#if> |
注释可以放在 FTL 标签和插值中。
和 HTML 标签一样,FTL 标签必须正确地嵌套使用,像下面这样就是错的:
1 | <ul> |
4. 表达式
4.1 直接指定值
字符串
确定字符串值的方法就是看双引号或单引号,这两个是等同的。
如果文本自身包含用于字符引用的引号( "
或 '
)或反斜杠时, 应该在它们的前面再加一个反斜杠,这就是转义。 转义允许直接在文本中输入任何字符, 也包括换行。
转义序列 | 含义 |
---|---|
\" |
引号 (u0022) |
\' |
单引号(又称为撇号) (u0027) |
\{ |
起始花括号:{ |
\\ |
反斜杠 (u005C) |
\n |
换行符 (u000A) |
\r |
回车 (u000D) |
\t |
水平制表符(又称为tab) (u0009) |
\b |
退格 (u0008) |
\f |
换页 (u000C) |
\l |
小于号:< |
\g |
大于号:> |
\a |
&符:& |
\xCode |
字符的16进制 Unicode码 (UCS码) |
在最后一样中的 Code 表示 1 - 4 位的 16 进制码。 如果紧跟 16 进制码后一位的字符也能解释成 16 进制码时, 就必须把 4 位补全,否则 FreeMarker 就会误解你的意图。
如果想要打印 ${
或 #{
, 就要使用原生字符串,或者进行转义。
为了表明字符串是原生字符串, 在开始的引号或单引号之前放置字母r
,例如:
1 | ${r"${foo}"} |
将会输出:
1 | ${foo} |
数字
数字的指定很简单,直接写就行了,但是不支持科学计数法也不能省略小数点前的 0。
下面这些写法都是正确的:
1 | 0.08、-5.013、8、008、11、+11 |
序列
指定一个文字的序列,使用逗号来分隔其中的每个子变量,然后把整个列表放到方括号中。例如:
1 | <#list ["foo", "bar", "baz"] as x> |
值域
值域也是序列,但它们由指定包含的数字范围所创建, 而不需指定序列中每一项。
比如: 0..<m
,假设 m
变量的值是5,那么这个序列就包含 [0, 1, 2, 3, 4]
。值域的主要作用有:使用 <#list ...>
来迭代一定范围内的数字、序列切分和字符串切分。
值域表达式的通用形式是( start
和 end
可以是任意的结果为数字表达式):
start..end
:包含结尾的值域。star..<end
或start..!end
:不包含结尾的值域。1..<1
表示[]
start..*length
:限定长度的值域,比如10..*4
就是[10, 11, 12, 13]
,10..*-4
就是[10, 9, 8, 7]
,而10..*0
表示[]
。当这些值域被用来切分时, 如果切分后的序列或者字符串结尾在指定值域长度之前,则切分不会有问题。start..
: 无右边界值域。注意内存溢出。
需要注意的是:
- 无右边界值域的定义大小是 2147483647, 这是由于技术上的限制(32位),但当列表显示它们的时候,实际的长度是无穷大。
- 值域并不存储它们包含的数字,那么对于
0..1
和0..100000000
来说,创建速度都是一样的, 并且占用的内存也是一样的。
哈希表
在模板中指定一个哈希表,就可以遍历用逗号分隔开的"键/值"对, 把列表放到花括号内即可。键和值成对出现并以冒号分隔。比如: { "name": "green mouse", "price": 150 }
。
键和值都是表达式,但是 用来检索的键必须是字符串类型, 而值可以是任意类型。
在遍历哈希表时,可以使用 keys
或 values
内建函数。
4.2 检索变量
检索遍历很简单,使用 ${...}
就可以了。说一下内部表达式需要注意的:
变量名只可以包含字母(也可以是非拉丁文), 数字(也可以是非拉丁数字),下划线 (_
), 美元符号 ($
),at 符号 (@
)。 此外,第一个字符不可以是 ASCII 码数字(0
-9
)。 从 FreeMarker 2.3.22 版本开始,变量名在任何位置也可以包含负号 (-
),点(.
)和冒号(:
), 但这些必须使用前置的反斜杠(\
)来转义, 否则它们将被解释成操作符。
比如,读取名为"data-id"的变量, 表达式为 data\-id
,因为 data-id
将被解释成 “data minus id”。
请注意,这些转义仅在标识符中起作用,而不是字符串中。
4.3 字符串操作
插值(或链接)
所谓插值,就是 ${...}
。但需要注意的是,插值只能在文本区中有效, 比如:
1 | <h1>Hello ${name}!</h1> |
但是,这样就是错误的:
1 | <#if ${big}>...</#if> |
这样写才是对的:<#if big>...</#if>
下面这两种写法是等效的:
1 | <#assign s = "Hello ${user}!"> |
由于地区差异,很多地区会使用千分位分隔符,那么 "someUrl?id=" + id
就可能会是 "someUrl?id=1 234"
(1
与 234
之间存在一个空格)。 要预防这种事情的发生,请使用 ?c
内建函数,那么在 "someUrl?id=" + id?c
或 "someUrl?id=${id?c}"
中, 就会得到如 "someUrl?id=1234"
的输出, 而不管本地化和格式的设置是什么,当然也可以在填充参数时就将数值转换成字符串。
获取字符
要想获取字符串中的某个字符,和获取序列中某个索引位置的值是一样的,比如:
假设 user 是 “mofan”:
1 | ${user[0]} |
将会得到:
1 | m |
字符串切分
字符串切分也很简单,但需要 注意 的是:
- 降序域不允许进行字符串切分。
- 如果变量的值既是字符串又是序列(变量时多类型值),那么切分将会对序列进行,而不是字符串。当处理 XML 时,这样的值就是普通的了。此时,可以使用
someXMLnode?string[range]
。 - 一个遗留的 bug:值域包含结尾时,结尾小于开始索引并且是非负的(就像在
"abc"[1..0]
中), 会返回空字符串而不是错误(因为不符合我们说的第一点)。在实际使用时,不应该这样使用。
字符串切分示例:
1 | <#assign s = "ABCDEF"> |
将会输出:
1 | CD |
4.4 序列操作
连接
序列可以像字符串那样使用 +
进行连接,比如:
1 | <#list ["Joe", "Fred"] + ["Julia", "Kate"] as user> |
序列切分
序列切分和字符串切分类似,使用 seq[range]
进行切分,range 是一个值域,比如:
1 | <#assert seq = ["A", "B", "C", "D", "E"]> |
将会输出:
1 | BCD |
但是序列切分允许使用降值域,比如:seq[3..1]
将会输出 DCB。
值域中的数字必须是序列可使用的合法索引, 否则模板的处理将会终止并报错。不能出现负数的索引,索引不能越界,但是 seq[100..<100]
或 seq[100..*0]
是合法的,因为这些值域是空的,尽管索引越界了。
限制长度的值域(start..*length
)和无右边界值域(start..
)适用于切分后序列的长度。它们会切分可用项中尽可能多的部分。
4.5 哈希表操作
连接
像连接字符串那样,也可以使用 +
号的方式来连接哈希表。如果两个哈希表含有键相同的项,那么在 +
号 右侧 的哈希表中的项优先。
4.6 算数运算
算数运算包含基本的四则运算和求模运算,运算符有:
- 加法:
+
- 减法:
-
- 乘法:
*
- 除法:
/
- 求模 (求余):
%
进行算数运算的两个操作数都是结果为数字的表达式。
但也有例外,+
号可以用来连接字符串,如果 +
号的一端是字符串,+
号的另外一端是数字,那么数字就会自动转换为字符串类型(使用当前页面语言的适当格式),之后使用 +
号作为字符串连接操作符。
FreeMarker 不能自动将字符串转换为数字,反之可以自动进行。
如果我们只想获取除法计算(或其它运算)的整数部分, 这可以使用内建函数 int
来解决:
1 | ${(x/2)?int} |
将会输出:
1 | 2 |
4.7 比较运算
如果要比较两个值是否相等,可以使用 =
,或者 ==
,两者是等效的。
=
或 !=
两边的表达式的结果都必须是标量,而且两个标量都必须是相同类型(也就是说字符串只能和字符串来比较,数字只能和数字来比较等)否则将会出错, 模板执行中断。
请注意FreeMarker进行的是精确的比较,所以字符串在比较时要注意大小写和空格: "x"
和 "x "
和 "X"
是不同的值。
对数字和日期类型的比较,也可以使用 <
, <=
,>=
和 >
。不能把它们当作字符串来比较。
使用 >=
和 >
的时候有一点小问题。FreeMarker 解释 >
的时候可以把它当作 FTL 标签的结束符。为了避免这种问题,可以使用 lt
代替 <
, lte
代替 <=
, gt
代替 >
还有 gte
代替 >=
, 例如 <#if x gt y>
。另外一个技巧是将表达式放到圆括号中,例如 <#if (x > y)>
,但第二种显然不优雅。
4.8 逻辑操作
常用的逻辑操作符:
- 逻辑 或:
||
- 逻辑 与:
&&
- 逻辑 非:
!
逻辑操作符仅仅在布尔值之间有效,若用在其他类型将会产生错误导致模板执行中止。
4.9 内建函数
内建函数就像 FreeMarker 在对象中添加的方法一样。要防止和实际方法和其它子变量的命名冲突,则不能使用点 (.
),这里使用问号 (?
)来和父对象分隔开。
为了简洁,如果方法没有参数,那么就可以忽略 ()
,比如想要获取 path
的长度,就可以写作:path?length
, 而不是 path?length()
。
重要的常见内建函数:
1 | ${testString?upper_case} |
假设 testString
中存储了字符串 ‘‘Tom & Jerry’’,而testSequnce中存储了字符串 “foo”,“bar” 和 “baz”,将会输出:
1 | TOM & JERRY |
内建函数的左侧可以是任意的表达式,而不仅仅是变量名。
完整的内建函数参考:内建函数参考
4.10 方法调用
如果有一个方法,那么可以使用方法调用操作。方法调用操作是使用逗号来分割在括号内的表达式而形成参数列表,这些值就是参数。方法调用操作将这些值传递给方法,然后返回一个结果。这个结果就是整个方法调用表达式的值。
假设程序员定义了一个可供调用的方法 repeat
。第一个参数是字符串类型,第二个参数是数字类型。方法的返回值是字符串类型,而方法要完成的操作是将第一个参数重复显示,显示的次数是第二个参数设定的值。
1 | ${repeat("Foo", 3)} |
将会输出:
1 | FooFooFoo |
方法调用也是普通表达式,和其它都是一样的,所以:
1 | ${repeat(repeat("x", 2), 3) + repeat("Foo", 4)?upper_case} |
将会输出:
1 | xxxxxxFOOFOOFOOFOO |
4.11 赋值运算符
这些并不是表达式,只是复制指令语法的一部分,比如 assign
, local
和 global
。 照这样,它们不能任意被使用。
<#assign x += y>
是 <#assign x = x + y>
的简写,<#assign x *= y>
是 <#assign x = x * y>
的简写等等…
<#assign x++ >
和 <#assign x += 1>
(或 <#assign x = x + 1>
)不同,它只做算术加法运算 (如果变量不是数字的话就会失败),而其它的是进行字符串,序列连接和哈希表连接的重载。 <#assign x-- >
是 <#assign x -= 1>
的简写。
4.12 操作符优先级
下表中的运算符按照优先程度降序排列:上面的操作符优先级高于它下面的。高优先级的运算符执行要先于优先级比它低的。表格同一行上的两个操作符优先级相同。当有相同优先级的二元运算符挨着出现时,它们按照从左到右的原则运算。
运算符组 | 运算符 |
---|---|
最高优先级运算符 | [subvarName] [subStringRange] . ?(methodParams) expr! expr?? |
一元前缀运算符 | + - ! |
乘除法,求模运算符 | * / % |
加减法运算符 | + - |
数字值域 | .. ..< ..! ..* |
关系运算符 | < > <= >= (gt ,lt ,etc.) |
相等,不等运算符 | == != = |
逻辑 “与” 运算符 | && |
逻辑 “或” 运算符 | ` |
5. 插值
插值的使用格式是: ${expression}
,这里的 expression
可以是所有种类的表达式(比如 ${100 + x}
)。插值只能在文本区和字符串表达式中使用。
表达式的结果必须是字符串,数字或者日期 / 时间 / 日期 - 时间值, 因为(默认是这样)仅仅这些值可以被插值自动转换为字符串。其它类型的值(比如布尔值,序列)必须 “手动地” 转换成字符串(后续会有一些建议), 否则就会发生错误,中止模板执行。
具体插值指南可以参考:插值
6. 自定义指令
6.1 基本内容
宏是有一个变量名的模板片段。可以在模板中使用宏作为自定义指令, 这样就能进行重复性的工作。比如:
1 | <#macro greet> |
在 <#macro greet>
和 </#macro>
之间的内容(称为 宏定义体)将会在使用该变量作为指令时执行。可以这样使用:
1 | <@greet></@greet> |
或者
1 | <@greet/> |
然后会输出:
1 | <font size="+2">Hello Joe!</font> |
6.2 参数
在自定义指令时,还可以指定若干个参数,比如:
1 | <#macro greet person> |
可以这样使用:
1 | <@greet person="Fred"/> and <@greet person="Batman"/> |
最终打印结果是这样的:
1 | <font size="+2">Hello Fred!</font> |
=
右边不一定是字符串,可以是 FTL 表达式。比如: <@greet person=Fred/>
也意味着使用变量的值 Fred
作为 person
参数, 而不是字符串"Fred"
。甚至还可以是复杂表达式。
自定义指令可以有多个参数。在使用时,参数的顺序不重要,因为在使用时我们已经指定了参数名和对应的值。
需要注意的是, 在使用自定义指令时,不能使用未提及的参数,同时也必须给出在宏中定义所有参数的值。
当然,我们在创建自定义指令时,可以给参数指定默认值,比如:
1 | <#macro greet person color="black"> |
后续可以这样使用宏:<@greet person="Fred"/>
,因为它和 <@greet person="Fred" color="black"/>
是相等的。如果在使用时重新设置 color 的值,那么新值会覆盖默认值。
明白下面这一点是至关重要的:someParam=foo
和 someParam="${foo}"
是不同的。第一种情况, 是把变量 foo
的值作为参数的值来使用。第二种情况则是使用插值形式的字符串,那么参数值就是字符串了,这个时候,foo
的值呈现为文本, 而不管 foo
是什么类型的。看下面这个例子: someParam=3/4
和 someParam="${3/4}"
是不同的。 如果指令需要 someParam
是一个数字值, 那么就不要用第二种方式。切记不要改变这些。
6.3 嵌套内容
自定义指令可以嵌套内容。 例如,下面这个例子中是创建了一个可以为嵌套的内容画出边框的宏:
1 | <#macro border> |
<#nested>
指令执行位于开始和结束标记指令之间的模板代码段。 如果这样写:
1 | <@border>The bordered text</@border> |
那么会输出:
1 | <table border=4 cellspacing=0 cellpadding=4><tr><td> |
<#nested>
指令也可以被多次调用:
1 | <#macro do_thrice> |
那么就会输出:
1 | Anything. |
如果不使用 nested
指令, 那么嵌套的内容就不会被执行。比如前面自定义的 great
指令:
1 | <#macro greet> |
然后这样使用:
1 | <@greet person="Joe"> |
greet
宏没有使用 nested
指令,FreeMarker 不会把它视为错误,只是输出:
1 | <font size="+2">Hello Joe!</font> |
嵌套的内容可以是任意有效的 FTL,包含其他的用户自定义指令。
在嵌套的内容中,宏的局部变量是不可见的。比如:
1 | <#macro repeat count> |
将会输出:
1 | test 3/1: ? ? ? |
此外不同的局部变量的设置是为每个宏自己调用的,不会导致混乱。比如有模板:
1 | <#macro test foo>${foo} (<#nested>) ${foo}</#macro> |
将会输出:
1 | A (B (C () C) B) A |
6.4 宏和循环变量
像 list
这样的预定义指令可以使用循环变量,自定义指令也可以有循环变量。比如:
1 | <#macro do_thrice> |
将会输出:
1 | 1 Anything. |
语法规则是给确定“循环”的循环变量传递真实值(比如重复嵌套内容)来作为 nested
指令的参数(当然参数可以是任意的表达式)。 循环变量的名称是在自定义指令的开始标记(<@...>
)的参数后面通过分号确定的。
一个宏可以使用多个循环变量(注意变量的顺序):
1 | <#macro repeat count> |
将会输出:
1 | 1. 0.5 |
在自定义指令的开始标签(分号之后)为循环变量指定不同的数字是没有问题的, 而不能在 nested
指令上使用。如果在分号之后指定的循环变量少, 那么就看不到 nested
指令提供的最后的值, 因为没有循环变量来存储这些值,下面这样使用是可以的:
1 | <@repeat count=4 ; c, halfc, last> |
如果在分号后面指定了比 nested
指令还多的变量, 那么最后的循环变量将不会被创建(在嵌套内容中不会被定义)。
7. 在模板中定义变量
我们知道,模板可以使用在数据模型中定义的变量。 在数据模型之外,模板本身也可以定义变量来使用,这些临时变量可以使用 FTL 指令来创建和替换。请注意每一次的模板执行工作都维护它自己的私有变量, 同时来渲染页面。变量的初始值是空,当模板执行工作结束这些变量便被销毁了。
可以访问一个在模板里定义的变量,就像是访问数据模型根 root 上的变量一样。 这个变量比定义在数据模型中的同名参数有更高的优先级,也就是说, 如果恰巧定义了一个名为 foo
的变量,而在数据模型中也有一个名为 foo
的变量, 那么模板中的变量就会将数据模型根上的变量隐藏(不是覆盖!)。 例如,${foo}
将会输出在模板中定义的变量。
在模板中可以定义三种类型的变量:
- “简单”变量: 它能从模板中的任何位置来访问,或者从使用
include
指令引入的模板访问。可以使用assign
指令来创建或替换这些变量。因为宏和方法只是变量,那么macro
指令和function
指令也可以用来设置变量,就像assign
那样。 - 局部变量:它们只能被设置在宏定义体内, 而且只在宏内可见。一个局部变量的生命周期只是宏的调用过程。可以使用
local
指令在宏定义体内创建或替换局部变量。 - 循环变量:循环变量是由如
list
指令自动创建的,而且它们只在指令的开始和结束标记内有效。宏的参数是局部变量而不是循环变量。 - 全局变量:这是一个高级话题了, 并且这种变量最好别用。即便它们属于不同的命名空间, 全局变量也被所有模板共享,因为它们是被
import
进来的, 不同于include
进来的。那么它们的可见度就像数据模型那样。 全局变量通过global
指令来定义。
使用 assign
创建和替换变量:
1 | <#assign x = 1> <#-- create variable x --> |
输出为:
1 | 1 |
局部变量也会隐藏(不是覆盖)同名的 “简单” 变量,循环变量也会隐藏(不是覆盖)同名的 “简单” 变量,比如:
1 | <#assign x = "plain"> |
将会输出:
1 | 1. plain |
内部循环变量可以隐藏外部循环变量,比如:
1 | <#list ["loop 1"] as x> |
将会输出:
1 | loop 1 |
请注意,循环变量的设置是通过指令调用时创建的(本例中的 <list ...>
标签)。没有其他的方式去改变循环变量的值(也就是说,不能使用定义指令来改变它的值),但可以使用一个循环变量来暂时隐藏另外一个。
有时会发生一个变量隐藏数据模型中的同名变量, 但是如果想访问数据模型中的变量,此时就可以使用 特殊变量 globals
。例如,假设我们在数据模型中有一个名为 user
的变量,值为 Big Joe
:
1 | <#assign user = "Joe Hider"> |
通过 global
指令设置的变量可以隐藏数据模型中的同名变量。通常,全局变量的设置会有精确的目的,但仍然可以使用如下方式来访问数据模型变量:.data_model.user
。
一个坑
使用 local
或 assign
指令在模板中定义变量通常有以下两种方式:
1 | <#local name = value> |
如果需要在不同的条件下定义不同的值,那么可以:
1 | <#local name> |
在一般的场景下,这两种方式并没有区别,但如果后续要使用定义的变量进行逻辑判断,那这两种方式就有些区别了。
假设当满足某些条件时,定义的变量值为 value1
,否则是空串,即 value2
是空串。后续使用定义的变量 name
进行 空串 判断,比如:
1 | <#if StringUtils.isNotEmpty(name)></#if> |
但最终得到的结果并不是原本期望的。
这是因为 采用第一种方式定义的变量所指向的值的两端存在一些空白字符。
为了避免这种情况,可以采用第二种方式来定义变量(更推荐),或者在使用定义的变量进行逻辑判断前,移除值两端的空白字符。
8. 命名空间
8.1 命名空间和库
当运行 FTL 模板时,可以使用 assign
和 macro
指令创建的变量的集合(可能是空的)。像这样的变量集合被称为 命名空间。 简单的情况下可以只使用一个命名空间,称之为 主命名空间。因为通常只使用该命名空间, 所以就没有意识到这点。
如果想创建可以重复使用的宏,函数和其他变量的集合,通常用术语来说就是引用 库。
使用多个命名空间是必然的。只要考虑你在一些项目中, 或者想和他人共享使用的时候,你是否有一个很大的宏的集合。但要确保库中没有宏(或其他变量)名和数据模型中变量同名, 而且也不能和模板中引用其他库中的变量同名是不可能的。通常来说,变量因为名称冲突时也会相互冲突。所以要为每个库中的变量使用不同的命名空间。
8.2 创建一个库
假设需要通用的变量 copyright
和 mail
:
1 | <#macro copyright date> |
把上面的这些定义存储在文件 lib/my_test.ftl
中(目录是存放模板的位置),假设想在 aWebPage.ftl
中使用这个模板。我们可以使用 <# import>
指令,就像下面这样:
1 | <#import "/lib/my_test.ftl" as my> |
将会输出:
1 | <p>Copyright (C) 1999-2002 Julia Smith. All rights reserved.</p> |
就算在主命名空间中有一个变量,名为 mail
或 copyright
,也不会引起混乱,因为两个模板使用了不同的命名空间。
8.2 在引入的命名空间中编写变量
偶尔想要在一个被包含的命名空间上创建或替换一个变量。 那么可以使用 assign
指令, 如果用到了它的 namespace
变量,例如下面这样:
1 | <#import "/lib/my_test.ftl" as my> |
将会输出:
1 | jsmith@acme.com |
8.3 命名空间和数据模型
数据模型中的变量在任何位置都是可见的。例如, 如果在数据模型中有一个名为 user
的变量,那么 lib/my_test.ftl
也能访问它, aWebPage.ftl
当然也能:
1 | <#macro copyright date> |
如果 user
是 Fred
的话,下面这个例子:
1 | <#import "/lib/my_test.ftl" as my> |
将会输出:
1 | <p>Copyright (C) 1999-2002 Fred. All rights reserved.</p> |
不要忘了在模板的命名空间(可以使用 assign
或 macro
指令来创建的变量)中的变量有着比数据模型中的变量更高的优先级。因此,数据模型的内容不会干涉到由库创建的变量。
8.4 命名空间的生命周期
命名空间由使用 import
指令中所写的路径来识别。如果想多次 import
这个路径,那么只会为第一次 import
引用创建命名空间并执行模板。后面相同路径的 import
只是创建一个哈希表当作访问相同命名空间的“门”。
my_test.ftl
中有:
1 | <#macro copyright date> |
在 aWebPage.ftl
中:
1 | <#import "/lib/my_test.ftl" as my> |
将会输出:
1 | jsmith@acme.com, jsmith@acme.com, jsmith@acme.com |
请注意,命名空间是不分层次的,它们相互之间是独立存在的。 那么,如果在命名空间 N1 中 import
命名空间 N2, 那 N2 也不在 N1 中,N1 只是可以通过哈希表来访问 N2。这和在主命名空间中 import
N2,然后直接访问命名空间 N2 是一样的过程。
每一次模板的执行过程,它都有一个私有的命名空间的集合。每一次模板执行工作都是一个分离且有序的过程,它们仅仅存在一段很短的时间,同时页面用以渲染内容,然后就和所有填充过的命名空间一起消失了。因此,无论何时我们说第一次调用 import
,一个单一模板执行工作的内容都是这样。
8.5 为他人编写库
如果想将一个库放在网络上,为了防止和其他作者使用库的命名相冲突,而且引入其他库时要书写简单,可以指定库路径的格式。
这个标准是: 库的路径必须对模板和其他库可用(可引用),就像这样:
1 | /lib/yourcompany.com/your_library.ftl |
假设我们为 Example 公司工作,它们拥有 www.example.com
网的主页, 我开发了一个部件库,那么要引入所写的 FTL 的路径应该是:
1 | /lib/example.com/widget.ftl |
需要注意的是,www 已经被省略了。第三次路径分割后的部分可以包含子目录,可以像下面这样写:
1 | /lib/example.com/commons/string.ftl |
一个重要的规则就是路径不应该包含大写字母,为了分隔词语, 使用下划线 _
,就像 wml_form
(而不是 wmlForm
这样的驼峰命名)
请注意,如果你的工作不是为公司或组织开发库,你应该使用项目主页的 URL,比如 /lib/example.sourceforge.net/example.ftl
,或 /lib/geocities.com/jsmith/example.ftl
。
9. 其他
这两个内容不是很重要,可以直接参考文档: