封面来源:由博主个人绘制,如需使用请联系博主。

本文摘录自:FreeMarker 中文官方参考手册

0. 前言

公司的项目需要用到模板引擎 FreeMarker,因此,不会的我也就只有学习。

曾经在学习 SpringBoot 的时候,学习过另外一种模板引擎 Thymeleaf,这俩也有相似之处,但是不同点也挺多。

本文只作为一个备忘录,或者说知识梳理,不作为入门级教程。

入门级教程建议查看:FreeMarker 中文官方参考手册本文大部分内容就是摘录此手册。

1. FreeMarker 入门

1.1 模板 + 数据模型 = 输出

模板引擎,模板引擎,那肯定是需要编写模板的。但是也不能只有一个光模板,还需要数据来填充。

为模板准备的数据整体被称作为 数据模型。 模板作者要关心的是,数据模型是树形结构(就像硬盘上的文件夹和文件),在视觉效果上, 数据模型就像是:

1
2
3
4
5
6
7
8
9
(root)
|
+- user = "Big Joe"
|
+- latestProduct
|
+- url = "products/greenmouse.html"
|
+- name = "green mouse"

总的来说,模板和数据模型是 FreeMarker 来生成输出所必须的,因此就有:模板 + 数据模型 = 输出

1.2 数据模型

哈希

数据模型的基本结构是树状的。 这棵树可以很复杂,并且可以有很大的深度,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(root)
|
+- animals
| |
| +- mouse
| | |
| | +- size = "small"
| | |
| | +- price = 50
| |
| +- elephant
| | |
| | +- size = "large"
| | |
| | +- price = 5000
| |
| +- python
| |
| +- size = "medium"
| |
| +- price = 4999
|
+- message = "It is a test"
|
+- misc
|
+- foo = "Something"

上图中的变量扮演目录的角色(比如 root、animalsmouseelephantpythonmisc)被称为 hashes

存储单值的变量(size, price, messagefoo)称为 scalars(标量)。

序列

还有一种很重要的变量是 sequences。 它们像哈希表那样存储子变量,但是子变量没有名字,它们只是列表中的项。 比如,在下面这个数据模型中, animalsmisc.fruits 就是序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
(root)
|
+- animals
| |
| +- (1st)
| | |
| | +- name = "mouse"
| | |
| | +- size = "small"
| | |
| | +- price = 50
| |
| +- (2nd)
| | |
| | +- name = "elephant"
| | |
| | +- size = "large"
| | |
| | +- price = 5000
| |
| +- (3rd)
| |
| +- name = "python"
| |
| +- size = "medium"
| |
| +- price = 4999
|
+- misc
|
+- fruits
|
+- (1st) = "orange"
|
+- (2nd) = "banana"

要访问序列的子变量,可以使用方括号形式的数字索引下标。 索引下标从 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
2
3
4
5
6
7
<#list misc.fruits>
<ul>
<#items as fruit>
<li>${fruit}
</#items>
</ul>
</#list>

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

当然,指令 listitemssepelse 可以联合起来使用。

include 指令的基本使用

使用 include 指令, 就可以在模板中插入其他文件的内容。

比方说,我们可以将版权信息单独存放在页面文件 copyright_footer.html 中:

1
2
3
4
5
6
<hr>
<i>
Copyright (c) 2020 <a href="https://mofan212.github.io/">Mofan</a>,
<br>
All Rights Reserved.
</i>

当需要用到这个文件时,就可以使用 include 指令来插入:

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<title>Test page</title>
</head>
<body>
<h1>Test page</h1>
<p>Blah blah...
<#include "/copyright_footer.html">
</body>
</html>

当修改了 copyright_footer.html 文件, 那么访问者在所有页面都会看到版权声明的新内容。

补充: 重用代码片段的一个更有力的方式是使用宏。


在页面上也可以多次使用指令,而且指令间也可以很容易地相互嵌套。比如,在 list 指令中嵌套 if 指令:

1
2
3
4
5
<#list animals as animal>
<div<#if animal.protected> class="protected"</#if>>
${animal.name} for ${animal.price} Euros
</div>
</#list>

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
2
3
<#assign h = {"name":"mouse", "price":50}>
<#assign keys = h?keys>
<#list keys as key>${key} = ${h[key]}; </#list>

输出:

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 内建函数。

循环变量内建函数

上文中介绍了循环变量的 indexcounteritem_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
2
3
<#list ['a', 'b', 'c', 'd', 'e', 'f', 'g'] as i>
<tr class="${i?item_cycle('row1', 'row2', 'row3')}">${i}</tr>
</#list>
1
2
3
4
5
6
7
<tr class="row1">a</tr>
<tr class="row2">b</tr>
<tr class="row3">c</tr>
<tr class="row1">d</tr>
<tr class="row2">e</tr>
<tr class="row3">f</tr>
<tr class="row1">g</tr>

内置函数 item_cycle 的使用细节:参数的个数至少是1个,没有上限,参数的类型是任意的,无需只是字符串。

  • item_parity:该内置函数只会返回字符串值 "odd""even",常用于表格中行间的颜色变换:
1
2
3
<#list ['a', 'b', 'c', 'd'] as i>
<tr class="${i?item_parity}Row">${i}</tr>
</#list>
1
2
3
4
<tr class="oddRow">a</tr>
<tr class="evenRow">b</tr>
<tr class="oddRow">c</tr>
<tr class="evenRow">d</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。 如果 animalspython 不存在, 那么模板处理过程将会以“未定义的变量”错误而停止。

为了防止这种情况的发生, 可以如下这样来编写代码 (animals.python.price)!0 来解决。 这种情况就是说当 animalspythonprice 任意一个不存在时, 表达式的结果是 0。对于 ?? 也是同样用来的处理这种逻辑的,可以将 animals.python.price?? 对比 (animals.python.price)??来看。

<#if> 标签中处理不存在的变量

假设需要判断 parent 中的 child 中的 str 是否等于 mofan,但 parentchildstr 都可能不存在,因此有这样“恶心”的判断:

1
2
<#if parent?? && parent.child?? && parent.child.str?? && parent.child.str == "mofan">
</#if>

或者是套多层 <#if> 标签。那是否有优雅点的写法呢?

1
<#if (parent.child.str)!"" == "mofan"></#if>

parentchildstr 三者中的任意一个不存在时,返回空字符串,空字符与字符串 mofan 比较结果返回 false,这与多次非空判断或多层 <#if> 标签嵌套返回的结果是一样的。

但在实际运行过程中,上述代码并不会按照预期执行,反而抛出异常,提示 <#if> 标签中的表达式 不是布尔值, 于是又进行如下尝试:

1
2
3
<#if ((parent.child.str)!"" == "mofan")></#if>

<#if ((parent.child.str)!"" == "mofan")?boolean></#if>

错误依旧没有得到结果,依旧显示同样的错误。

按照以下方式编写方可正常执行:

1
<#if ((parent.child.str)!"") == "mofan"></#if>

2. 数值与类型

2.1 基本内容

数值的概念很好理解,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(root)
|
+- user = "Big Joe"
|
+- today = Jul 6, 2007
|
+- todayHoliday = false
|
+- lotteryNumbers
| |
| +- (1st) = 20
| |
| +- (2st) = 14
|
+- mouse = "Yerri"
|
+- age = 12
|
+- color = "brown"

变量 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 转义(因为 ${...} 的特性), 而用户自定义指令的输出则不是(因为 <@...> 的特性所致,它的输出假定是标记,因此已经转义过了);
  • 没有返回值;
  • 会进行流程的控制(就像 listif 指令那样),但是不能在函数 / 方法上这么做。

3. 模板总体结构

所谓模板,就是我们编写的 .ftl 文件,模板是由以下部分混合而成的:

  • 文本:照着原样输出

  • 插值:${....}

  • FTL 标签

  • 注释

FTL 标签是区分大小写的。 list 是指令的名称但 List 就不是,同样 ${name}${Name}${NAME} 也是不同的。

请注意非常重要的一点: 插值仅仅可以在文本中使用。

FTL 标签不可以在其他 FTL 标签和插值中使用。比如,下面这样就是错的:

1
<#if <#include 'foo'>='bar'>...</#if>

注释可以放在 FTL 标签和插值中。

和 HTML 标签一样,FTL 标签必须正确地嵌套使用,像下面这样就是错的:

1
2
3
4
5
6
7
8
<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>

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
2
${r"${foo}"}
${r"C:\foo\bar"}

将会输出:

1
2
${foo}
C:\foo\bar

数字

数字的指定很简单,直接写就行了,但是不支持科学计数法也不能省略小数点前的 0。

下面这些写法都是正确的:

1
0.08、-5.013、8、008、11、+11

序列

指定一个文字的序列,使用逗号来分隔其中的每个子变量,然后把整个列表放到方括号中。例如:

1
2
3
<#list ["foo", "bar", "baz"] as x>
${x}
</#list>

值域

值域也是序列,但它们由指定包含的数字范围所创建, 而不需指定序列中每一项。

比如: 0..<m,假设 m 变量的值是5,那么这个序列就包含 [0, 1, 2, 3, 4]。值域的主要作用有:使用 <#list ...> 来迭代一定范围内的数字、序列切分和字符串切分。

值域表达式的通用形式是( startend 可以是任意的结果为数字表达式):

  • start..end:包含结尾的值域。
  • star..<endstart..!end:不包含结尾的值域。1..<1 表示 []
  • start..*length:限定长度的值域,比如 10..*4 就是 [10, 11, 12, 13]10..*-4 就是 [10, 9, 8, 7],而 10..*0 表示 []。当这些值域被用来切分时, 如果切分后的序列或者字符串结尾在指定值域长度之前,则切分不会有问题。
  • start..: 无右边界值域。注意内存溢出。

需要注意的是:

  • 无右边界值域的定义大小是 2147483647, 这是由于技术上的限制(32位),但当列表显示它们的时候,实际的长度是无穷大。
  • 值域并不存储它们包含的数字,那么对于 0..10..100000000 来说,创建速度都是一样的, 并且占用的内存也是一样的。

哈希表

在模板中指定一个哈希表,就可以遍历用逗号分隔开的"键/值"对, 把列表放到花括号内即可。键和值成对出现并以冒号分隔。比如: { "name": "green mouse", "price": 150 }

键和值都是表达式,但是 用来检索的键必须是字符串类型, 而值可以是任意类型。

在遍历哈希表时,可以使用 keysvalues 内建函数。

4.2 检索变量

检索遍历很简单,使用 ${...} 就可以了。说一下内部表达式需要注意的:

变量名只可以包含字母(也可以是非拉丁文), 数字(也可以是非拉丁数字),下划线 (_), 美元符号 ($),at 符号 (@)。 此外,第一个字符不可以是 ASCII 码数字(0-9)。 从 FreeMarker 2.3.22 版本开始,变量名在任何位置也可以包含负号 (-),点(.)和冒号(:), 但这些必须使用前置的反斜杠(\)来转义, 否则它们将被解释成操作符。

比如,读取名为"data-id"的变量, 表达式为 data\-id,因为 data-id 将被解释成 “data minus id”。

请注意,这些转义仅在标识符中起作用,而不是字符串中。

4.3 字符串操作

插值(或链接)

所谓插值,就是 ${...}。但需要注意的是,插值只能在文本区中有效, 比如:

1
2
<h1>Hello ${name}!</h1>
<#include "/footer/${company}.html">

但是,这样就是错误的:

1
2
<#if ${big}>...</#if>
<#if "${big}">...</#if> <#--参数相当于字符串,但是 if 只能接受布尔值-->

这样写才是对的:<#if big>...</#if>

下面这两种写法是等效的:

1
2
<#assign s = "Hello ${user}!">
<#assign s = "Hello " + user + "!">

由于地区差异,很多地区会使用千分位分隔符,那么 "someUrl?id=" + id 就可能会是 "someUrl?id=1 234"1234 之间存在一个空格)。 要预防这种事情的发生,请使用 ?c 内建函数,那么在 "someUrl?id=" + id?c"someUrl?id=${id?c}"中, 就会得到如 "someUrl?id=1234" 的输出, 而不管本地化和格式的设置是什么,当然也可以在填充参数时就将数值转换成字符串。

获取字符

要想获取字符串中的某个字符,和获取序列中某个索引位置的值是一样的,比如:

假设 user 是 “mofan”:

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

将会得到:

1
2
m
n

字符串切分

字符串切分也很简单,但需要 注意 的是:

  • 降序域不允许进行字符串切分。
  • 如果变量的值既是字符串又是序列(变量时多类型值),那么切分将会对序列进行,而不是字符串。当处理 XML 时,这样的值就是普通的了。此时,可以使用 someXMLnode?string[range]
  • 一个遗留的 bug:值域包含结尾时,结尾小于开始索引并且是非负的(就像在 "abc"[1..0] 中), 会返回空字符串而不是错误(因为不符合我们说的第一点)。在实际使用时,不应该这样使用。

字符串切分示例:

1
2
3
4
5
6
<#assign s = "ABCDEF">
${s[2..3]}
${s[2..<4]}
${s[2..*3]}
${s[2..*100]}
${s[2..]}

将会输出:

1
2
3
4
5
CD
CD
CDE
CDEF
CDEF

4.4 序列操作

连接

序列可以像字符串那样使用 + 进行连接,比如:

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

序列切分

序列切分和字符串切分类似,使用 seq[range] 进行切分,range 是一个值域,比如:

1
2
<#assert seq = ["A", "B", "C", "D", "E"]>
<#list seq[1..3] as i> ${i} </#list>

将会输出:

1
BCD 

但是序列切分允许使用降值域,比如:seq[3..1] 将会输出 DCB。

值域中的数字必须是序列可使用的合法索引, 否则模板的处理将会终止并报错。不能出现负数的索引,索引不能越界,但是 seq[100..<100]seq[100..*0] 是合法的,因为这些值域是空的,尽管索引越界了。

限制长度的值域(start..*length)和无右边界值域(start..)适用于切分后序列的长度。它们会切分可用项中尽可能多的部分。

4.5 哈希表操作

连接

像连接字符串那样,也可以使用 + 号的方式来连接哈希表。如果两个哈希表含有键相同的项,那么在 +右侧 的哈希表中的项优先。

4.6 算数运算

算数运算包含基本的四则运算和求模运算,运算符有:

  • 加法: +
  • 减法: -
  • 乘法: *
  • 除法: /
  • 求模 (求余): %

进行算数运算的两个操作数都是结果为数字的表达式。

但也有例外,+ 号可以用来连接字符串,如果 + 号的一端是字符串,+ 号的另外一端是数字,那么数字就会自动转换为字符串类型(使用当前页面语言的适当格式),之后使用 + 号作为字符串连接操作符。

FreeMarker 不能自动将字符串转换为数字,反之可以自动进行。

如果我们只想获取除法计算(或其它运算)的整数部分, 这可以使用内建函数 int 来解决:

1
2
3
4
5
${(x/2)?int}
${1.1?int}
${1.999?int}
${-1.1?int}
${-1.999?int}

将会输出:

1
2
3
4
5
2
1
1
-1
-1

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
2
3
4
5
6
${testString?upper_case}
${testString?html}
${testString?upper_case?html}

${testSequence?size}
${testSequence?join(", ")}

假设 testString 中存储了字符串 ‘‘Tom & Jerry’’,而testSequnce中存储了字符串 “foo”,“bar” 和 “baz”,将会输出:

1
2
3
4
5
6
TOM & JERRY
Tom &amp; Jerry
TOM &amp; JERRY

3
foo, bar, baz

内建函数的左侧可以是任意的表达式,而不仅仅是变量名。

完整的内建函数参考:内建函数参考

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, localglobal。 照这样,它们不能任意被使用。

<#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??
一元前缀运算符 + - !
乘除法,求模运算符 * / %
加减法运算符 + -
数字值域 .. ..< ..! ..*
关系运算符 < > <= >=gtlt,etc.)
相等,不等运算符 == != =
逻辑 “与” 运算符 &&
逻辑 “或” 运算符 `

5. 插值

插值的使用格式是: ${expression},这里的 expression 可以是所有种类的表达式(比如 ${100 + x})。插值只能在文本区和字符串表达式中使用。

表达式的结果必须是字符串,数字或者日期 / 时间 / 日期 - 时间值, 因为(默认是这样)仅仅这些值可以被插值自动转换为字符串。其它类型的值(比如布尔值,序列)必须 “手动地” 转换成字符串(后续会有一些建议), 否则就会发生错误,中止模板执行。

具体插值指南可以参考:插值

6. 自定义指令

6.1 基本内容

宏是有一个变量名的模板片段。可以在模板中使用宏作为自定义指令, 这样就能进行重复性的工作。比如:

1
2
3
<#macro greet>
<font size="+2">Hello Joe!</font>
</#macro>

<#macro greet></#macro> 之间的内容(称为 宏定义体)将会在使用该变量作为指令时执行。可以这样使用:

1
<@greet></@greet>

或者

1
<@greet/>

然后会输出:

1
<font size="+2">Hello Joe!</font>

6.2 参数

在自定义指令时,还可以指定若干个参数,比如:

1
2
3
<#macro greet person>
<font size="+2">Hello ${person}!</font>
</#macro>

可以这样使用:

1
<@greet person="Fred"/> and <@greet person="Batman"/>

最终打印结果是这样的:

1
2
<font size="+2">Hello Fred!</font>
and <font size="+2">Hello Batman!</font>

= 右边不一定是字符串,可以是 FTL 表达式。比如: <@greet person=Fred/> 也意味着使用变量的值 Fred 作为 person 参数, 而不是字符串"Fred"。甚至还可以是复杂表达式。

自定义指令可以有多个参数。在使用时,参数的顺序不重要,因为在使用时我们已经指定了参数名和对应的值。

需要注意的是, 在使用自定义指令时,不能使用未提及的参数,同时也必须给出在宏中定义所有参数的值。

当然,我们在创建自定义指令时,可以给参数指定默认值,比如:

1
2
3
<#macro greet person color="black">
<font size="+2" color="${color}">Hello ${person}!</font>
</#macro>

后续可以这样使用宏:<@greet person="Fred"/>,因为它和 <@greet person="Fred" color="black"/> 是相等的。如果在使用时重新设置 color 的值,那么新值会覆盖默认值。

明白下面这一点是至关重要的:someParam=foosomeParam="${foo}" 是不同的。第一种情况, 是把变量 foo 的值作为参数的值来使用。第二种情况则是使用插值形式的字符串,那么参数值就是字符串了,这个时候,foo 的值呈现为文本, 而不管 foo 是什么类型的。看下面这个例子: someParam=3/4someParam="${3/4}" 是不同的。 如果指令需要 someParam 是一个数字值, 那么就不要用第二种方式。切记不要改变这些。

6.3 嵌套内容

自定义指令可以嵌套内容。 例如,下面这个例子中是创建了一个可以为嵌套的内容画出边框的宏:

1
2
3
4
5
<#macro border>
<table border=4 cellspacing=0 cellpadding=4><tr><td>
<#nested>
</tr></td></table>
</#macro>

<#nested> 指令执行位于开始和结束标记指令之间的模板代码段。 如果这样写:

1
<@border>The bordered text</@border>

那么会输出:

1
2
3
4
<table border=4 cellspacing=0 cellpadding=4><tr><td>
The bordered text
</td></tr>
</table>

<#nested> 指令也可以被多次调用:

1
2
3
4
5
6
7
8
<#macro do_thrice>
<#nested>
<#nested>
<#nested>
</#macro>
<@do_thrice>
Anything.
</@do_thrice>

那么就会输出:

1
2
3
Anything.
Anything.
Anything.

如果不使用 nested 指令, 那么嵌套的内容就不会被执行。比如前面自定义的 great 指令:

1
2
3
<#macro greet>
<font size="+2">Hello Joe!</font>
</#macro>

然后这样使用:

1
2
3
<@greet person="Joe">
Anything.
</@greet>

greet 宏没有使用 nested 指令,FreeMarker 不会把它视为错误,只是输出:

1
<font size="+2">Hello Joe!</font>

嵌套的内容可以是任意有效的 FTL,包含其他的用户自定义指令。

在嵌套的内容中,宏的局部变量是不可见的。比如:

1
2
3
4
5
6
7
<#macro repeat count>
<#local y = "test">
<#list 1..count as x>
${y} ${count}/${x}: <#nested>
</#list>
</#macro>
<@repeat count=3>${y!"?"} ${x!"?"} ${count!"?"}</@repeat>

将会输出:

1
2
3
test 3/1: ? ? ?
test 3/2: ? ? ?
test 3/3: ? ? ?

此外不同的局部变量的设置是为每个宏自己调用的,不会导致混乱。比如有模板:

1
2
<#macro test foo>${foo} (<#nested>) ${foo}</#macro>
<@test foo="A"><@test foo="B"><@test foo="C"/></@test></@test>

将会输出:

1
A (B (C () C) B) A

6.4 宏和循环变量

list这样的预定义指令可以使用循环变量,自定义指令也可以有循环变量。比如:

1
2
3
4
5
6
7
8
<#macro do_thrice>
<#nested 1>
<#nested 2>
<#nested 3>
</#macro>
<@do_thrice ; x> <#-- user-defined directive uses ";" instead of "as" -->
${x} Anything.
</@do_thrice>

将会输出:

1
2
3
1 Anything.
2 Anything.
3 Anything.

语法规则是给确定“循环”的循环变量传递真实值(比如重复嵌套内容)来作为 nested 指令的参数(当然参数可以是任意的表达式)。 循环变量的名称是在自定义指令的开始标记(<@...>)的参数后面通过分号确定的。

一个宏可以使用多个循环变量(注意变量的顺序):

1
2
3
4
5
6
7
8
<#macro repeat count>
<#list 1..count as x>
<#nested x, x/2, x==count>
</#list>
</#macro>
<@repeat count=4 ; c, halfc, last>
${c}. ${halfc}<#if last> Last!</#if>
</@repeat>

将会输出:

1
2
3
4
1. 0.5
2. 1
3. 1.5
4. 2 Last!

在自定义指令的开始标签(分号之后)为循环变量指定不同的数字是没有问题的, 而不能在 nested 指令上使用。如果在分号之后指定的循环变量少, 那么就看不到 nested指令提供的最后的值, 因为没有循环变量来存储这些值,下面这样使用是可以的:

1
2
3
4
5
6
7
8
9
<@repeat count=4 ; c, halfc, last>
${c}. ${halfc}<#if last> Last!</#if>
</@repeat>
<@repeat count=4 ; c, halfc>
${c}. ${halfc}
</@repeat>
<@repeat count=4>
Just repeat it...
</@repeat>

如果在分号后面指定了比 nested 指令还多的变量, 那么最后的循环变量将不会被创建(在嵌套内容中不会被定义)。

7. 在模板中定义变量

我们知道,模板可以使用在数据模型中定义的变量。 在数据模型之外,模板本身也可以定义变量来使用,这些临时变量可以使用 FTL 指令来创建和替换。请注意每一次的模板执行工作都维护它自己的私有变量, 同时来渲染页面。变量的初始值是空,当模板执行工作结束这些变量便被销毁了。

可以访问一个在模板里定义的变量,就像是访问数据模型根 root 上的变量一样。 这个变量比定义在数据模型中的同名参数有更高的优先级,也就是说, 如果恰巧定义了一个名为 foo 的变量,而在数据模型中也有一个名为 foo 的变量, 那么模板中的变量就会将数据模型根上的变量隐藏(不是覆盖!)。 例如,${foo} 将会输出在模板中定义的变量。

在模板中可以定义三种类型的变量:

  • “简单”变量: 它能从模板中的任何位置来访问,或者从使用 include 指令引入的模板访问。可以使用 assign 指令来创建或替换这些变量。因为宏和方法只是变量,那么 macro 指令和 function 指令也可以用来设置变量,就像 assign 那样。
  • 局部变量:它们只能被设置在宏定义体内, 而且只在宏内可见。一个局部变量的生命周期只是宏的调用过程。可以使用 local 指令在宏定义体内创建或替换局部变量。
  • 循环变量:循环变量是由如 list 指令自动创建的,而且它们只在指令的开始和结束标记内有效。宏的参数是局部变量而不是循环变量。
  • 全局变量:这是一个高级话题了, 并且这种变量最好别用。即便它们属于不同的命名空间, 全局变量也被所有模板共享,因为它们是被 import进来的, 不同于 include 进来的。那么它们的可见度就像数据模型那样。 全局变量通过 global指令来定义。

使用 assign 创建和替换变量:

1
2
3
4
<#assign x = 1>  <#-- create variable x -->
${x}
<#assign x = x + 3> <#-- replace variable x -->
${x}

输出为:

1
2
1
4

局部变量也会隐藏(不是覆盖)同名的 “简单” 变量,循环变量也会隐藏(不是覆盖)同名的 “简单” 变量,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<#assign x = "plain">
1. ${x}
<@test/>
6. ${x}
<#list ["loop"] as x>
7. ${x}
<#assign x = "plain2">
8. ${x}
</#list>
9. ${x}

<#macro test>
2. ${x}
<#local x = "local">
3. ${x}
<#list ["loop"] as x>
4. ${x}
</#list>
5. ${x}
</#macro>

将会输出:

1
2
3
4
5
6
7
8
9
1. plain
2. plain
3. local
4. loop
5. local
6. plain
7. loop
8. loop
9. plain2

内部循环变量可以隐藏外部循环变量,比如:

1
2
3
4
5
6
7
8
9
10
11
<#list ["loop 1"] as x>
${x}
<#list ["loop 2"] as x>
${x}
<#list ["loop 3"] as x>
${x}
</#list>
${x}
</#list>
${x}
</#list>

将会输出:

1
2
3
4
5
loop 1
loop 2
loop 3
loop 2
loop 1

请注意,循环变量的设置是通过指令调用时创建的(本例中的 <list ...> 标签)。没有其他的方式去改变循环变量的值(也就是说,不能使用定义指令来改变它的值),但可以使用一个循环变量来暂时隐藏另外一个。

有时会发生一个变量隐藏数据模型中的同名变量, 但是如果想访问数据模型中的变量,此时就可以使用 特殊变量 globals。例如,假设我们在数据模型中有一个名为 user 的变量,值为 Big Joe

1
2
3
<#assign user = "Joe Hider">
${user} <#-- prints: Joe Hider -->
${.globals.user} <#-- prints: Big Joe -->

通过 global 指令设置的变量可以隐藏数据模型中的同名变量。通常,全局变量的设置会有精确的目的,但仍然可以使用如下方式来访问数据模型变量:.data_model.user

一个坑

使用 localassign 指令在模板中定义变量通常有以下两种方式:

1
2
3
4
5
<#local name = value>
<#-- 或者 -->
<#local name>
capture this
</#local>

如果需要在不同的条件下定义不同的值,那么可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
<#local name>
<#if xxx>
value1
<#else>
value2
</#if>
</#local>
<#-- 或者 -->
<#if xxx>
<#local name = value1>
<#else>
<#local name = value2>
</#if>

在一般的场景下,这两种方式并没有区别,但如果后续要使用定义的变量进行逻辑判断,那这两种方式就有些区别了。

假设当满足某些条件时,定义的变量值为 value1,否则是空串,即 value2 是空串。后续使用定义的变量 name 进行 空串 判断,比如:

1
2
3
<#if StringUtils.isNotEmpty(name)></#if>
<#-- 或者 -->
<#if name?? && name?length gt 0></#if>

但最终得到的结果并不是原本期望的。

这是因为 采用第一种方式定义的变量所指向的值的两端存在一些空白字符。

为了避免这种情况,可以采用第二种方式来定义变量(更推荐),或者在使用定义的变量进行逻辑判断前,移除值两端的空白字符。

8. 命名空间

8.1 命名空间和库

当运行 FTL 模板时,可以使用 assignmacro 指令创建的变量的集合(可能是空的)。像这样的变量集合被称为 命名空间。 简单的情况下可以只使用一个命名空间,称之为 主命名空间。因为通常只使用该命名空间, 所以就没有意识到这点。

如果想创建可以重复使用的宏,函数和其他变量的集合,通常用术语来说就是引用

使用多个命名空间是必然的。只要考虑你在一些项目中, 或者想和他人共享使用的时候,你是否有一个很大的宏的集合。但要确保库中没有宏(或其他变量)名和数据模型中变量同名, 而且也不能和模板中引用其他库中的变量同名是不可能的。通常来说,变量因为名称冲突时也会相互冲突。所以要为每个库中的变量使用不同的命名空间。

8.2 创建一个库

假设需要通用的变量 copyrightmail

1
2
3
4
5
<#macro copyright date>
<p>Copyright (C) ${date} Julia Smith. All rights reserved.</p>
</#macro>

<#assign mail = "jsmith@acme.com">

把上面的这些定义存储在文件 lib/my_test.ftl 中(目录是存放模板的位置),假设想在 aWebPage.ftl 中使用这个模板。我们可以使用 <# import> 指令,就像下面这样:

1
2
3
<#import "/lib/my_test.ftl" as my> 
<@my.copyright date="1999-2002"/>
${my.mail}

将会输出:

1
2
 <p>Copyright (C) 1999-2002 Julia Smith. All rights reserved.</p>
jsmith@acme.com

就算在主命名空间中有一个变量,名为 mailcopyright,也不会引起混乱,因为两个模板使用了不同的命名空间。

8.2 在引入的命名空间中编写变量

偶尔想要在一个被包含的命名空间上创建或替换一个变量。 那么可以使用 assign 指令, 如果用到了它的 namespace 变量,例如下面这样:

1
2
3
4
<#import "/lib/my_test.ftl" as my>
${my.mail}
<#assign mail="jsmith@other.com" in my>
${my.mail}

将会输出:

1
2
jsmith@acme.com
jsmith@other.com

8.3 命名空间和数据模型

数据模型中的变量在任何位置都是可见的。例如, 如果在数据模型中有一个名为 user 的变量,那么 lib/my_test.ftl 也能访问它, aWebPage.ftl 当然也能:

1
2
3
4
5
<#macro copyright date>
<p>Copyright (C) ${date} ${user}. All rights reserved.</p>
</#macro>

<#assign mail = "${user}@acme.com">

如果 userFred 的话,下面这个例子:

1
2
3
<#import "/lib/my_test.ftl" as my>
<@my.copyright date="1999-2002"/>
${my.mail}

将会输出:

1
2
  <p>Copyright (C) 1999-2002 Fred. All rights reserved.</p>
Fred@acme.com

不要忘了在模板的命名空间(可以使用 assignmacro 指令来创建的变量)中的变量有着比数据模型中的变量更高的优先级。因此,数据模型的内容不会干涉到由库创建的变量。

8.4 命名空间的生命周期

命名空间由使用 import 指令中所写的路径来识别。如果想多次 import 这个路径,那么只会为第一次 import 引用创建命名空间并执行模板。后面相同路径的 import 只是创建一个哈希表当作访问相同命名空间的“门”。

my_test.ftl 中有:

1
2
3
4
5
<#macro copyright date>
<p>Copyright (C) ${date} Julia Smith. All rights reserved.</p>
</#macro>

<#assign mail = "jsmith@acme.com">

aWebPage.ftl 中:

1
2
3
4
5
6
<#import "/lib/my_test.ftl" as my>
<#import "/lib/my_test.ftl" as foo>
<#import "/lib/my_test.ftl" as bar>
${my.mail}, ${foo.mail}, ${bar.mail}
<#assign mail="jsmith@other.com" in my>
${my.mail}, ${foo.mail}, ${bar.mail}

将会输出:

1
2
jsmith@acme.com, jsmith@acme.com, jsmith@acme.com
jsmith@other.com, jsmith@other.com, jsmith@other.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. 其他

这两个内容不是很重要,可以直接参考文档:

空白处理

替换(方括号)语法