freeMarker(四)——模板开发指南之模板
学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net
模板开发指南之模板
1. 总体结构
实际上用程序语言编写的程序就是模板。 FTL (代表FreeMarker模板语言)。 这是为编写模板设计的非常简单的编程语言。
模板(FTL编程)是由如下部分混合而成的:
文本:文本会照着原样来输出。
插值:这部分的输出会被计算的值来替换。插值由
${
and}
所分隔(或者#{
and}
,这种风格已经不建议再使用了;点击查看更多)。FTL 标签:FTL标签和HTML标签很相似,但是它们却是给FreeMarker的指示, 而且不会打印在输出内容中。
注释:注释和HTML的注释也很相似,但它们是由
<#--
和-->
来分隔的。注释会被FreeMarker直接忽略, 更不会在输出内容中显示。
我们来看一个具体的模板。
<html>[BR] <head>[BR] <title>Welcome!</title>[BR] </head>[BR] <body>[BR] <#-- Greet the user with his/her name -->[BR] <h1>Welcome ${user}!</h1>[BR] <p>We have these animals:[BR] <ul>[BR] <#list animals as animal>[BR] <li>${animal.name} for ${animal.price} Euros[BR] </#list>[BR] </ul>[BR] </body>[BR] </html>
FTL是区分大小写的。 list
是指令的名称而 List
就不是。类似地 ${name}
和 ${Name}
或 ${NAME}
也是不同的。
请注意非常重要的一点: 插值 仅仅可以在 文本 中使用。
FTL 标签 不可以在其他 FTL 标签 和 插值中使用。比如, 这样做是 错误 的: <#if <#include 'foo'>='bar'>...</#if>
注释 可以放在 FTL 标签 和 插值中。比如:
<h1>Welcome ${user <#-- The name of user -->}!</h1>[BR] <p>We have these animals:[BR] <ul>[BR] <#list <#-- some comment... --> animals as <#-- again... --> animal>[BR] ...
如果您已经自行尝试测试上面所有示例的话:那么也许会注意到,一些空格、 制表符和换行符从模板的输出中都不见了,尽管我们之前已经说了 文本 是按照原样输出的。 现在不用为此而计较,这是由于FreeMarker的"空格剥离"特性在起作用, 它当然会自动去除一些多余的空格,制表符和换行符了。 这个特性在 后续内容 中会解释到。
2. 指令
使用 FTL标签来调用 指令。 在示例中已经调用了 list
指令。在语法上我们使用了两个标签: <#list animals as animal>
和 </#list>
。
FTL 标签分为两种:
开始标签:
<#directivename parameters>
结束标签:
</#directivename>
除了标签以 #
开头外,其他都和HTML,XML的语法很相似。 如果标签没有嵌套内容(在开始标签和结束标签之间的内容),那么可以只使用开始标签。 例如 <#if something>...</#if>
, 而FreeMarker知道 <#include something>
中的 include
指令没有可嵌套的内容。
parameters
的格式由 directivename
来决定。
事实上,指令有两种类型: 预定义指令 和 用户自定义指令。 对于用户自定义的指令使用 @
来代替 #
,比如,<@mydirective parameters>...</@mydirective>
。 更深的区别在于如果指令没有嵌套内容,那么必须这么使用 <@mydirective parameters />
,这和XML语法很相似 (例如 <img ... />
)。 但用户自定义指令是更高级的话题,将会在 后续章节 中继续讨论。
像HTML标签一样,FTL标签也必须正确地嵌套使用。下面这段示例代码就是错的, 因为 if
指令在 list
指令嵌套内容的内外都有:
<ul> <#list animals as animal> <li>${animal.name} for ${animal.price} Euros <#if user == "Big Joe"> (except for you) </#list> <#-- WRONG! The "if" has to be closed first. --> </#if> </ul>
请注意,FreeMarker 仅仅关心FTL标签的嵌套而不关心HTML标签的嵌套。 它只会把HTML看做是文本,不会来解释HTML。
如果你尝试使用一个不存在的指令(比如,输错了指令的名称), FreeMarker 就会拒绝执行模板,同时抛出错误信息。
FreeMarker会忽略FTL标签中多余的 空白标记,所以也可以这么来写代码:
<#list[BR] animals as[BR] animal[BR] >[BR] ${animal.name} for ${animal.price} Euros[BR] </#list >
当然,也不能在 <
,</
和指令名中间入空白标记。
1.通过配置,FreeMarker 可以在FTL标签和FTL注释中, 使用 [
和 ]
来代替 <
和 >
,就像 [#if user == "Big Joe"]...[/#if]
。
2.通过配置,FreeMarker 可以不需要 #
来理解预定义指令(比如 <if user == "Big Joe">...</if>
)。 而我们不建议这样来使用。
3. 表达式
当需要给插值或者指令参数提供值时,可以使用变量或其他复杂的表达式。 例如,我们设x为8,y为5,那么 (x + y)/2
的值就会被处理成数字类型的值6.5。
在我们展开细节之前,先来看一些具体的例子:
当给插值提供值时:插值的使用方式为
${expression}
, 把它放到你想输出文本的位置上,然后给值就可以打印出来了。 即${(5 + 8)/2}
会打印出 ''6.5'' 来 (如果输出的语言不是美国英语,也可能打印出''6,5''来)。当给指令参数提供值时:在入门章节我们已经看到
if
指令的使用了。这个指令的语法是:<#if expression>...</#if>
。 这里的表达式计算结果必须是布尔类型的。比如<#if 2 < 3>
中的2 <3
(2小于3)是结果为true
的布尔表达式。
3.1 快速浏览(备忘单)
- 直接指定值
- 检索变量
- 字符串操作
- 序列操作
- 哈希表操作
- 连接:
passwords + { "joe": "secret42" }
- 连接:
- 算术运算:
(x * 1.5 + 10) / 2 - y % 100
- 比较运算:
x == y
,x != y
,x < y
,x > y
,x >= y
,x <= y
,x lt y
,x lte y
,x gt y
,x gte y
, 等等。。。。。。 - 逻辑操作:
!registered && (firstVisit || fromEurope)
- 内建函数:
name?upper_case
,path?ensure_starts_with('/')
- 方法调用:
repeat("What", 3)
- 处理不存在的值:
- 赋值操作:
=
,+=
,-=
,*=
,/=
,%=
,++
,--
请参考: 运算符优先级
3.2 直接确定值
通常我们喜欢是使用直接确定的值而不是计算的结果。
字符串
在文本中确定字符串值的方法是看双引号,比如: "some text"
,或单引号,比如: 'some text'
。这两种形式是等同的。 如果文本自身包含用于字符引用的引号 ( "
或 '
)或反斜杠时, 应该在它们的前面再加一个反斜杠;这就是转义。 转义允许直接在文本中输入任何字符, 也包括换行。例如:
${"It's \"quoted\" and this is a backslash: \\"} ${'It\'s "quoted" and this is a backslash: \\'}
将会输出:
It's "quoted" and this is a backslash: \ It's "quoted" and this is a backslash: \
这里当然可以直接在模板中输入文本而不需要 ${...}
。 但是我们在这里用它只是为了示例来说明表达式的使用。
下面的表格是FreeMarker支持的所有转义字符。 在字符串使用反斜杠的其他所有情况都是错误的,运行这样的模板都会失败。
转义序列 | 含义 |
---|---|
\" |
引号 (u0022) |
\' |
单引号(又称为撇号) (u0027) |
\{ |
起始花括号:{ |
\\ |
反斜杠 (u005C) |
\n |
换行符 (u000A) |
\r |
回车 (u000D) |
\t |
水平制表符(又称为tab) (u0009) |
\b |
退格 (u0008) |
\f |
换页 (u000C) |
\l |
小于号:< |
\g |
大于号:> |
\a |
&符:& |
\xCode |
字符的16进制 Unicode 码 (UCS 码) |
在 \x
之后的 Code
是1-4位的16进制码。下面这个示例中都是在字符串中放置版权符号: "\xA9 1999-2001"
, "\x0A9 1999-2001"
, "\x00A9 1999-2001"
。 如果紧跟16进制码后一位的字符也能解释成16进制码时, 就必须把4位补全,否则FreeMarker就会误解你的意图。
请注意,字符序列 ${
(和 #{
) 有特殊的含义,它们被用做插入表达式的数值(典型的应用是变量的值: "Hello ${user}!"
)。这将在 后续章节中解释。 如果想要打印 ${
或 #{
, 就要使用下面所说的原生字符串,或者进行转义。就像 "foo $\{bar}"
中的 {
。
原生字符串是一种特殊的字符串。在原生字符串中, 反斜杠和 ${
没有特殊含义, 它们被视为普通的字符。为了表明字符串是原生字符串, 在开始的引号或单引号之前放置字母r
,例如:
${r"${foo}"} ${r"C:\foo\bar"}
将会输出:
${foo} C:\foo\bar
数字
输入不带引号的数字就可以直接指定一个数字, 必须使用点作为小数的分隔符而不能是其他的分组分隔符。 可以使用 -
或 +
来表明符号 (+
是多余的)。 科学记数法暂不支持使用 (1E3
就是错误的), 而且也不能在小数点之前不写0(.5
也是错误的)。
下面的数字都是合法的:0.08
, -5.013
,8
, 008
,11
, +11
请注意,像 08
、 +8
、 8.00
和 8
这样的数值是完全等同的,它们都是数字8。 所以, ${08}
、${+8}
、 ${8.00}
和 ${8}
的输出都是一样的。
布尔值
直接写 true
或者 false
就表示一个布尔值了,不需使用引号。
序列
指定一个文字的序列,使用逗号来分隔其中的每个 子变量, 然后把整个列表放到方括号中。例如:
<#list ["foo", "bar", "baz"] as x> ${x} </#list>
将会输出:
foo bar baz
列表中的项目是表达式,那么也可以这样做: [2 + 2, [1, 2, 3, 4], "foo"]
。 其中第一个子变量是数字4,第二个子变量是一个序列, 第三个子变量是字符串"foo"。
值域
值域也是序列,但它们由指定包含的数字范围所创建, 而不需指定序列中每一项。比如: 0..<m
,这里假定 m
变量的值是5,那么这个序列就包含 [0, 1, 2, 3, 4]
。值域的主要作用有:使用 <#list...>
来迭代一定范围内的数字,序列切分 和 字符串切分。
值域表达式的通用形式是( start
和 end
可以是任意的结果为数字表达式):
start..end
: 包含结尾的值域。比如1..4
就是[1, 2, 3, 4]
, 而4..1
就是[4, 3, 2, 1]
。当心一点, 包含结尾的值域不会是一个空序列,所以0..length-1
就是 错误的,因为当长度是0
时, 序列就成了[0, -1]
。start..<end
或start..!end
: 不包含结尾的值域。比如1..<4
就是[1, 2, 3]
,4..<1
就是[4, 3, 2]
, 而1..<1
表示[]
。请注意最后一个示例; 结果可以是空序列,和..<
和..!
没有区别; 最后这种形式在应用程序中使用了<
字符而引发问题(如HTML编辑器等)。start..*length
: 限定长度的值域,比如10..*4
就是[10, 11, 12, 13]
,10..*-4
就是[10, 9, 8, 7]
,而10..*0
表示[]
。当这些值域被用来切分时, 如果切分后的序列或者字符串结尾在指定值域长度之前,则切分不会有问题;请参考 序列切分 来获取更多信息。注意:限定长度的值域是在 FreeMarker 2.3.21版本中引入的。
start..
: 无右边界值域。这和限制长度的值域很像,只是长度是无限的。 比如1..
就是[1, 2, 3, 4, 5, 6, ... ]
,直到无穷大。 但是处理(比如列表显示)这种值域时要万分小心,处理所有项时, 会花费很长时间,直到内存溢出应用程序崩溃。 和限定长度的值域一样,当它们被切分时, 遇到切分后的序列或字符串结尾时,切分就结束了。警告!无右边界值域在 FreeMarker 2.3.21 版本以前只能用于切分, 若用于其它用途,它就像空序列一样了。要使用新的特性, 使用 FreeMarker 2.3.21 版本是不够的,程序员要设置
incompatible_improvements
至少到2.3.21版本。
值域的进一步注意事项:
值域表达式本身并没有方括号,比如这样编写代码
<#assign myRange = 0..<x>
, 而不是<#assign myRange = [0..<x]>
。 后者会创建一个包含值域的序列。方括号是切分语法的一部分,就像seq[myRange]
。可以在
..
的两侧编写算术表达式而不需要圆括号, 就像n + 1 ..< m / 2 - 1
。..
,..<
,..!
和..*
是运算符, 所以它们中间不能有空格。就像n .. <m
这样是错误的,但是n ..< m
这样就可以。无右边界值域的定义大小是2147483647 (如果
incompatible_improvements
低于2.3.21版本,那么就是0), 这是由于技术上的限制(32位)。但当列表显示它们的时候,实际的长度是无穷大。值域并不存储它们包含的数字,那么对于
0..1
和0..100000000
来说,创建速度都是一样的, 并且占用的内存也是一样的。
哈希表
在模板中指定一个哈希表,就可以遍历用逗号分隔开的"键/值"对, 把列表放到花括号内即可。键和值成对出现并以冒号分隔。比如: { "name": "green mouse", "price": 150 }
。 请注意名和值都是表达式,但是用来检索的名称就必须是字符串类型, 而值可以是任意类型。
3.3 检索变量
顶层变量
访问顶层的变量,可以简单地使用变量名。例如, 用表达式 user
就可以在根上获取以 "user" 为名存储的变量值。然后打印出存储在里面的内容:
${user}
如果没有顶层变量,那么 FreeMarker 在处理表达式时就会发生错误, 进而终止模板的执行(除非程序员事先配置了 FreeMarker)。
在这种表达式中,变量名只可以包含字母(也可以是非拉丁文), 数字(也可以是非拉丁数字),下划线 (_
), 美元符号 ($
),at符号 (@
)。 此外,第一个字符不可以是ASCII码数字(0
-9
)。 从 FreeMarker 2.3.22 版本开始,变量名在任何位置也可以包含负号 (-
),点(.
)和冒号(:
), 但这些必须使用前置的反斜杠(\
)来转义, 否则它们将被解释成操作符。比如,读取名为"data-id"的变量, 表达式为 data\-id
,因为 data-id
将被解释成 "data minus id"。 (请注意,这些转义仅在标识符中起作用,而不是字符串中。)
从哈希表中检索数据
如果有一个表达式的结果是哈希表, 那么我们可以使用点和子变量的名字得到它的值, 假设我们有如下的数据模型:
(root) | +- book | | | +- title = "Breeding green mouses" | | | +- author | | | +- name = "Julia Smith" | | | +- info = "Biologist, 1923-1985, Canada" | +- test = "title"
现在,就可以通过book.title
来读取 title
,book表达式将返回一个哈希表 (就像上一章中解释的那样)。按这种逻辑进一步来说,我们可以使用表达式 book.author.name
来读取到auther的name。
如果我们想指定同一个表达式的子变量,那么还有另外一种语法格式: book["title"]
。在方括号中可以给出任意长度字符串的表达式。 在上面这个数据模型示例中还可以这么来获取title: book[test]
。 下面这些示例它们含义都是相等的: book.author.name
, book["author"].name
, book.author.["name"]
, book["author"]["name"]
。
当使用点式语法时,顶层变量名的命名也有相同的限制 (命名时只能使用字母,数字,_
,$
, @
,但是不能使用 0
-9
开头, 同时,从2.3.22版本开始,也可以使用 \-
,\.
和 \:
)。当使用方括号语法时,则没有这样的限制, 因为名称可以是任意表达式的结果。(请注意,对于FreeMarker的XML支持来说, 如果子变量名称是 *
(星号) 或者 **
,那么就不要使用方括号语法。)
对于顶层变量来说,如果尝试访问一个不存在的变量也会引起错误导致解析执行模板中断 (除非程序员事先配置过FreeMarker)。
从序列中检索数据
这和从哈希表中检索是相同的,但是只能使用方括号语法形式来进行, 而且方括号内的表达式最终必须是一个数字而不是字符串。比如,要从 示例数据模型 中获取第一个动物的名字 (记住第一项数字索引是0而不是1),可以这么来写: animals[0].name
特殊变量
特殊变量是由FreeMarker引擎本身定义的。 使用它们,可以按照如下语法形式来进行: .variable_name
。.
3.4 字符串操作
插值 (或连接)
如果要在字符串中插入表达式的值,可以在字符串的文字中使用 ${...}
(已经废弃的 #{...}
)。 ${...}
在字符串中的作用和在 文本 区是相同的 (它遵守相同的 本地化敏感 的数字和日期/时间格式), 而不是 自动转义。
示例 (假设user是 ''Big Joe''):
<#assign s = "Hello ${user}!"> ${s} <#-- Just to see what the value of s is -->
将会输出:
Hello Big Joe!
用户所犯的一个常见错误是将插值放在了不需要/不应该使用的地方。 插值 仅 在 文本 区 中有效。(比如, <h1>Hello ${name}!</h1>
) 还有在字符串值中 (比如, <#include "/footer/${company}.html">
)。 典型的 错误 使用是 <#if ${big}>...</#if>
, 这会导致语法错误。简单写为 <#if big>...</#if>
即可。 而且, <#if "${big}">...</#if>
也是 错误的, 因为它将参数值转换为字符串,但是 if
指令只接受布尔值, 那么这将导致运行时错误。
另外,也可以使用 +
号来达到类似的效果:
<#assign s = "Hello " + user + "!">
这样的效果和使用 ${...}
是一样的。
因为 +
和使用 ${...}
的规则相同,附加的字符串受到 locale
, number_format
,date_format
, time_format
,datetime_format
和 boolean_format
等等设置的影响, 这是对人来说的,而不是通常机器的解析。默认情况下,这会导致数字出问题, 因为很多地区使用分组(千分位分隔符),那么 "someUrl?id=" + id
就可能会是 "someUrl?id=1 234"
。 要预防这种事情的发生,请使用 ?c
(对计算机来说)内建函数,那么在 "someUrl?id=" + id?c
或 "someUrl?id=${id?c}"
中, 就会得到如 "someUrl?id=1234"
这样的输出, 而不管本地化和格式的设置是什么。
获取字符
在给定索引值时可以获取字符串中的一个字符,这和 序列的子变量是相似的, 比如 user[0]
。这个操作执行的结果是一个长度为1的字符串, FTL并没有独立的字符类型。和序列中的子变量一样,这个索引也必须是数字, 范围是从0到字符串的长度,否则模板的执行将会发生错误并终止。
由于序列的子变量语法和字符的getter语法冲突, 那么只能在变量不是序列时使用字符的getter语法(因为FTL支持多类型值,所以它是可能的), 这种情况下使用序列方式就比较多。(为了变通,可以使用 内建函数 string
,比如 user?string[0]
)
示例(假设 user 是 "Big Joe"):
${user[0]} ${user[4]}
将会输出(请注意第一个字符的索引是0):
B J
字符串切分 (子串)
可以按照 切分序列 (请参看)的相同方式来切分字符串,这就是使用字符来代替序列。不同的是:
降序域不允许进行字符串切分。 (因为不像序列那样,很少情况下会想反转字符串。 如果真要这样做了,那就是疏忽。)
如果变量的值既是字符串又是序列(多类型值), 那么切分将会对序列进行,而不是字符串。当处理XML时, 这样的值就是普通的了。此时,可以使用
someXMLnode?string[range]
。一个遗留的bug:值域 包含 结尾时, 结尾小于开始索引并且是是非负的(就像在
"abc"[1..0]
中), 会返回空字符串而不是错误。(在降序域中这应该是个错误。) 现在这个bug已经向后兼容,但是不应该使用它,否在就会埋下一个错误。
示例:
<#assign s = "ABCDEF"> ${s[2..3]} ${s[2..]} ${s[2..*3]} ${s[2..*100]} ${s[2..]}
将会输出:
CD CD CDE CDEF CDEF
下面包括了一些使用内建函数来方便字符串切分的典型用例: remove_beginning
, remove_ending
, keep_before
, keep_after
, keep_before_last
, keep_after_last
3.5 序列操作
连接
序列的连接可以按照字符串那样使用 +
号来进行,例如:
<#list ["Joe", "Fred"] + ["Julia", "Kate"] as user> - ${user} </#list>
将会输出:
- Joe - Fred - Julia - Kate
请注意,不要在很多重复连接时使用序列连接操作, 比如在循环中往序列上追加项目,而这样的使用是可以的: <#list users + admins as person>
。 尽管序列连接的速度很快,而且速度是和被连接序列的大小相独立的, 但是最终的结果序列的读取却比原先的两个序列慢那么一点。 通过这种方式进行的许多重复连接最终产生的序列读取的速度会慢。
序列切分
使用 seq[range]
, 这里 range
是一个值域 此处有说明, 就可以得到序列的一个切分。结果序列会包含原序列 (seq
)中的项, 而且索引在值域中。例如:
<#assert seq = ["A", "B", "C", "D", "E"]> <#list seq[1..3] as i>${i}</#list>
将会输出:
BCD
此外,切分后序列中的项会和值域的顺序相同。 那么上面的示例中,如果值域是 3..1
将会输出 DCB
。
值域中的数字必须是序列可使用的合法索引, 否则模板的处理将会终止并报错。像上面的示例那样, seq[-1..0]
就会出错, 而 seq[-1]
就是合法的。 seq[1..5]
也不对, 因为 seq[5]
是非法的。 (请注意,尽管100已经越界,但是 seq[100..<100]
或 seq[100..*0]
是合法的,因为那些值域都是空。)
限制长度的值域 (start..*length
) 和无右边界值域 (start..
) 适用于切分后序列的长度。它们会切分可用项中尽可能多的部分:
<#assign seq = ["A", "B", "C"]> Slicing with length limited ranges: - <#list seq[0..*2] as i>${i}</#list> - <#list seq[1..*2] as i>${i}</#list> - <#list seq[2..*2] as i>${i}</#list> <#-- Not an error --> - <#list seq[3..*2] as i>${i}</#list> <#-- Not an error --> Slicing with right-unlimited ranges: - <#list seq[0..] as i>${i}</#list> - <#list seq[1..] as i>${i}</#list> - <#list seq[2..] as i>${i}</#list> - <#list seq[3..] as i>${i}</#list>
将会输出:
Slicing with length limited ranges: - AB - BC - C - Slicing with right-unlimited ranges: - ABC - BC - C -
请注意,上面的有限长度切分和无右边界切分都允许开始索引超过最后项 一个 (但不能再多了)。
要对序列进行给定大小的切分,就应该使用内建函数 chunk
。
3.6 哈希表操作
连接
像连接字符串那样,也可以使用 +
号的方式来连接哈希表。如果两个哈希表含有键相同的项,那么在 +
号右侧的哈希表中的项优先。例如:
<#assign ages = {"Joe":23, "Fred":25} + {"Joe":30, "Julia":18}> - Joe is ${ages.Joe} - Fred is ${ages.Fred} - Julia is ${ages.Julia}
将会输出:
- Joe is 30 - Fred is 25 - Julia is 18
请注意,很多项连接时不要使用哈希表连接, 比如在循环时往哈希表中添加新项。这和序列连接 的情况是一致的。
3.7 算数运算
算数运算包含基本的四则运算和求模运算,运算符有:
- 加法:
+
- 减法:
-
- 乘法:
*
- 除法:
/
- 求模 (求余):
%
示例:
${100 - x * x} ${x / 2} ${12 % 10}
假设 x
是 5,将会输出:
75 2.5 2
要保证两个操作数都是结果为数字的表达式。 下面的这个例子在运行时,FreeMarker就会发生错误, 因为是字符串 "5"
而不是数字5:
${3 * "5"} <#-- WRONG! -->
但这种情况也有一个例外,就是 +
号,它是用来 连接字符串的。 如果 +
号的一端是字符串,+
号的另外一端是数字,那么数字就会自动转换为字符串类型(使用当前页面语言的适当格式), 之后使用 +
号作为字符串连接操作符。示例如下:
${3 + "5"}
将会输出:
35
通常来说,FreeMarker不会自动将字符串转换为数字,反之会自动进行。
有时我们只想获取除法计算(或其它运算)的整数部分, 这可以使用内建函数 int
来解决。(关于内建函数 后续章节会来解释):
${(x/2)?int} ${1.1?int} ${1.999?int} ${-1.1?int} ${-1.999?int}
假设 x
是 5,将会输出:
2 1 1 -1 -1
3.8 比较运算
有时我们需要知道两个值是否相等,或者哪个值更大一点。
为了演示具体的例子,我们在这里使用 if
指令。 if
指令的用法是: <#if expression>...</#if>
, 其中的表达式的值必须是布尔类型,否则将会出错,模板执行中断。 如果表达式的结果是 true
, 那么在开始和结束标记内的内容将会被执行,否则就会被跳过。
测试两个值相等使用 =
(或者采用Java和C语言中的 ==
;二者是完全等同的。) 测试两个值不等使用 !=
。比如, 假设 user
是 ''Big Joe'':
<#if user == "Big Joe"> It is Big Joe </#if> <#if user != "Big Joe"> It is not Big Joe </#if>
<#if ...>
中的表达式 user = "Big Joe"
就是布尔值 true
,面的代码将会输出 ''It is Big Joe''。
=
或 !=
两边的表达式的结果都必须是标量,而且两个标量都必须是相同类型 (也就是说字符串只能和字符串来比较,数字只能和数字来比较等)否则将会出错, 模板执行中断。例如 <#if 1 = "1">
就会导致错误。 请注意FreeMarker进行的是精确的比较,所以字符串在比较时要注意大小写和空格: "x"
和 "x "
和 "X"
是不同的值。
对数字和日期类型的比较,也可以使用 <
, <=
,>=
和 >
。不能把它们当作字符串来比较。比如:
<#if x <= 12> x is less or equivalent with 12 </#if>
使用 >=
和 >
的时候有一点小问题。FreeMarker解释 >
的时候可以把它当作FTL标签的结束符。为了避免这种问题,可以使用 lt
代替 <
, lte
代替 <=
, gt
代替 >
还有 gte
代替 >=
, 例如 <#if x gt y>
。另外一个技巧是将表达式放到 圆括号 中, 尽管这么写并不优雅,例如 <#if (x > y)>
。
FreeMarker 也支持一些其它的选择,但是这些已经废弃了:
在可能出问题的关系标记处使用
>
和<
,就像:<#if x > y>
或<#if x >= y>
。 请注意通常FTL不支持标签中的实体引用(如&...;
这些东西); 做算术比较时就会有异常。\lt
,\lte
,\gt
和\gte
使用他们时, 不带反斜杠的效果一样。
3.9 逻辑操作
常用的逻辑操作符:
- 逻辑 或:
||
- 逻辑 与:
&&
- 逻辑 非:
!
逻辑操作符仅仅在布尔值之间有效,若用在其他类型将会产生错误导致模板执行中止。
例如:
<#if x < 12 && color == "green"> We have less than 12 things, and they are green. </#if> <#if !hot> <#-- here hot must be a boolean --> It's not hot. </#if>
3.10 内建函数
内建函数就像FreeMarker在对象中添加的方法一样。 要防止和实际方法和其它子变量的命名冲突,则不能使用点 (.
),这里使用问号 (?
)来和父对象分隔开。 比如,想要保证 path
有起始的 /
,那么可以这么来写: path?ensure_starts_with('/')
。 path
后的Java对象(通常就是 String
) 并没有这样的方法,这是FreeMarker添加的。为了简洁,如果方法没有参数, 那么就可以忽略 ()
,比如想要获取 path
的长度,就可以写作:path?length
, 而不是 path?length()
。
内建函数关键性的另外一个原因是常见的(尽管它依赖于配置的设置), FreeMarker不会暴露对象的Java API。那么尽管Java的 String
类有 length()
方法,但在模板中却是不可见的, 就 不得不 使用 path?length
来代替。 这里的优点是模板不依赖下层Java对象的精确类型。(比如某些场景中, path
也可能是 java.nio.Path
类型, 如果程序员配置了FreeMarker去暴露 Path
对象作为FTL字符串类型,那么模板就不会在意了,使用 ?length
也是可以的, 即便 java.nio.Path
没有类似的方法。)
可以找到一些 此处提及的常用内建函数,还有 完整的内建函数参考。 现在,我们只需了解一些重要的内建函数就行了:
比如:
${testString?upper_case} ${testString?html} ${testString?upper_case?html} ${testSequence?size} ${testSequence?join(", ")}
假设 testString
中存储了字符串 ''Tom & Jerry'', 而testSequnce中存储了字符串 "foo", "bar" 和 "baz", 将会输出:
TOM & JERRY Tom & Jerry TOM & JERRY 3 foo, bar, baz
请注意:上面的 testString?upper_case?html
。因为 test?upper_case
的结果是字符串,那么就可以在它的上面 使用内建函数 html
。
很自然可以看到,内建函数的左侧可以是任意的表达式,而不仅仅是变量名:
${testSeqence[1]?cap_first} ${"horse"?cap_first} ${(testString + " & Duck")?html} Bar Horse Tom & Jerry & Duck
3.11 方法调用
如果有一个方法,那么可以使用方法调用操作。 方法调用操作是使用逗号来分割在括号内的表达式而形成参数列表,这些值就是参数。 方法调用操作将这些值传递给方法,然后返回一个结果。 这个结果就是整个方法调用表达式的值。
假设程序员定义了一个可供调用的方法 repeat
。 第一个参数是字符串类型,第二个参数是数字类型。方法的返回值是字符串类型, 而方法要完成的操作是将第一个参数重复显示,显示的次数是第二个参数设定的值。
${repeat("Foo", 3)}
将会输出:
FooFooFoo
这里的 repeat
就是方法变量(根据如何 访问顶层变量), ("What", 3)
就调用了该方法。
这里需要强调方法调用也是普通表达式,和其它都是一样的,所以:
${repeat(repeat("x", 2), 3) + repeat("Foo", 4)?upper_case}
将会输出:
xxxxxxFOOFOOFOOFOO
3.12 处理不存在的值
这些操作符是从 FreeMarker 2.3.7 版本开始引入的(用来代替内建函数 default
, exists
和 if_exists
)。
正如我们前面解释的那样,当试图访问一个不存在的变量时, FreeMarker 将会报错而导致模板执行中断。 通常我们可以使用两个特殊操作符来压制这个错误,控制这种错误情况。 被控制的变量可以是顶层变量,哈希表或序列的子变量。 此外这些操作符还能处理方法调用的返回值不存在的情况 (这点对Java程序员来说: 返回值是 null
或者返回值为 void
类型),通常来说,我们应该使用这些操作符来控制可能不存在的值, 而不仅仅是不存在的变量。
对于知道Java中 null
的人来说,FreeMarker 2.3.x 版本把它们视为不存在的变量。单地说,模板语言中没有 null
这个概念。比如有一个bean,bean中有一个 maidenName
属性, 对于模板而言(假设没有配置FreeMarker来使用一些极端的对象包装), 该属性的值是 null
,和不存在这个属性的情况是一致的。 调用方法的返回值如果是 null
的话 FreeMarker 也会把它当作不存在的变量来处理 (假定只使用了普通的对象包装)。可以在 FAQ 中了解更多内容。
默认值操作符
使用形式: unsafe_expr!default_expr
或 unsafe_expr!
or (unsafe_expr)!default_expr
或 (unsafe_expr)!
这个操作符允许你为可能不存在的变量指定一个默认值。
例如,假设下面展示的代码中没有名为 mouse
的变量:
${mouse!"No mouse."} <#assign mouse="Jerry"> ${mouse!"No mouse."}
将会输出:
No mouse. Jerry
默认值可以是任何类型的表达式,也可以不必是字符串。 也可以这么写:hits!0
或 colors!["red", "green", "blue"]
。 默认值表达式的复杂程度没有严格限制,还可以这么来写: cargo.weight!(item.weight * itemCount + 10)
。
如果在 !
后面有复合表达式, 如 1 + x
,通常 使用括号,如 ${x!(1 + y)}
或 ${(x!1) + y)}
,这样就根据你的意图来确定优先级。 由于FreeMarker 2.3.x 版本的源码中的小失误所以必须这么来做。 !
(作为默认值操作) 右侧的优先级非常低。 这就意味着 ${x!1 + y}
会被 FreeMarker 误解为 ${x!(1 + y)}
,而真实的意义是 ${(x!1) + y}
。 这个源码错误在FreeMarker 2.4中会得到修正。 在编程中注意这个错误,要么就使用FreeMarker 2.4!
如果默认值被省略了,那么结果将会是空串,空序列或空哈希表。 (这是 FreeMarker 允许多类型值的体现)请注意,如果想让默认值为 0
或 false
,则不能省略它。例如:
(${mouse!}) <#assign mouse = "Jerry"> (${mouse!})
将会输出:
() (Jerry)
因为语法的含糊 <@something a=x! b=y />
将会解释为 <@something a=x!(b=y) />
,那就是说 b=y
将会被视为是比较运算,然后结果作为 x
的默认值,而不是想要的参数 b
。 为了避免这种情况,如下编写代码即可: <@something a=(x!) b=y />
用于非顶层变量时,默认值操作符可以有两种使用方式:
product.color!"red"
如果是这样的写法,那么在 product
中, 当 color
不存在时(返回 "red"
), 将会被处理,但是如果连 product
都不存在时将不会处理。 也就是说这样写时变量 product
必须存在,否则模板就会报错。
(product.color)!"red"
这时,如果当 product.color
不存在时也会被处理, 那就是说,如果 product
不存在或者 product
存在而 color
不存在,都能显示默认值 "red"
而不会报错。 本例和上例写法的重要区别在于用括号时, 就允许其中表达式的任意部分可以未定义。而没有括号时, 仅允许表达式的最后部分可以不被定义。
当然,默认值操作也可以作用于序列子变量,比如:
<#assign seq = ['a', 'b']> ${seq[0]!'-'} ${seq[1]!'-'} ${seq[2]!'-'} ${seq[3]!'-'}
将会输出:
a b - -
如果序列索引是负数(比如 seq[-1]!'-'
) 也会发生错误,不能使用该运算符或者其它运算符去压制它。
不存在值检测操作符
使用形式: unsafe_expr??
或 (unsafe_expr)??
这个操作符告诉我们一个值是否存在。基于这种情况, 结果是 true
或 false
。
示例如下,假设并没有名为 mouse
的变量:
<#if mouse??> Mouse found <#else> No mouse found </#if> Creating mouse... <#assign mouse = "Jerry"> <#if mouse??> Mouse found <#else> No mouse found </#if>
将会输出:
No mouse found Creating mouse... Mouse found
访问非顶层变量的使用规则和默认值操作符也是一样的, 也就是说,可以写 product.color??
和 (product.color)??
。
3.13 赋值操作符
这些并不是表达式,只是复制指令语法的一部分,比如 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>
的简写。
3.14 括号
括号可以用来给任意表达式分组。示例如下:
<#-- Output will be: --> ${3 * 2 + 2} <#-- 8 --> ${3 * (2 + 2)} <#-- 12 --> ${3 * ((2 + 2) * (1 / 2))} <#-- 6 --> ${"green " + "mouse"?upper_case} <#-- green MOUSE --> ${("green " + "mouse")?upper_case} <#-- GREEN MOUSE --> <#if !(color == "red" || color == "green")> The color is nor red nor green </#if>
请注意,方法调用表达式 使用的括号和给表达式分组的括号含义是完全不同的。
3.15 表达式中的空格
FTL 忽略表达式中的多余的 空格。下面的表示是相同的:
${x + ":" + book.title?upper_case}
和
${x+":"+book.title?upper_case}
和
${ x + ":" + book . title ? upper_case }
3.16 操作符的优先级
下面的表格显示了已定义操作符的优先级。 表格中的运算符按照优先程度降序排列:上面的操作符优先级高于它下面的。 高优先级的运算符执行要先于优先级比它低的。表格同一行上的两个操作符优先级相同。 当有相同优先级的二元运算符(运算符有两个''参数'',比如 +
和-
)挨着出现时,它们按照从左到右的原则运算。
运算符组 | 运算符 |
---|---|
最高优先级运算符 | [subvarName] [subStringRange] . ? (methodParams) expr! expr?? |
一元前缀运算符 | +expr -expr !expr |
乘除法,求模运算符 | * / % |
加减法运算符 | + - |
数字值域 | .. ..< ..! ..* |
关系运算符 | < > <= >= (and equivalents: gt , lt , etc.) |
相等,不等运算符 | == != (and equivalents: = ) |
逻辑 "与" 运算符 | && |
逻辑 "或" 运算符 | || |
如果你熟悉C语言,Java语言或JavaScript语言, 请注意 FreeMarker 中的优先级规则和它们是相同的, 除了那些只有FTL本身含有的操作符。
因为编程的失误,默认值操作符 (exp!exp
) 不在上面的表格中,按照向后兼容的原则,在 FreeMarker 2.4 版本中将会修正它。 而且它将是最高优先级的运算符,但是在FreeMarker 2.3.x 版本中它右边的优先级由于失误就非常低。 所以在默认值操作符的右边中使用复杂表达式时可以使用括号, 可以是 x!(y + 1)
或者是 (x!y) + 1
。 而不能是 x!y + 1
。
4. 插值
4.1 概览
插值的使用格式是: ${expression}
,这里的 expression
可以是所有种类的表达式(比如 ${100 + x}
)。
插值是用来给 表达式
插入具体值然后转换为文本(字符串)。插值仅仅可以在两种位置使用:在 文本 区 (比如 <h1>Hello ${name}!</h1>
) 和 字符串表达式 (比如 <#include "/footer/${company}.html">
)中。
表达式的结果必须是字符串,数字或者日期/时间/日期-时间值, 因为(默认是这样)仅仅这些值可以被插值自动转换为字符串。其它类型的值 (比如布尔值,序列)必须 "手动地" 转换成字符串(后续会有一些建议), 否则就会发生错误,中止模板执行。
一个常犯的错误是在不能使用插值的地方使用了它。 插值 仅仅 在 文本 区 (比如 <h1>Hello ${name}!</h1>
) 和 字符串 (比如 <#include "/footer/${company}.html">
)中起作用。 典型的 错误 使用是 <#if ${big}>...</#if>
,这是语法上的错误。 只要简单写为 <#if big>...</#if>
即可。 而且 <#if "${big}">...</#if>
也是 错误的,因为这样参数就是字符串类型了, 但是 if
指令的参数要求是布尔值,所以就会发生运行时错误。
4.2 字符串插入指南:不要忘了转义!
如果插值在 文本 区 (也就是说,不在 字符串表达式 中),如果 escape
指令 起作用了,那么将被插入的字符串会被自动转义。如果要生成HTML, 那么强烈建议你利用它来阻止跨站脚本攻击和非格式良好的HTML页面。这里有一个示例:
<#escape x as x?html> ... <p>Title: ${book.title}</p> <p>Description: <#noescape>${book.description}</#noescape></p> <h2>Comments:</h2> <#list comments as comment> <div class="comment"> ${comment} </div> </#list> ... </#escape>
这个示例展示了当生成HTML时,最好将完整的模板放入到 escape
指令中。那么,如果 book.title
包含 &
, 在输出中它就会被替换成 &
, 而页面还会保持为格式良好的HTML。如果用户注释包含如 <iframe>
(或其它元素)的标记,那么就会被转义成 <iframe>
的样子,使他们没有任何有害点。 但有时在数据模型中真的需要HTML,我们假设上面的 book.description
在数据库中的存储是HTML格式的, 那么此时不得不使用 noescape
来抵消 escape
的转义,不包含 escape
, 模板就会像这样了:
... <p>Title: ${book.title?html}</p> <p>Description: ${book.description}</p> <h2>Comments:</h2> <#list comments as comment> <div class="comment"> ${comment?html} </div> </#list> ...
这和之前示例的效果是一样的, 但是这里可能会忘记 ?html
等内建函数,那么这就会有安全上的问题了。 在之前的示例中,你可能忘记 noescape
等内建函数, 也会造成不良的输出,但是起码是没有安全隐患的。
4.3 数字插入指南
如果表达式是数字类型,那么根据数字的默认格式, 数值将会转换成字符串。这也许会包含最大的小数, 数字分组和相似处理的问题。通常程序员应该设置默认的数字格式; 而模板设计者不需要处理它(但是可以使用 number_format
来设置;详情请参考 setting
指令部分的文档)。 可以使用内建函数 string
为一个插值来重写默认数值格式。
小数的分隔符通常(其他类似的符号也是这样,如分组符号) 是根据所在地的标准(语言,国家)来确定的,这也需要程序员来设置。例如这个模板:
${1.5}
如果当前本地化设置为英语时,将会输出:
1.5
而当前地区为德国时,将会输出:
1,5
这是因为德国人使用逗号作为小数点。
可以看出,插值的打印都是给用户看的(至少是这样的), 而不是给''计算机''的。有时候这样并不好,比如要打印数据库记录的主键, 用来作为URL中的一部分或HTML表单的隐藏域来作为提交内容, 或者要打印CSS/JavaScript中的数字,因为这些值都是给计算机程序去识别的而不是给用户看的。 很多程序对数字格式的要求非常严格,它们只能理解一部分简单的美式数字格式。那样的话, 可以使用内建函数c
(代表''计算机'')来解决这个问题,比如:
<a href="/shop/productdetails?id=${product.id?c}">Details...</a>
4.4 日期/时间插入指南
如果表达式的值是时间日期类型,那么日期中的数字将会按照默认格式来转换成文本。 通常程序员应该设置默认格式,而页面设计者无需处理这一点。(如果需要的话, 可以参考 date_format
, time_format
和 datetime_format
的 setting
指令设置)。当然,也可以使用 内建函数string
来覆盖单独插值的默认格式。
为了将日期显示成文本,FreeMarker 必须知道日期中的哪一部分在使用,也就是说, 如果仅仅日期部分(年,月,日)使用或仅仅时间部分(时,分,秒,毫秒)使用或两部分都用。 不幸的是,由于Java平台技术的限制,自动探测一些变量是不现实的。 这时可以找程序员对数据模型中可能出问题的变量进行处理。 如果找出时间日期变量的哪部分在使用是不太可能的话,就必须帮助 FreeMarker 使用内建函数 date
, time
和 datetime
来识别 (比如 ${lastUpdated?datetime}
),否则就会出现错误停止执行。
4.5 布尔值插入指南
若要使用插值方式来打印布尔值会引起错误,中止模板的执行。 例如: ${a == 2}
就会引起错误, 它不会打印''true''或其他内容。这是因为没有全局来表示布尔值的好方法 (有时想输出yes/no,但有时是想要enabled/disabled,on/off等等)。
我们可以使用内建函数 ?string
来将布尔值转换为字符串形式。比如输出变量"married"的值(假设它是一个布尔值), 那么可以这么来写: ${married?string("yes", "no")}
。
可以使用设置参数 boolean_format
来为 FreeMarker 配置默认的布尔值格式。那么,直接编写 ${married}
这样的代码就不会有问题了。但在很多应用程序中, 这样的做法是不推荐使用的,因为布尔值在不同的地方就应该呈现出不同的格式, 同时将格式留作默认值也可以认为是疏忽,因为这可能导致错误产生。
当想生成JavaScript或其它计算机语言代码部分时,那么可以考虑使用 ${someBoolean?c}
("c" 代表计算机)来输出布尔值true/false。 (请记住 ?c
也可以用来输出给计算机看的数字。)
4.6 精确的转换规则
对于有兴趣研究的人,表达式的值转换为字符串(仍受限于转义) 精确的规则就是下面这些,以这个顺序进行:
如果这个值是数字,那么它会按照指定的
number_format
设置规则来转换为字符串。所以这些转换通常是对用户进行的,而不是对计算机。如果这个值是日期,时间或时间日期类型的一种,那么它们会按照指定的
date_format
,time_format
或者datetime_format
设置规则来转换为字符串。 如果它不能被探测出来是哪种日期类型(日期或时间或日期时间)时,就会发生错误了。如果值本来就是字符串类型的,不需要转换。
如果 FreeMarker 引擎在传统兼容模式下:
如果值是布尔类型,true值就转换成"true",false值将会转换为空字符串。
如果表达式未被定义(
null
或者变量未定义), 那么就转换为空字符串。否则就会发生错误中止模板执行。
否则就会发生错误中止模板执行。
译自 Email: ddekany at users.sourceforge.net
freeMarker(四)——模板开发指南之模板的更多相关文章
- FreeMarker模板开发指南知识点梳理
freemarker是什么? 有什么用? 怎么用? (问得好,这些都是我想知道的问题) freemarker是什么? FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生 ...
- freeMarker(五)——模板开发指南补充知识
学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net 模板开发指南补充知识 1. 自定义指令 自定义指令可以使用 macro ...
- freeMarker(三)——模板开发指南之数值、类型
学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net 模板开发指南——数值.类型 1.基本内容 1.1 什么是数值? 正如你 ...
- freeMarker(二)——模板开发指南之入门
学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net 模板开发指南-入门 1.模板+数据模型=输出 假设在一个在线商店的应 ...
- FreeMarker:模板开发指南
ylbtech-FreeMarker:模板开发指南 1.返回顶部 1. Section Contents 入门 模板 + 数据模型 = 输出 数据模型一览 模板一览 数值,类型 基本内容 类型 模板 ...
- Blazor 模板化组件开发指南
翻译自 Waqas Anwar 2021年4月15日的文章 <A Developer's Guide To Blazor Templated Components> [1] 在我之前的一篇 ...
- freeMarker(八)——程序开发指南之配置(Configuration)
学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net 1.基本内容 配置(configuration)就是 freemark ...
- freeMarker(六)——程序开发指南入门
学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net 1.创建Configuration实例 首先,你应该创建一个 free ...
- Blazor 组件库开发指南
翻译自 Waqas Anwar 2021年5月21日的文章 <A Developer's Guide To Blazor Component Libraries> [1] Blazor 的 ...
随机推荐
- 4.对urls.py的解释
解释: 路由配置文件(URL分发器),它的本质是URL模式以及要为该URL模式调用的视图函数之间的映射表.就是以这种方式告诉Django对于每个URL的处理类.Django启动的时候回去加载urls. ...
- 打开或者 关闭 php 的错误报告
一般线上的环境,我会 php的报错信息屏蔽掉,php.ini 设置的办法 如下: display_errors = Off error_reporting = E_ALL 在代码中,可以这样~~: e ...
- java 从零开始 第二天
2015年4月28号晚,珠海.晴. Java 的基本数据类型 有整型(integer),浮点型(float),布尔型(boolean),字符型(char) 1.整型(integer) java最基本的 ...
- rtpdataheader.h
#ifndef RTPDATAHEADER_H #define RTPDATAHEADER_H // For 32bit intel machines typedef short int16; typ ...
- zabbix监控入门初步
1.Zabbix是什么? Zabbix是一个基于Web界面的分布式系统监控的企业级开源软件.可以监视各种系统与设备的参数,保障服务器及设备的安全运营. 2.Zabbix的功能和特性 (1)安装与配置简 ...
- memcpy使用
void memcpy(void dest, const void *src, size_t n); 功能编辑 从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置 ...
- 基于CocoaPods的iOS项目模块化实践
什么是CocoaPods? CocoaPods is a dependency manager for Swift and Objective-C Cocoa projects. It has ove ...
- OSI 与 TCP/IP
OSI参考模型 物理层(Physical Layer) --- 数据表示.物理层规定了激活.维持.关闭通信端点之间的机械特性.电气特性. 功能特性以及过程特性.该层为上层协 ...
- python 批量修改预定字符串并将修改后的字符串插入文件指定位置
下面的例子是在文件的指定位置增加指定字符串的例子 修改配置文件: def add_str(pre_str): lines = [] flag = True f = open("z.txt&q ...
- <Perl算法小菜>排序加速--Schwatzian变换及Guttman-Rosler变换
原创博客,转载请联系博主! perl里的数据都是以双精度为单元存储的,也就是相当于C/Cpp中的double型,而正则的解析是由perl内置的正则引擎完成的,那么除了重写一个属于自己的排序方法之外,我 ...