[FreeMarker 2.3.20] Part I 关于模版设计的介绍 ~模板~表达式

来源:互联网 发布:手机淘宝客户端 官方 编辑:程序博客网 时间:2024/05/17 09:34

简介


当你在为插值或者指令提供具体值的时候除了变量外,还可以使用更复杂的表达式。比如,如果 x 的值是8,y 的值为5,那么 (x + y) / 2 就等于5。


在我们深入表达式细节之前,我们来看看一些具体的例子:

  • 为插值提供值时:插值的用法是,${expression},表达式中的expression 就是提供的值,它会被插入到输出的文本文件中。因此,${(5 + 8) / 2}将会把 "6.5" 插入到输出文本中。
  • 为指令提供值时:你在准备阶段已经见过 if 指令的用法了。它的语法是: <#if expression> ... </#if>, expression 的值必须是 boolean 类型的。比如, <#if 2 < 3> 中的表达式 2 < 3 (2小于3) 最终就会得到 true。


快速浏览(传说中的小抄:))


这是一份专门为那些对 FreeMarker 已经比较熟悉或者是有经验的程序员准备的汇总信息:

  • 直接指定的值
    • Strings: "Foo" or 'Foo' or"It's \"quoted\"" orr"C:\raw\string"
    • Number: 123.45
    • Booleans: true, false["foo", "bar", 123.45],1..100
    • Sequences: ["foo", "bar", 123.45], 1..100
    • Hashes: {"name":"green mouse", "price":150}
  • 访问变量
    • 顶级变量:user
    • 从 hash 中访问值:user.name,user["name"]
    • 从 sequence 中访问值:products[5]
    • 特殊变量:.main
  • String 运算
    • 插值 (或者字符串连接):"Hello ${user}!" (or "Free" + "Marker")
    • 访问一个字符:name[0]
  • Sequence 运算
    • 序列连接:users + ["guest"]
    • 序列片段:products[10..19] or products[5..]
  • Hash 运算
    • hash 连接:passwords + {"joe":"secret42"}
  • 数学运算:(x * 1.5 + 10) / 2 - y % 100
  • 比较运算:x == y, x != y, x < y, x > y,x >= y,x <= y, ...etc.
  • 逻辑运算:!registered && (firstVisit || fromEurope)
  • 内置函数:name?upper_case
  • 方法调用:repeat("What", 3)
  • 丢失值处理运算符:
    • 默认值:name!"unknown" or (user.name)!"unknown" or name! or (user.name)!
    • 丢失测试:name?? or (user.name)??
  • 参见本文末的运算符优先级


直接指定的值


Strings


当要直接指定值的时候,只要将相应的值放入引号里边就可以了,比如,"some text", 或者是放在单引号里边的'some text',两者之间的效果相同。如果在这些文本里边还包含了单引号、双引号或者反斜杠 (\) ,那么我们就只能通过在其前边添加反斜杠来处理,这个过程我们称之为转义(escape),那么对应的字符就称之为转义字符。当然,在这些文本里边我们也可以放一些别的转义字符进去,比如换行符等等。

${"It's \"quoted\" andthis is a backslash: \\"}${'It\'s "quoted" andthis is a backslash: \\'}

会产生如下结果:

It's "quoted" andthis is a backslash: \It's "quoted" andthis is a backslash: \

Note

当然,我们是可以直接将例子中的文本写到模板文件中而不需要放入 ${...} 中,这里只是给个样例,展示表达式的用法。


这个表格里边列出了所有支持的转义序列。在字符串中,其它字符想要实现转义是不正确的用法,在模板中的任何此类尝试都会失败。


Escape sequenceMeaning\"Quotation mark (u0022)\'Apostrophe (a.k.a. apostrophe-quote) (u0027)\\Back slash (u005C)\nLine feed (u000A)\rCarriage return (u000D)\tHorizontal tabulation (a.k.a. tab) (u0009)\bBackspace (u0008)\fForm feed (u000C)\lLess-than sign: <\gGreater-than sign: >\aAmpersand: &\xCodeCharacter given with its hexadecimal Unicode code (UCS code)

在 code 后边的 \x 是 1 到 4 位的16进制的数字。比如说下边的例子都是放的版权符号: "\xA9 1999-2001","\x0A9 1999-2001","\x00A9 1999-2001"。在16进制数后边紧跟的数字都会被认为是16进制的数字 (When the character directly after the last hexadecimal digit can be interpreted as hexadecimal digit, 这句话挺难翻译的,感觉是指连续一堆的数字是会被认为是一个16进制数),你必须写好4个数字否则 FreeMarker 会觉得困惑。


要注意字符组合 ${ (and  #{) 有不特别的意思。它通常是被用来插入表达式的值 (典型的情况就是插入变量的值,"Hello ${user}!")。这个会在下文解释。如果你想输出  ${ ,那么你就需要使用下边介绍的原字面字符串 (raw string literals)。


在字面字符串中的一种特别情况就是原字面字符串。在原字面字符串中,反斜杠和 ${ 是没有特殊意义的,仅仅是普通的字符串而已。要想将一个字面字符串表示为原字面字符串,那么在引号、单引号表示的字面字符串前边直接写上字母 r 就可以了。 如下例:

${r"${foo}"}${r"C:\foo\bar"} 

那么输出为:

${foo}C:\foo\bar

Numbers


要给定一个数值我们只需要使用不带引号的数字就可以了。你只能使用小数点 (dot) 来做分隔符,而不能使用任何组分隔符 (grouping separator symbols,一般来说指的是将数三位一分的逗号分割符,但是原文用的是复数,可是别的分隔符就清楚了,有了解的哥们忘给个解答,谢啦。)。你可以使用 - or + 来表示数的正负 (+ 是可以省略的)。科学技术法符号是不支持的 (因此 1E3 是错误的)。此外,我们不能将小数点前的 0 省略掉 (因此,0.5 是错误的)。


合法的数格式如:  0.08, -5.013,8,008,11,+11


注意字面数值如: 08, +8,8.00 and8 实际上是相等的,它们都表示的是数字8。因此${08},${+8},${8.00} and ${8} 输出的值是相同的值。


Booleans


我们写个 true or false 就能指定一个 boolean 类型的值。记住不能有引号。


Sequences


我们在直接指定字面序列时,只要将要列举的值用逗号分隔开,然后放入一个方括号里边即可,如下例:

<#list ["winter", "spring", "summer", "autumn"] as x>${x}</#list>

输出为:

winterspringsummerautumn

在 list 中的每个值都是表达式,所以你可以这么写:[2 + 2, [1, 2, 3, 4], "whatnot"] 。在这个例子中第一个值是4,第二个是一个序列,第三个是一个字符串 "whatnot"。


我们可以用 start..end 来表示一个有范围的数据序列start andend是可以解析成数据范围的表达式。比如,2..5[2, 3, 4, 5]是相同的,但是前者更高效 (占用更少的内存并且更快),而且也注意到这里是没有用方括号的。同样我们也可以定义降序的数值范围, e.g.:5..2. (此外我们还可以将end 忽略掉, 比如5..,这个就表示大于等于5的所有整数。)


Hashes


我们将以逗号分隔开的键/值对用逗号分隔开,然后放入到花括号里边就Ok,键/值队之间使用冒号分隔。这里有个例子:{"name":"green mouse", "price":150} ,也需要注意的是,键值对中两者都是表达式。不过,键一定是字符串。


访问变量


顶级变量


要访问顶级变量我们只需要简单是使用它的变量名就好。比如说,表达式 user 会将 root 里边以"user" 为变量名存储的值取出。所以下边的例子就会将存储在里边的值输出:

${user}

当 FreeMarker 试着求这个表达式的值时,如果没有找到对应的顶级变量,那么这时就会产生错误信息,终止模板的运行 (除非程序员有对FreeMarker进行配置来处理这种情况所采用的措施)。


在上例表达式中,那个变量里边只能存储字母 (包括 non-Latin 字母),数, 下划线, $, @, #。此外,变量名还不能是以数字打头的。


从 hash 中访问值


如果当前表达式所表达的值是一个 hash 类型的,那么我就可以通过在这个变量后边加上一个点以及对应的字变量名称来获取子变量的值。假设我们有下边这个 data-model :

(root) | +- book |   | |   +- title = "Breeding green mouses" |   | |   +- author |       | |       +- name = "Julia Smith" |       | |       +- info = "Biologist, 1923-1985, Canada" | +- test = "title" 

如同之前所讲到的, book表达式返回的是一个 hash 值,所以现在我们可以通过 book.title 表达式来获取title 的值。我们以同样的逻辑考虑,那么就可以通过book.author.name 来获取 作者的名字。


我们其实还有一个访问子变量的可选语法,在表达式中指定好子变量的名称:book["title"] 。在方括号里边是可以放任意的表达式只要它最后的值是一个字符串就可以。在这个 data-model 例子中我们还可以这样来访问标题值:book[test]. 再来举些例子,不过他们的值都是相同的:book.author.name,book["author"].name,book.author.["name"],book["author"]["name"].


当我们使用点语法的时候,其后跟的变量名需要遵守和顶级变量名相同的命名规则 (名称里边只能包含字母,数字,_, $, @, etc..).。不过在我们使用方括号语法的时候就没有这个限制了,方括号里边是任意一个表达式(注意下,为了支持 FreeMarker XML如果自变量名是* 或者是**, 那么我们没有必要使用方括号语法,关于这里不是很了解,还没具体见过例子)


如同访问顶级变量一样,当访问一个不存在的子变量时会产生一个错误并且模板会终止处理 (除非程序员有对FreeMarker进行配置来处理这种情况所采用的措施)。


插值 (or 字符串连接)


如果你想将一个表达式的值插入字符串中,只需要在字面字符串中使用 ${...} (and#{...}) 就可以实现。${...} 的作用就如同前边的文本处一样。如下边例子 (假设变量 user 的值是 "Big Joe"):

${"Hello ${user}!"}${"${user}${user}${user}${user}"} 

那么输出的结果是:


Hello Big Joe!Big JoeBig JoeBig JoeBig Joe

同样我们也可以使用 + 运算符来达到相同的结果。这是个老方法,这样会调用字符串连接。如下:

${"Hello " + user + "!"}${user + user + user + user}

这个和上边使用多个${...} 例子结果是相同的。


Warning!

一个用户常会范的错误就是会在不应该或是不能的地方使用插值。插值只会在文本区域(e.g. <h1>Hello ${name}!</h1>)和字面字符串处能使用(e.g.<#include "/footer/${company}.html">)。一个典型的错误,<#if ${isBig}>Wow!</#if> ,正确的写法应该是这样的,<#if isBig>Wow!</#if>。 此外,<#if "${isBig}">Wow!</#if> 也是错误的,这里的参数最终是一个字符串,而 if 需要的是一个 boolean 值,这会产生一个运行时错误。


访问一个字符


如同我们获取一个序列的子变量一样,通过指定一个字符的下表值我们就能从一个字符串中获取一个单一字符,e.g. user[0]。这个结果会是一个长度为1的字符串。在 FTL 中没有一个单独的字符类型。和序列子变量一样,下标的取值范围在0到字符串长度-1之间,否则会产生错误并且会中断模板的运行。


因为获取序列子变量语法和字符获取语法有冲突,只有在变量不是一个序列的时候才可以使用字符获取语法 (这主要是因为 FTL 支持多类型的值),因为在同样情况下序列拥有更高的优先级(为了达到我们的目的,我们将会使用系统内置方法 string,user?string[0],如果你不是很懂这个没关心,后边的内置方法章节会在后边讨论的)。例子 (假设变量 user 的值是 "Big Joe"):

${user[0]}${user[4]}


输出为 (注意第一个字符的 index 是 0):

BJ

Note

如同获取序列片段一样,我们也可以通过同样的方法来获取每个范围的字符串,e.g ${user[1..4]} and${user[4..]}。不过现在这个方法是不推荐使用的 (deprecated),一个替代方案就是使用内置方法 substring,后续会详细讨论内置方法。


序列运算


序列连接


我们可以用和通过+ 将字符串连接起来一样的方法将序列连接在一起。如下例:


<#list ["Joe", "Fred"] + ["Julia", "Kate"] as user>- ${user}</#list>  

输出结果:

- Joe- Fred- Julia- Kate

要注意的是字符串连接是不能够用在重复连接的地方,比如在一个循环中将相应的值连接到一个序列上。其只能在类似这样的情况下生效:<#list users + admins as person>。尽管连接序列的速度是比较快的,但是这个也是由所连接的字符串长度所决定的,而且连接后的字符串读取速度始终会比原始字符串读取速度要慢。这也就是说由多个字符串连接起来的字符串读起来始终会是相对比较慢的。


序列片段


通过[firstindex..lastindex] 我们可以获取一个序列的片段,这里firstindex andlastindex 是能过产生数字的表达是。比如,如果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"


Note

lastindex的省略是从 FreeMarker 2.3.3生效的。


尝试访问超出序列最后一个子变量或是在第一个子变量之前的子变量都是会引起一个错误并且会终止模板的运行。


Hash 运算


hash连接


我们可以用和通过+ 将字符串连接起来一样的方法将 hash 连接在一起。如果两个 hash 中包含了同样的 key, 那么在 + 右边的 hash 将会有更高的优先级,如下例:

<#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  

和序列的连接一样, hash 的连接是不会使用在重复的连接中,像在循环中将相应的值加入到 hash 中一样是不可行的。


数学运算


这里包含4个基本运算符和一个模运算符,它们是:

  • 加号:+
  • 减号:-
  • 乘号:*
  • 除号:/
  • 模元算符号:%

例子:

${100 - x * x}${x / 2}${12 % 10} 

我们假设 x 的值是5,那么输出就是:

752.52  

两个运算数都是表达式而且它们最终产生的值必须是一个数值。所以下边的例子中当 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,那么输出为:


211-1-1


比较


有时候我们想知道两个数是否相等或者谁更大。


我会用if 指令来展示一些具体例子。 if 指令的使用方法:<#ifexpression>...</#if>, 对于 expression 来说它必须得到的是一个 boolean 值,否则这将会产生一个错误导致模板停止运行。如果表达式的值是true 那么处于开始标签和结尾之间的表达式将会被执行,否则会被略过。


在测试两个值是否相等我们可以使用=(或则向在 Java or 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"。


= or != 两边的表达式最终的结果要是 scalar 值。此外,两边的scalar值得类型还必须是相同的才行(i.e. string 类型的只能和 string 进行比较,数值类型也只能和数值类型进行比较,etc)否则错误将会产生,模板也会终止运行。比如,<#if 1 = "1">将会产生一个错误。要注意的还有, FreeMarker 进行的是精确比较,在字符串比较中是分别大小的,而且对于空格也是比较敏感的:"x" and"x " and"X"是完全不同的值。


对于数值和日期的比较来说<, <=,>= and>也会派上用场的,值是不能将它们用到 string 上,字符串不能比较大小

例子:

<#if x <= 12>  x is less or equivalent with 12</#if>  


不过就>= and >来说是存在一点问题的,FreeMarker 会将字符>解析成 FTL 标签的结束字符。为了防止这种误解的事情发生我们不得不将这俩放入括号中:<#if (x > y)>。或则,我们可以使用&gt; and &lt;来代替存在问题的符号:<#if x &gt; y> (要注意的是在 FTL 标签中并不是支持所有的 entity references,(诸如&...;一类的符号),恰好数值比较符号是个例外)。此外,这里还有一个可选替换方案,我们可以使用lt 来代替<, lte代替<=,gt代替> 以及gte代替 >=。由于历史原因, FTL 标签同样也明白\lt,\lte,\gt and\gte对应的意思,和上边不带左斜杠是同样的意思。


逻辑运算


我们有以下的运算符:

  • 逻辑或:||
  • 逻辑与:&&
  • 逻辑非:!


这些个运算符只对 boolean 类型的值有效,否则一个对应的错误会产生同时会终止模板进程。如下例:

<#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>

内置方法


内置方法,就如同它们的名字一样,它们是一些随时可以使用的内置方法。一个内置方法是变量的另一种版本,也为存在疑问的一些变量提供了信息。访问内置方法的语法与访问 hash 子变量的方法是比较像的,值是我们会用?代替 . 。比如,获取字符串的大写版本:user?upper_case


关于完整的内置方法你可以在后边的查询手册中找到,后续完成后再添上连接。 不过现在我们先介绍几个比较重要的:

  • 用在字符串上的:
    • html:字符串中所有 HTML特殊字符都会被替换成 entity references (e.g.< with&lt;)
    • cap_first:字符串的手字符会被转换成大写的
    • lower_case:整个字符串会被转换成小谢
    • upper_case:整个字符串会被转换成大写
    • trim:去除字符串开头和结尾的空格
  • 用在序列上的:
    • size:返回一个序列中元素的个数
  • 用在数值上的:
    • int:返回一个数值的整数部分(e.g. -1.9?int is -1)

例子如下:

${test?html}${test?upper_case?html} 

我们假设test的值是``Tom & Jerry'',那么输出为:

Tom & JerryTOM & JERRY 

注意test?upper_case?html,因为test?upper_case的结果是一个字符串类型,一次我们可以使用html内置方法。


另外一个例子:

${seasons?size}${seasons[1]?cap_first} <#-- left side can by any expression -->${"horse"?cap_first} 

我们假设seasons存储的序列为:"winter","spring","summer","autumn",那么输出为:

4SpringHorse

方法调用


我们可以使用方法调用操作来调用相应的方法。方法调用操作是一些在括号内由逗号分隔的表达式组成的,这些分隔的值被称为参数。方法调用操作会将这些值传入方法内,相应的方法会返回一些值作为回馈。这个回馈就是整个方法调用表达式的值。


比如,假设程序员有一个能够访问的方法变量叫做repeat。我们将一个字符串传给它作为它的第一个参数,然后将一个数值作为其第二个参数,最后它会返回将第一个字符串参数重复了第二个参数指定遍数的字符串。

${repeat("What", 3)}

输出为:

WhatWhatWhat 

这里repeat起到方法变量的作用(根据我们访问顶级变量的方式),("What", 3)调用了那个方法。


我要强调一点就是方法的调用其实也是一般的表达式,就像其他表达式一样,所以下边这个例子:

${repeat(repeat("x", 2), 3) + repeat("What", 4)?upper_case}  

会有下边的输出:

xxxxxxWHATWHATWHATWHAT


丢失值得处理


Note

这些运算符从 FreeMarker 2.3.7后便存在(替代了default, exists and if_exists内置方法)。


就如我们早先所介绍的当模板处理过程中去访问一个丢失的值时会产生一个错误并终止模板运行。但是有两个运算符可以压制这种错误产生并且处理这种问题。这种情况可以处理顶级变量、hash子变量、序列子变量,此外这些运算符还可以处理方法调用中不返回值的情况(从程序角度来看,它的返回值可能是null或者其类型是void)。


对于那些了解Java的 null值的程序员, FreeMarker 2.3.x 将它们视为丢失值。或者简单的说,模板其实是不知道null是啥玩意儿。比如说:假设我们有个属性名称为maidenName 的bean,不过这个属性的值是null,在这种情况下模板似乎就将其认为是根本就不存在这样一个属性一样(这里我们假设没有配置过FreeMarker,为其使用一些特别的对象包装器什么的)。这个在方法调用的返回值为null时是一样的。


Note

如果你对为啥 FM 对于丢失值如此的苛刻,那就请查看这个FAQ吧,链接后续补上,还没翻译完哈。


默认值运算符


摘要:unsafe_expr! or(unsafe_expr)!orunsafe_expr!default_expr or (unsafe_expr)!default_expr 



这个运算符可以指定在值丢失后所有用的默认值。



看下边例子,假设是不存在mouse变量的:

${mouse!"No mouse."}<#assign mouse="Jerry">${mouse!"No mouse."}  

输出为:

No mouse.Jerry  


默认值可以是任何的表达式,所以不一定是字符串类型的值。比如我们可以写:hits!0 or colors!["red", "green", "blue"]。在表达式的复杂程度上并没有相应的限制,我们可以写成这些样子:cargo.weight!(item.weight * itemCount + 10).


Warning!

如果你有一个组合的表达式在!后边,像1 + x,是一定要使用括号的,比如${x!(1 + y)} or${(x!1) + y)},这里括号的位置就看插值的具体意义了。 在FreeMarker 2.3.x中,将作为默认值运算符的时候,相对于它右边的运算符优先级其自身是非常低的(the precedence of ! is very low at its right side.)。比如,${x!1 + y} 这个看起来虽然像是这样的: ${(x!1) + y} ,而事实上会被 FreeMarker 误解为 ${x!(1 + y)} 。其实这点在2.3.X系列中是一个程序错误,基本是有"bug"的,不过这个会在2.4的版本中被修正,因此不要使用这样的错误方式,否则到2.4你的代码就要再修正了。



0 0