Skip to end of metadata
Go to start of metadata

本章内容是对FreeMarker进行简略的介绍,后续章节中将会详细展开。不过没有关系,只要你阅读了本章节的内容后,你就能够编写简单,但却很有用的FreeMarker模板程序了。

本页面中的内容:

模板 + 数据模型 = 输出

假设你在一个在线商店的应用系统中需要一个HTML页面,和下面这个页面类似:

<html>
   <head>
      <title>Welcome!</title>
   </head>
   <body>
      <h1>Welcome Big Joe!</h1>
      <p>Our latest product:
         <a href="products/greenmouse.html">green mouse</a>!
   </body>
</html>

在这里,比方说用户名(所有的”Big Joe”),应该是登录这个网页的访问者的名字,并且最新产品的数据应该来自于数据库,这样它们才可以随时进行更新。这样的情况下,你不能在HTML页面中直接输入登录用户的用户名,最新产品的URL和名称,你不能使用静态的HTML代码,那样是不能即时改变的。

对于这个问题,FreeMarker的解决方案是使用模板来代替静态的HTML文本。模板文件同样是静态的HTML代码,但是除了这些HTML代码外,代码中还包括了一些FreeMarker指令元素,这些指令就能够做到动态效果。

<html>
   <head>
      <title>Welcome!</title>
   </head>
   <body>
      <h1>Welcome ${user}!</h1>
      <p>Our latest product:
         <a href="${latestProduct.url}">${latestProduct.name}</a>!
   </body>
</html>

这个模板存放在Web服务器上,看上去像是静态的HTML页面。但不管何时,只要有人来访问这个页面,FreeMarker将会介入执行,然后动态转换模板,用最新的数据内容替换模板中${…}的部分(例如:用Big Joe或者其他的访问者的用户名来代替${user}),生成普通的HTML文本并发送结果到访问者的Web浏览器中去显示。所以访问者的Web浏览器会接收到类似于第一个HTML示例的内容(也就是说,显示普通的HTML文本而没有FreeMarker的指令),因为浏览器也不会感知到FreeMarker在服务器端被调用了。模板文件本身(存储在Web服务器端的文件)在这个过程中也不会改变什么,所以这个转换过程发生在一次又一次的访问中。这样就保证了显示的信息总是即时的。

现在,也许你已经注意到,该模板并没有包含关于如何找出当前的访问者是谁,或者是如何去查询数据库中查找最新的产品的指令。它似乎已经知道了这些数据是什么。事实也确实是这样的,在FreeMarker背后(确切的说是在MVC模式的背后)的重要思想就是表现逻辑和业务逻辑相分离。在模板中,只是处理显示相关的问题,也就是视觉设计问题和格式问题。所准备要显示的数据(如用户名等)与FreeMarker无关,这通常是使用Java语言或其他目的语言来编写的程序。所以模板开发人员不需要关心这些数值是如何计算出来的。事实上,在模板保持不变的同时,这些数值的计算方式可以发生根本的变化。而且,除了模板外,页面外观发生的变化可以完全不触碰其他任何东西。当模板开发人员和程序员是不同一个人的时候,分离带来的好处更是显而易见的。

FreeMarker(还有模板开发人员)并不关心数据是如何计算出来的,FreeMarker只是知道真实的数据是什么。模板能用的所有数据被包装成data-model数据模型。数据模型的创建是通过已经存在的程序计算得到的。至于模板开发人员,数据模型像是树形结构(比如硬盘上的文件夹和文件),正如本例中的数据模型,就可以如下形式来描述:

(root)
|
+- user = "Big Joe"
|
+- latestProduct
	|
	+- url = "products/greenmouse.html"
	|
	+- name = "green mouse"

(为了避免误解:数据模型并不是文本文件,上面所描述的只是一种数据模型的表现形式。它来自于Java对象,但这会成为Java程序员要面对的问题。)

比较之前你在模板中看到的${user}和${latestProduct.name}。作为一种比喻:数据模型就像计算机文件系统上的内容:根root和latestProduct对应目录(文件夹),user,url和name对应文件。url和name在latestProduct目录中,所以latestProduct.name就像是说latestProduct目录的name一样。但是我所说的,这仅仅是个比喻,这里并没有真实的文件和目录。

概括地讲,模板和数据模型是FreeMarker所需,并用来生成输出内容的(比如之前展示的HTML):模板+数据模型=输出

数据模型一览

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

(root)
|
+- animals
| |
| +- mouse
| | |
| | +- size = "small"
| | |
| | +- price = 50
| |
| +- elephant
| | |
| | +- size = "large"
| | |
| | +- price = 5000
| |
| +- python
| 	|
|	 +- size = "medium"
| 	|
| 	+- price = 4999
|
+- test = "It is a test"
|
+- whatnot
	|
	+- because = "don't know"

上图中变量扮演目录的角色(根root,animal,mouse,elephant,python,whatnot)被称为hash哈希表。哈希表通过可查找的名称(例如:”animal”, ”mouse”, ”price”)来访问存储的其他变量(如子变量)。

如果仅存储单值的变量(size,price,text和because)则它们被称为scalars标量。

如果要在模板中使用子变量,那应该从根root开始指定它的路径,每级之间用点来分隔。要访问price和mouse的话,应该从根开始,先是animals,然后是mouse,最后是price,所以应该这样写:animals.mouse.price。当放置${…}这种特定代码在表达式的前后时,我们就告诉FreeMarker在那个位置上要来输出对应的文本。

sequences序列也是一种非常重要的变量,它们和哈希表变量相似,但是它们不存储所包含变量的名称,而是按顺序存储子变量。这样,就可以使用数字索引来访问这些子变量。在这种数据模型中,animal和whatnot.fruits就是序列:

(root)
|
+- animals
| |
| +- (1st)
| | |
| | +- name = "mouse"
| | |
| | +- size = "small"
| | |
| | +- price = 50
| |
| +- (2nd)
| | |
| | +- name = "elephant"
| | |
| | +- size = "large"
| | |
| | +- price = 5000
| |
| +- (3rd)
| 	|
| 	+- name = "python"
| 	|
| 	+- size = "medium"
| 	|
| 	+- price = 4999
|
+- whatnot
	|
	+- fruits
		|
		+- (1st) = "orange"
		|
		+- (2nd) = "banana"

可以使用数组的方括号方式来访问一个序列的子变量。索引从零开始(从零开始是程序员写代码的传统习惯),那么就意味着序列第一项的索引是0,第二项的索引是1,并以此类推。要得到第一个动物的名称的话,那么就应该这么写代码:animals[0].name。要得到whatnot.fruits(就是”banana”这个字符串)的第二项,那么就应该这么来写:whatnot.fruits[1]。

标量可以分为如下类别:

字符串:这是文本类型,字符的任意序列,比如”m”,“o”,“u”,“s”,“e”这些,而且name-S和size-S也是字符串范畴。

数字:这是数字值类型,比如price-S这些。在FreeMarker中字符串”50”和数字50是两种完全不同的类型。前者只是两个字符的序列(这恰好是我们可以读的一个数字),而后者是一个可以在算数运算中直接被使用的数值。

日期/时间:这是时间日期类型。例如动物被捕捉的日期,或商店开始营业的时间。

布尔值:对应对/错(是/否,开/关等)这样仅仅代表正反的值。比如动物可以有一个受保护(protected,译者注)的子变量,这个变量存储这个动物是否被保护起来。

总结:

数据模型可以被看做是树状结构的。

标量存储单一的值,这种类型的值可以是字符串,数字,日期/时间或者是布尔值。

哈希表是存储变量和与其相关且有唯一标识名称变量的容器。

序列是存储有序变量的容器。存储的变量可以通过数字索引来检索,索引通常从零开始。

模板一览

简介

最简单的模板是普通HTML文件(或者是其他任何文本文件—FreeMarker本身不属于HTML)。当客户端访问页面时,FreeMarker要发送HTML代码至客户端浏览器端显示。如果想要页面动起来,就要在HTML中放置能被FreeMarker所解析的特殊部分。

${…}:FreeMarker将会输出真实的值来替换花括号内的表达式,这样的表达式被称为interpolations插值,可以参考第上面示例的内容。

FTL tags标签(FreeMarker 模板的语言标签):FTL标签和HTML标签有一点相似,但是它们是FreeMarker的指令而且是不会直接输出出来的东西。这些标签的使用一般以符号#开头。(用户自定义的FTL标签使用@符号来代替#,但这是更高级的主题内容了,后面会详细地讨论)

Comments注释:FreeMarker的注释和HTML的注释相似,但是它用来分隔的。任何介于这两个分隔符(包含分隔符本身)之间内容会被FreeMarker忽略,就不会输出出来了。

其他任何不是FTL标签,插值或注释的内容将被视为静态文本,这些东西就不会被FreeMarker所解析,会被按照原样输出出来。

directives指令:就是所指的FTL标签。这些指令在HTML的标签(如<table>和</table>)和HTML元素(如table元素)中的关系是相同的。(如果现在你还不能区分它们,那么把“FTL标签”和“指令”看做是同义词即可。)

指令示例

尽管FreeMarker有很多指令,作为入门,在快速了解过程中我们仅仅来看三个最为常用的指令。

if 指令

使用 if 指令可以有条件地跳过模板的一部分,这和程序语言中if是相似的。假设在第一个示例中,你只想向你的老板Big Joe(而不是其他人)问好,就可以这样做:

<html>
   <head>
      <title>Welcome!</title>
   </head>
   <body>
      <h1>
         Welcome ${user}<#if user == "Big Joe">, our beloved leader</#if>!
      </h1>
      <p>Our latest product:
         <a href="${latestProduct.url}">${latestProduct.name}</a>!
   </body>
</html>

在这里,我们告诉FreeMarker,我们尊敬的领导才是if条件中那唯一的user变量值,当它和”Big Joe”相同时才显示出来。那么,当condition的判断结果为false(布尔值)时,在<#if condition>和</#if>标签之间的内容将会被略过。

我们来详细说说condition的使用:==是来判断在它两侧的值相等的操作符,比较的结果是布尔值,true或者false。在==的左侧,是引用的变量,我们很熟悉这样的语法,它会被变量的值来替代。右侧是指定的字符串,在模板中的字符串必须放在引号内。

当price是0的时候,下面的代码将会打印:”Pythons are free today!”

<#if animals.python.price == 0>
Pythons are free today!
</#if>

和前面的示例相似,字符串被直接指定,但是这里则是数字(0)被直接指定。注意到数字是不用放在引号内的。如果将0放在引号内(”0”),FreeMarker就会将其误判为字符串了。

当price不是0的时候,下面的代码将会打印:”Pythons are not free today!”

<#if animals.python.price != 0>
Pythons are free today!
</#if>

你也许会猜测了,!=就是不等于。

你也可以这样来写代码(使用数据模型来描述哈希表变量):

<#if animals.python.price < animals.elephant.price>
Pythons are cheaper than elephants today.
</#if>

使用<#else>标签可以指定当条件为假时程序执行的内容。例如:

<#if animals.python.price < animals.elephant.price>
Pythons are cheaper than elephants today.
<#else>
Pythons are not cheaper than elephants today.
</#if>

如果蟒蛇的价格比大象的价格低,将会打印”Python are cheaper than elephants today.”,否则就打印”Pythons are not cheaper than elephants today.”

如果变量本身就是布尔值(true或者false),那么可以直接让其作为if的条件condition:

<#if animals.python.protected>
Warning! Pythons are protected animals!
</#if>

list 指令

当需要用列表来遍历集合的内容时,list指令是非常好用的。

例如,如果在模板中用前面示例描述序列的数据模型。

<p>We have these animals:
<table border=1>
   <tr>
      <th>Name
      <th>Price
         <#list animals as being>
   <tr>
      <td>${being.name}
      <td>${being.price} Euros
         </#list>
</table>

那么输出结果将会是这样的:

<p>We have these animals:
<table border=1>
   <tr>
      <th>Name
      <th>Price
   <tr>
      <td>mouse
      <td>50 Euros
   <tr>
      <td>elephant
      <td>5000 Euros
   <tr>
      <td>python
      <td>4999 Euros
</table>

list指令的一般格式为:

<#list sequence as loopVariable>repeatThis</#list>

repeatThis部分将会在给定的sequence遍历时在每项中重复,从第一项开始,一个接着一个。在所有的重复中,loopVariable将持有当前项的值。这个循环变量仅存在于<#list …>和</#list>标签之间。

再看一个示例,遍历示例数据模型fruits。

<p>And BTW we have these fruits:
<ul>
<#list whatnot.fruits as fruit>
<li>
   ${fruit}
   </#list>
   <ul>

whatnot.fruits表达式应该很熟悉了,我们引用了数据模型章节中示例的变量。

include指令

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

假设要在一些页面中显示版权声明的信息。那么可以创建一个文件来单独包含版权声明,之后在需要它的地方插入即可。比方说,我们可以将版权信息单独存放在页面文件copyright_footer.html中。

<hr>
<i>
Copyright (c) 2000 <a href="http://www.acmee.com">Acmee Inc</a>,
<br>
All Rights Reserved.
</i>

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

<html>
   <head>
      <title>Test page</title>
   </head>
   <body>
      <h1>Test page</h1>
      <p>Blah blah...
         <#include "/copyright_footer.html">
   </body>
</html>

输出的内容为:

<html>
   <head>
      <title>Test page</title>
   </head>
   <body>
      <h1>Test page</h1>
      <p>Blah blah...
      <hr>
      <i>
      Copyright (c) 2000 <a href="http://www.acmee.com">Acmee Inc</a>,
      <br>
      All Rights Reserved.
      </i>
   </body>
</html>

联合使用指令

在页面也可以多次使用指令,而且指令间可以相互嵌套,正如在HTML元素中嵌套使用标签一样。下面的代码会遍历动物集合,用大号字体来打印大型动物的名字。

<p>We have these animals:
<table border=1>
   <tr>
      <th>Name
      <th>Price
         <#list animals as being>
   <tr>
      <td>
         <#if being.size == "large"><font size="+1"></#if>
         ${being.name}
         <#if being.size == "large"></font></#if>
      <td>${being.price} Euros
         </#list>
</table>

注意到FreeMarker并不解析FTL标签外的文本,插值和注释,当条件不满足时它也会忽略所有嵌套的font标签。

处理不存在的变量

在实际应用中数据模型经常会有可选的变量(也就是说有时可能不存在实际值)。除了一些典型的人为原因导致失误,FreeMarker不能容忍引用不存在的变量,除非明确地告诉它当变量不存在时如何处理。这里介绍两种典型的处理方法。

这部分对程序员而言:一个不存在的变量和一个是null的变量,对于FreeMarker来说是一样的,所以这里所指的丢失包含这两种情况。

不论在哪里引用变量,都可以指定一个默认值来避免变量丢失这种情况,通过在变量名后面跟着一个!和默认值。就像下面的例子,当user从数据模型中丢失时,模板将会将user的值表示为字符串”Anonymous”。(若user并没有丢失,那么模板就会表现出”Anonymous”不存在一样):

<h1>Welcome ${user!"Anonymous"}!</h1>

当然也可以在变量名后面通过放置??来询问FreeMarker一个变量是否存在。将它和if指令合并,那么如果user变量不存在的话将会忽略整个问候代码段:

<#if user??><h1>Welcome ${user}!</h1></#if>

关于多级访问的变量,比如animals.python.price,书写代码:animals.python.price!0,仅当animals.python存在而仅仅最后一个子变量price可能不存在(这种情况下我们假设价格是0)。

如果animals或者python不存在,那么模板处理过程将会以“未定义的变量”错误而停止。为了防止这种情况的发生,可以这样来书写代码(animals.python.price)!0。这种情况下当animals或python不存在时表达式的结果仍然是0。对于??也是同样用来的处理这种逻辑的:animals.python.price??对比(animals.python.price)??来看。

 

 

  • No labels