一、数值和类型
1.1类型的概述:
数值中非常重要的一个概念就是类型。 比方说, 变量 user 的类型是字符串,lotteryNumbers 的类型是序列。 数值的类型非常重要,因为它决定了这些数值可以在
哪里使用的最大限度。 比如${user/2}就是错误的,但是${cargo.weight/2}就能计算出结果 20,除法仅对数字值有效,而不能作用于字符串。 仅当 cargo 是一个哈希表
时 cargo.name 可以使用。 也可以用<#list …>仅仅来遍历序列。 <#if …>指令的条件 condition 只能是布尔值等。
数值同时也可以含有多种类型,尽管这样很少使用。 看下面这个数据模型 mouse ,就又是字符串又是哈希表。
如果用上面的数据模型合并到模板中,就该这么来写
它的输出内容为:
Yerri
12
brown
支持的类型有:
标量:
字符串
数字
布尔值
日期
容器:
哈希表
序列
集
子程序:
方法和函数
用户自定义指令
其它
/
很少使用:
节点
标量
标量是最基本,最简单的数值类型,它们可以是:
- 字符串:简单的文本,例如:产品的名称。
如果想在模板中直接给出字符串的值,而不是使用数据模型中的变量,那么将文本写在引号内即可,比如”green mouse”或者’green mouse’。 - 数字:例如:产品的价格。 整数和非整数是不区分的,只有单一的数字类型。比如使用了计算器,计算
3/2
的结果是
1.5
而不是
1
。
如果要在模板中直接给出数字的值,可以这么来写: 150 , -90.05 ,或者 0.001 。 - 布尔值: 布尔值代表了逻辑上的对或错(是或否)。例如:用户到底是否登录了。
典型的应用是使用布尔值作为 if 指令的条件,比如 <#if loggedIn>…</#if>
或者 <#if price==0>…</#if> ,后面这个 price==0 部分的结果就是布尔值。在模板中可以使用保留字 true 和 false 来指定布尔值 - 日期:日期变量可以存储和日期
/
时间相关的数据。 一共有三种变化。
精确到天的日期(通常指的是“日期”),比如 April 4, 2003
每天的时间(不包括日期部分),比如 10:19:18 PM 。时间的存储精确到毫秒。
日期 - 时间(也称作“时间戳”),比如 April 4, 2003 10:19:18 PM 。时间部分的存储精确到毫秒。
要记住, FreeMarker 区别字符串,数字和布尔值,所以字符串”150”和数字 150 是完全不同的两种数值。数字持有的是数字的值,布尔值表达的是逻辑上的对或错。 字符串可以是任意字符的序列。
容器
这些值存在的目的是为了包含其他变量,它们仅仅作为容器。被包含的变量通常是子变量。容器的类型有:
- 哈希表: 每个子变量都可以通过一个唯一的名称来查找,这个名称是不受限制的字符串。 哈希表并不确定其中子变量的顺序,也就是说没有第一个变量,第二个变量这样的说法,变量仅仅是通过名称来访问的。(就像Java 语言中的 HashMap 一样,是实现了 Hash算法的 Map,不记录内部元素的顺序,仅仅通过名称来访问。
- 序列:每个子变量通过一个整数来标识。 第一个子变量的标识符是0,第二个是 1,第三个是2,这样来类推,而且子变量是有顺序的。 这些数字通常被称为是子变量的索引。序列通常比较密集,也就是所有的索引,包括最后一个子变量的,它们和子变量都是相关联的,但不是绝对必要的。子变量的数值类型也并不需要完全一致。
- 集:从模板设计者角度来看,集是有限制的序列。不能获取集的大小,也不能通过索引取出集中的子变量,但是它们仍然可以通过list 指令来遍历。
方法和函数
一个值是方法或函数的时候那么它就可以计算其他值,结果取决于传递给它的参数。
这部分是对程序员来说的:方法/函数是第一类值,就像函数化的编程语言。 也就是说函数/方法也可以是其他函数或方法的参数或者返回值,并可以把它们定义成变量。假设程序员在数据模型中放置了一个方法变量avg,那么它就可以被用来计算数字的平均值。给定3 和 5作为参数,访问 avg 时就能得到结果 4。
下面这个示例会帮助我们理解方法的使用:
可以得到如下的输出:
The average of 3 and 5 is: 4
The average of 6 and 10 and 20 is: 12
The average of the price of a python and an elephant is:
4999.5
那么方法和函数有什么区别呢? 这是模板作者所关心的,它们没有关系,但也不是一点关系都没有。方法是来自于数据模型(它们反射了
Java
对象的方法),而函数是定义在模板内的(使用了函数指令
-
这也是高级主题),但二者可以用同一种方式来使用。
用户自定义指令
用户自定义指令( 换句话说,就是
FreeMarker
的标签)这种类型的值也是一种子程序,一种可以复用的模板代码段。
这里仅仅对用户自定义指令有一个认识即可(如果现在还不能理解可以先忽略它)。假设现在有一个变量,
box
,它的值是用户自定义的指令,用来打印一些特定的
HTML
信息,这个指令定义了一个标题和其中的信息。
函数/方法和用户自定义指令的比较
这部分内容也是对高级用户来说的(如果你还不能理解可以先忽略它)。 如果要使用函数
/
方法或自定义指令去实现一些东西的时候,二者之间的选择是两难的。按经验来说,
如果能够实现,请先用自定义指令而不要用函数
/
方法。 指令的特征如下:
- 输出(返回值)的是标记( HTML,XML等)。主要原因是函数的返回结果可以自动进行XML 转义(这是因为${…}的特性),而用户自定义指令的输出则不是(这是因为<@...>的特性所致, 它的输出假定为是标记,因此就不再转义)。
- 副作用也是很重要的一点,它没有返回值。例如一个指令的目的是往服务器日志中添加一个条目。(事实上你不能得到自定义指令的返回值,但有些反馈的类型是有可能设置非本地变量的)
- 会进行流程的控制(就像 list 或 if指令那样),但是不能在函数/方法上这么做。
在模板中,
FreeMarker
不知道的
Java
对象的方法通常是可以作为方法来使用的,而不用考虑
Java
对象方法本身的特性,因为在这里没有其他的选择。
节点
节点变量代表了树状结构中的一个节点,而且通常是配合
XML
格式来处理的,这是专业而且更高级的主题。
这里我们仅对高级用户进行一个概要说明:节点和存储在其他节点中的序列很相似,通常也被当作为子节点。节点存储它所在的容器节点的引用,也就是父节点。节点的主要作用是拓扑信息。 其它数据必须通过使用多类型的值来存储。 就像一个值可以同时是一个节点和一个数字,这样它存储的数字可以作为如支付额来使用。 除了拓扑信息,节点也可以存储一些元信息(即
metadata
,译者注):如节点名称, 它的类型(字符串),命名空间( 作为字符串)。若一个节点象征
XHTML
文档中的
h1
元素,那么它的名字可以是
”h1”
,类型可以是
”element”
,命名空间可以是
”http://www.w3.org/1999/xhtml”
。
二、模板
2.1概述:
模板( FTL编程)是由如下部分混合而成的:
Text 文本:文本会照着原样来输出。
Interpolation 插值: 这部分的输出会被计算的值来替换。插值由${和}所分隔(或者#{和},这种风格已经不建议再使用了)。
FTL tags 标签: FTL 标签和 HTML标签很相似,但是它们却是给 FreeMarker的指示, 而且不会打印在输出内容中。
Comments 注释: FTL 的注释和 HTML的注释也很相似,但它们是由<#--和-->来分隔的。注释会被FreeMarker 所忽略,更不会在输出内容中显示。
我们来看一个具体的模板,其中的内容已经用颜色来标记了: 文本,插值,FTL标签,
注释,为了看到可见的换行符,这里使用了[BR]。
FTL 是区分大小写的。 list 是指令的名称而List 就不是,类似地${name}和${Name}或者${NAME}它们也是不同的。
FTL
标签
不可以在其他
FTL
标签
和
插值
中使用。下面这样写就是错的:
<#if <#include 'foo'>='bar'>...</#if>
注释
可以放在
FTL
标签
和
插值
中间。比如:
注意:
如果目前您已经自己尝试了上面所有的示例的话,那么你也许会注意一些空格、制表符和换行符从模板输出中都不见了,尽管我们之前已经说了文本是按照原样输出的。 现在不用为此而计较,这是由于
FreeMarker
的“空格剥离”特性在起作用,它当然会自动去除一些多余的空格,制表符和换行符了。这个特性后续也会解释到。
2.2指令
使用 FTL标签来调用 directives 指令,比如调用 list 指令。 在语法上我们使用了两个标签:<#list animals as being>和</#list>。
标签分为两种:
- 开始标签: <#directivename parametes>
- 结束标签:</#directivename>
除了标签以#开头外,其他都和HTML, XML的语法很相似。 如果标签没有嵌套内容(在开 始 标 签 和 结 束 标 签 之 内 的 内 容 ) , 那 么 可 以 只 使 用 开 始 标 签 。 例 如<#ifsomething>...</#if>,但是 FreeMarker 知道<#includesomething> 中include指令没有可嵌套的内容。
parameters的格式由 directivename来决定。
事实上,指令有两种类型:预定义指令和用户自定义指令。对于用户自定义的指令使用@来代替#,比如<@mydirective parameters>...</@mydirective>。更深的 区 别 在 于 如 果 指 令 没 有 嵌 套 内 容 , 那 么 必 须 这 么 使 用 <@mydirective parameters />, 这和 XML 语法很相似(例如<img ... />) .
注意:
通过配置,
FreeMarker
可以在
FTL
标签和
FTL
注释中,使用
[
和
]
来代替
<
和
>
,就像
[#ifuser == "Big Joe"]
...
[/#if]
。
2.3表达式
- 直接指定值
- 检索变量
3.
字符串操作
4. 序列操作
5.
哈希表操作
6.
算数运算
7.
比 较 运 算
8.
逻辑操作
9.
内建函数
10.
方法调用
11.
处理不存在的值
2.4直接确定值
2.4.1字符串
在文本中确定字符串值的方法是看引号和单引号,比如
"some text"
或
'some text'
,这两种形式是相等的。 如果文本本身包含用于字符引用的引号(双引号
”
或单引
号
’
)或反斜杠时,应该在它们的前面再加一个反斜杠,这就是转义。 转义允许你直接在文本中输入任何字符, 也包括反斜杠。 例如:
输出为:
下面的表格是
FreeMarker
支持的所有转义字符。在字符串使用反斜杠的其他所有情况都是错误的,运行这样的模板都会失败。
转义序列 | 含义 |
\ | 引号( u0022) |
\’ | 单引号(又称为撇号)( u0027) |
\\ | 反斜杠( u005C) |
\n | 换行符( u000A) |
\r | 回车( u000D) |
\t | 水平制表符(又称为标签)( 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
的解析就会出现问题。
一种特殊的字符串就是原生字符串。在原生字符串中,反斜杠和 ${ 没有特殊的含义,它们被视为普通的字符。 为了表明字符串是原生字符串,在开始的引号或单引号之前放置字 母 r ,例如
将会打印:
${foo}
C:\foo\bar
2.4.2数字
输入不带引号的数字就可以直接指定一个数字,必须使用点作为小数的分隔符而不能是其他的分组分隔符。 可以使用-或+来表明符号( +是多余的)。 科学记数法暂不支持使用( 1E3 就是错误的),而且也不能在小数点之前不写 0( .5 也是错误的)。
下面的数字都是合法的: 0.08, -5.013, 8, 008, 11, +11。
数值文字 08, +8, 8.00 和 8 是完全相等的,它们都是数字 8。因此${08}, ${+8},${8.00}和${8}打印的都是相同的。
2.4.3序列
指定一个文字的序列,使用逗号来分隔其中的每个子变量,然后把整个列表放到方括号中。例如:
将会打印出:
winter
spring
summer
autumn
列表中的项目是表达式,那么也可以这样做: [2 + 2, [1, 2, 3, 4],"whatnot"],其中第一个子变量是数字 4,第二个子变量是一个序列,第三个子变量是
字符串”whatnot”。也可以用 start..end 定义存储数字范围的序列,这里的 start 和 end 是处理数字值表达式,比如 2..5 和[2, 3, 4, 5]是相同的,但是使用前者会更有效率( 内存占用少而且速度快)。 可以看出前者也没有使用方括号,这样也可以用来定义递减的数字范围,比如 5..2。(此外,还可以省略 end,只需 5..即可, 但这样序列默认包含 5,6,7,8等递增量直到无穷大)
2.5检索变量
2.5.1顶层变量
为了访问顶层的变量,可以简单地使用变量名。例如,用表达式 user 就可以在根上获取以“ user ”为名存储的变量值。然后就可以打印出存储在里面的内容
如果没有顶层变量,那么
FreeMarker
在处理表达式时就会发生错误,进而终止模板的执行(除非程序员事先配置了
FreeMarker
)。
2.5.2从哈希表中检索数据
如果有一个表达式的结果是哈希表,那么我们可以使用点和子变量的名字得到它的值,假设我们有如下的数据模型:
现在,就可以通过 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"]。
当使用点式语法时,顶层变量名的命名也有相同的限制(命名时只能使用字母,数字,下划线,
$
,
@
等),而使用方括号语法形式时命名有没有这样的限制,它可以是任意的表达式。(为了
FreeMarker
支持
XML
,如果变量名是
*
(星号)或者
**
,那么就应该使用方括号语法格式。)
对于顶层变量来说,如果尝试访问一个不存在的变量也会引起错误导致模板解析执行的中断(除非程序员事先配置过
FreeMarker
)。
2.5.3从序列中检索数据
这和从哈希表中检索是相同的,但是你只能使用方括号语法形式来进行,而且方括号内的表达式最终必须是一个数字而不是字符串。 在第一章的数据模型示例中,为了获取第一个动物的名字(记住第一项数字索引是 0 而不是 1)可以这么来写: animals[0].name。
2.6字符串操作
2.6.1插值(或连接)
如果要在字符串中插入表达式的值,可以在字符串的文字中使用${…}( #{…})。${...}的作用和在文本区的是相同的。假设用户是”Big Joe”,看下面的代码:
将会打印如下内容:
Hello Big Joe!
Big JoeBig JoeBig JoeBig Joe
2.6.2获取一个字符
看一个例子(假设 user 是”Big Joe”)
将会打印出(注意第一个字符的索引是
0
):
B J
2.7序列操作
2.7.1连接
序列的连接可以使用+号来进行,例如:
将会打印出:
- Joe
- Fred
- Julia
- Kate
要注意不要在很多重复连接时使用序列连接操作,比如在循环中往序列上追加项目,而这样的使用是可以的:
<#list users + admins as person>
。 尽管序列连接的
很快,而且速度是和被连接序列的大小相独立的,但是最终的结果序列的读取却比原先的两个序列慢那么一点。 通过这种方式进行的许多重复连接最终产生的序列读取的速度会慢。
2.7.2序列切分
使 用 [firstindex..lastindex] 可 以 获 取 序 列 中 的 一 部 分 , 这 里 的firstindex 和lastindex 表达式的结果是数字。如果seq 存储序列"a", "b", "c",
"d", "e", "f",那么表达式 seq[1..4]将会是含有"b", "c", "d", "e"的序列(索引为 1 的项是"b",索引为 4 的项是"e")。
lastindex 可以被省略,那么这样将会读取到序列的末尾。如果 seq 存储序列"a","b", "c", "d", "e", "f",那么 seq[3..]将是含有"d", "e", "f"的序列。
2.8哈希表操作
像连接字符串那样,也可以使用
+
号的方式来连接哈希表。如果两个哈希表含有键相同的项,那么在
+
号右侧的哈希表中的项目优先。例如:
将会打印出:
- Joe is 30
- Fred is 25
- Julia is 18
2.9算数运算
算数运算包含基本的四则运算和求模运算,运算符有:
加法: +
减法: -
乘法: *
除法: /
求模(求余): %
示例如下:
假设
x
是
5
, 就会打印出:
75
2.5
2
要保证两个操作数都是结果为数字的表达式。下面的这个例子在运行时, FreeMarker就会发生错误,因为是字符串”5” 而不是数字 5。
但这种情况也有一个例外,就是+号,它是用来连接字符串的,如果+号的一端是字符串,另外一端是数字,那么数字就会自动转换为字符串类型(使用适当的格式)。示例如下:
将会输出:
35
有时我们只想获取计算结果的整数部分,这可以使用内建函数
int
来解决。
仍然假设
x
的值是
5
,那么将会输出:
2.10比较运算
为了演示具体的例子,我们在这里使用 if 指令。 if 指令的用法是: <#if expression>...</#if>,其中的表达式的值必须是布尔类型,否则将会出错,模板
执行中断。 如果表达式的结果是 true,那么在开始和结束标记内的内容将会被执行,否则就会被跳过。
测试两个值相等使用=(或者采用 Java 和 C 语言中的==,二者是完全等同的。)
测试两个值不等使用!=。例子中假设 user 是”Big Joe”。
<#if ...>
中的表达式
user = "Big Joe"
结果是布尔值
true
, 上面的代码将会输出
”It is Big Joe”
。
=
或
!=
两边的表达式的结果都必须是标量,而且两个标量都必须是相同类型(也就是
说字符串只能和字符串来比较,数字只能和数字来比较等)。否则将会出错,模板执行中断。
例如
<#if 1 = "1">
就会导致错误。 注意
FreeMarker
进行的是精确的比较,所以字符串在比较时要注意大小写和空格:
"x"
,
"x "
和
"X"
是不同的值。
对数字和日期类型的比较,也可以使用
<
,
<=
,
>=
和
>
。不能把它们当作字符串来比较。 比如:
使用
>=
和
>
的时候有一点小问题。
FreeMarker
解释
>
的时候可以把它当作
FTL
标签的结束符。为了避免这种问题,不得不将表达式放到括号内:
<#if (x > y)>
,或者可以在
比较关系处使用
>
和
<
:
<#if x > y>
。( 通常在
FLT
标签中不支持实体引用(比如
&
...
;
这些), 否则就会抛出算数比较异常)。 另外,可以使用
lt
代替
<
,
lte
代替
<=
,
gt
代替
>
,
gte
代替
>=
, 由于历史遗留的原因,
FTL
也支持
\lt
,
\lte
,
\gt
和
\gte
, 使用他们和使用不带反斜杠的效果一样。
2.11逻辑操作
常用的逻辑操作符:
逻辑或:
||
逻辑与:
&&
逻辑非:
!
逻辑操作符仅仅在布尔值之间有效,若用在其他类型将会产生错误导致模板执行中止。
例如:
2.12内建函数
正如其名,内建函数提供始终可用的内置功能。 内建函数以?形式提供变量的不同形式或者其他信息。使用内建函数的语法和访问哈希表子变量的语法很像, 除了使用?号来代替点,其他的都一样。 例如得到字符串的大写形式: user?upper_case。
字符串使用的内建函数:
html: 字符串中所有的特殊 HTML 字符都需要用实体引用来代替(比如<代替<)。
cap_first:字符串的第一个字母变为大写形式
lower_case:字符串的小写形式
upper_case:字符串的大写形式
trim:去掉字符串首尾的空格
序列使用的内建函数:
size:序列中元素的个数
数字使用的内建函数:
int
:
数字的整数部分(比如
-1.9?int
就是
-1
)
示例:
假设字符串
test
存储
”Tom & Jerry”
,那么输出为:
Tom & Jerry
TOM & JERRY
注意
test?upper_case?html
,内嵌函数双重使用,
test?upper_case
的结果是字符串了,但也还可以继续在其后使用
html
内建函数。
另外一个例子:
假设
seasons
存储了序列
"winter"
,
"spring"
,
"summer"
,
"autumn"
,那么上面的输出将会是:
4
Spring
Horse
2.13方法调用
可以使用方法调用操作来使用一个已经定义过的方法。方法调用的语法形式是使用逗号来分割在括号内的表达式而形成的参数列表,这些值就是参数。 方法调用操作将这些值传递给方法,然后返回一个结果,这个结果就是整个方法调用表达式的值。
假设程序员定义了一个可供调用的方法
repeat
。第一个参数字符串类型,第二个参数是数字类型。 方法的返回值是字符串类型,而方法要完成是将第一个参数重复显示,显示
的次数是第二个参数的值。
将会打印出:
WhatWhatWhat
2.14括号
括号可以用来给表达式分组。示例如下:
别忘了方法调用时使用的括号和给表达式分组的括号含义是完全不同的。
警告:
可以看出, 插值的打印都是给用户看的,而不是给计算机的。有时候这样并不好,比如
你要打印数据库记录的主键,用来作为
URL
中的一部分或
HTML
表单的隐藏域来作为提交内
容,或者要打印
CSS/JavaScript
的数字。这些值都是给计算机程序去识别的而不是用户,很
多程序对数字格式的要求非常严格, 它们只能理解一部分简单的美式数字格式。 那样的话,
可以使用内建函数
c
( 代表计算机) 来解决这个问题,比如: