FreeMarker 中文说明文档


FreeMarker 手册 用于 FreeMarker 2.3.18 Translated By Nan Lei 南磊 译 Copyright: The Chinese translation of the FreeMarker Manual by Nan Lei is licensed under a Creative Commons Attribution 3.0 Unported License (see http://creativecommons.org/licenses/by/3.0/ ). This licence only applies to the Chinese translation, not to the original (English) FreeMarker Manual. 版权说明: FreeMarker 中文版手册由南磊翻译,本文档基于 Creative Commons Attribution 3.0 Unported 授权许可(参见 http://creativecommons.org/licenses/by/3.0/deed.zh) 本许可仅应用于中文版,不对原版英文手册。 (译者联系方式为:nanlei1987@gmail.com 或 http://weibo.com/nanlei1987) 目录 FreeMarker 手册 .......................................................................................................... 1 用于 FreeMarker 2.3.18 ........................................................................................... 1 目录................................................................................................................................. 2 前言................................................................................................................................. 7 什么是 FreeMarker? ................................................................................................. 7 我们应该阅读什么内容? .......................................................................................... 7 文档规约................................................................................................................... 8 联系我们................................................................................................................... 8 几点说明................................................................................................................... 8 第一部分 模板开发指南 ................................................................................................... 9 第一章 模板开发入门 ............................................................................................... 9 1.1 简介 ............................................................................................................ 9 1.2 模板 + 数据模型 = 输出 ............................................................................. 9 1.3 数据模型一览 ............................................................................................ 10 1.4 模板一览 ................................................................................................... 13 第二章 数值和类型 ................................................................................................. 19 2.1 基本内容 ................................................................................................... 19 2.2 类型 .......................................................................................................... 21 第三章 模板 ........................................................................................................... 25 3.1 总体结构 ................................................................................................... 25 3.2 指令 .......................................................................................................... 26 3.3 表达式 ....................................................................................................... 27 3.4 插值 .......................................................................................................... 42 第四章 其它 ........................................................................................................... 45 4.1 自定义指令 ................................................................................................ 45 4.2 在模板中定义变量 ..................................................................................... 50 4.3 命名空间 ................................................................................................... 53 4.4 空白处理 ................................................................................................... 56 4.5 替换(方括号)语法 ................................................................................... 59 第二部分 程序开发指南 ................................................................................................. 61 第一章 程序开发入门 ............................................................................................. 61 1.1 创建配置实例 ............................................................................................ 61 1.2 创建数据模型 ............................................................................................ 61 1.3 获得模板 ................................................................................................... 62 1.4 合并模板和数据模型 .................................................................................. 63 1.5 将代码放在一起 .......................................................................................... 63 第二章 数据模型 .................................................................................................... 65 2.1 基本内容 ................................................................................................... 65 2.2 标量 .......................................................................................................... 65 2.3 容器 .......................................................................................................... 66 2.4 方法 .......................................................................................................... 67 2.5 指令 .......................................................................................................... 68 2.6 节点变量 ................................................................................................... 74 2.7 对象包装 ................................................................................................... 75 第三章 配置 ........................................................................................................... 79 3.1 基本内容 ................................................................................................... 79 3.2 共享变量 ................................................................................................... 79 3.3 配置信息 ................................................................................................... 80 3.4 模板加载 ................................................................................................... 82 3.5 错误控制 ................................................................................................... 85 第四章 其它 ........................................................................................................... 89 4.1 变量 .......................................................................................................... 89 4.2 字符集问题 ................................................................................................ 89 4.3 多线程 ....................................................................................................... 91 4.4 Bean 的包装 ................................................................................................ 91 4.5 日志 .......................................................................................................... 97 4.6 在 Servlet 中使用 FreeMarker ...................................................................... 98 4.7 为 FreeMarker 配置安全策略......................................................................105 4.8 遗留的 XML 包装实现 ................................................................................106 4.9 和 Ant 一起使用 FreeMarker.......................................................................109 4.10 Jython 包装器 ..........................................................................................110 第三部分 XML 处理指南................................................................................................112 前言.......................................................................................................................112 第一章 揭示 XML 文档 ...........................................................................................113 1.1 节点树 ......................................................................................................113 1.2 将 XML 放到数据模型中 ............................................................................115 第二章 必要的 XML 处理 ........................................................................................117 2.1 通过例子来学习 ........................................................................................117 2.2 形式化描述 ...............................................................................................124 第三章 声明的 XML 处理 ........................................................................................129 3.1 基础内容 ..................................................................................................129 3.2 详细内容 ..................................................................................................131 第四部分 参考文档 .......................................................................................................134 第一章 内建函数参考文档 .....................................................................................134 1.1 处理字符串的内建函数 .............................................................................134 1.2 处理数字的内建函数 .................................................................................147 1.3 处理日期的内建函数 .................................................................................151 1.4 处理布尔值的内建函数 .............................................................................155 1.5 处理序列的内建函数 .................................................................................156 1.6 处理哈希表的内建函数 .............................................................................161 1.7 处理节点(XML)的内建函数....................................................................162 1.8 很少使用的和专家级的内建函数 ...............................................................163 第二章 指令参考文档 ............................................................................................167 2.1 if,else,elseif 指令 ...................................................................................167 2.2 switch,case,default,break 指令..............................................................169 2.3 list,break 指令 .........................................................................................170 2.4 include 指令 ...............................................................................................171 2.5 import 指令 ...............................................................................................174 2.6 noparse 指令 ..............................................................................................175 2.7 compress 指令 ............................................................................................176 2.8 escape,noescape 指令...............................................................................177 2.9 assign 指令 ................................................................................................179 2.10 global 指令 ..............................................................................................181 2.11 local 指令 ................................................................................................182 2.12 setting 指令 .............................................................................................182 2.13 用户自定义指令(<@...>) ......................................................................184 2.14 macro,nested,return 指令 ....................................................................186 2.15 function,return 指令...............................................................................190 2.16 flush 指令 ................................................................................................192 2.17 stop 指令.................................................................................................192 2.18 ftl 指令 ....................................................................................................193 2.19 t,lt,rt 指令 ..........................................................................................194 2.20 nt 指令 ....................................................................................................195 2.21 attempt,recover 指令 .............................................................................196 2.22 visit,recurse,fallback 指令 .....................................................................197 第三章 特殊变量参考文档 .....................................................................................202 第四章 FTL 中的保留名称.......................................................................................204 第五章 废弃的 FTL 结构 .........................................................................................205 5.1 废弃的指令列表 ........................................................................................205 5.2 废弃的内建函数列表 .................................................................................205 5.3 老式的 macro 和 call 指令 ..........................................................................205 5.4 转换指令 ..................................................................................................207 5.5 老式 FTL 语法 ............................................................................................208 5.6 #{…}式的数字插值 ......................................................................................209 第五部分 附录 ..............................................................................................................211 附录 A FAQ .............................................................................................................211 1. JSP 和 FreeMarker 的对比 .............................................................................211 2. Velocity 和 FreeMarker 的对比.......................................................................212 3. 为什么 FreeMarker 对 null-s 和不存在的变量很敏感,如何来处理它? ......212 4. 文档编写了特性 X,但是好像 FreeMarker 并不知道它,或者它的行为和文档描 述的不同,或者一个据称已经修改的 BUG 依然存在。 .....................................213 5. 为什么 FreeMarker 打印奇怪的数字数字格式(比如 1,000,000 或 1 000 000 而不 是 1000000)? ...............................................................................................213 6. 为什么 FreeMarker 会打印不好的小数和/或分组分隔符号(比如 3.14 而不是 3,14) .......................................................................................................................214 7. 为什么当我想用如格式打印布尔值时,FreeMarker 会抛出错误,又如何来修正 呢?................................................................................................................214 8. FreeMarker 标签中的<和>混淆了编辑器或 XML 处理器,应该怎么做? .........214 9. 什么是合法的变量名? ...............................................................................214 10. 如何使用包含空格,或其他特殊字符的变量(宏)名? ............................215 11. 当我试图使用 JSP 客户标签时为什么会得到非法参数异常:形式参数类型不匹 配?................................................................................................................215 12. 如何像 jsp:include 一样的方式引入其它的资源?................................216 13. 如 何 给 普 通 Java 方法/ TemplateMethodModelEx/ TemplateTransformModel/ TemplateDirectiveModel 的 实现传 递普通 java.lang.* / java.util.*对象的参数? ...............................................216 14. 为什么在 myMap[myKey]表达式中不能使用非字符串的键?那现在应该怎么 做?................................................................................................................217 15. 当使用?keys/?values 遍历 Map(哈希表)的内容时,得到了混合真正 map 条目的 java.util.Map 的方法。当然,只是想获取 map 的条目。 ...............218 16. 在 FreeMarker 的模板中如何改变序列(lists)和哈希表(maps)? ...........218 17. 关于 null 在 FreeMarker 模板语言是什么样的? ......................................219 18. 我该怎么在表达式(作为另外一个指令参数)中使用指令(宏)的输出? 220 19. 在输出中为什么用“?”来代替字符 X? ..................................................220 20. 在模板执行完成后,怎么在模板中获取计算过的值? ................................221 21. 我能允许用户上传模板吗?又如何保证安全呢? .......................................221 22. 如何在 Java 语言中实现方法或宏而不是在模板语言中?............................222 23. 为什么 FreeMarker 的日志压制了我的应用程序? ......................................222 24. 在基于 Servlet 的应用程序中,如何在模板执行期间发生错误时,展示一个友 好的错误提示页面,而不是堆栈轨迹? ...........................................................223 25. 我正使用一个可视化的 HTML 割裂模板标记的编辑器。你们可以改变模板语言 的语法来兼容我的编辑器么? .........................................................................223 26. FreeMarker 有多快?真的是 2.X 版本的要比 1.X 版本(经典的 FreeMarker)的慢 吗?................................................................................................................223 27. 我的 Java 类怎么才能获取到关于模板结构的信息(比如所有变量的列表)? .......................................................................................................................224 28. 你会一直提供向后的兼容性吗? ...............................................................224 29. 如果我们把 FreeMarker 和我们的产品一起发行,我们需要发布我们产品的源代 码么? ............................................................................................................225 附录 B 安装 FreeMarker..........................................................................................226 附录 C 构建 FreeMarker..........................................................................................227 附录 D 版本 ...........................................................................................................228 2.3.18 版 .........................................................................................................228 2.3.17 版 .........................................................................................................228 2.3.16 版.........................................................................................................231 2.3.15 版.........................................................................................................231 2.3.14 版.........................................................................................................232 2.3.13 版.........................................................................................................233 2.3.12 版.........................................................................................................233 2.3.11 版.........................................................................................................234 2.3.10 版.........................................................................................................235 2.3.9 版 ..........................................................................................................236 2.3.8 版 ..........................................................................................................237 2.3.7 版 ..........................................................................................................237 2.3.7 RC1 版 .....................................................................................................238 2.3.6 版 ..........................................................................................................239 2.3.5 版 ..........................................................................................................239 2.3.4 版 ..........................................................................................................240 2.3.3 版 ..........................................................................................................241 2.3.2 版 ..........................................................................................................242 2.3.1 版 ..........................................................................................................243 2.3 版 .............................................................................................................245 2.2.8 版 ..........................................................................................................258 2.2.7 版 ..........................................................................................................258 2.2.6 版 ..........................................................................................................258 2.2.5 版 ..........................................................................................................259 2.2.4 版 ..........................................................................................................260 2.2.3 版 ..........................................................................................................260 2.2.2 版 ..........................................................................................................261 2.2.1 版 ..........................................................................................................261 2.2 版 .............................................................................................................262 2.1.5 版 ..........................................................................................................270 2.1.4 版 ..........................................................................................................270 2.1.3 版 ..........................................................................................................270 2.1.2 版 ..........................................................................................................271 2.1.1 版 ..........................................................................................................271 2.1 版 .............................................................................................................272 2.01 版 ...........................................................................................................276 2.0 版 .............................................................................................................276 2.0 RC3 版........................................................................................................277 2.0 RC2 版........................................................................................................278 2.0 RC1 版........................................................................................................279 附录 E 许可 ...........................................................................................................282 词汇表 ..........................................................................................................................283 前言 什么是 FreeMarker? FreeMarker 是一款模板引擎:一种基于模板的、用来生成输出文本(任何来自于 HTML 格式的文本用来自动生成源代码)的通用工具。它是为 Java 程序员提供的一个开发包或者说 是类库。它不是面向最终用户,而是为程序员提供的可以嵌入他们开发产品的一款应用程序。 FreeMarker 的设计实际上是被用来生成 HTML 网页,尤其是通过基于实现了 MVC(Model View Controller,模型-视图-控制器)模式的 Servlet 应用程序。使用 MVC 模式的动态网页的构 思使得你可以将前端设计者(编写 HTML)从程序员中分离出来。所有人各司其职,发挥其擅 长的一面。网页设计师可以改写页面的显示效果而不受程序员编译代码的影响,因为应用程 序的逻辑(Java 程序)和页面设计(FreeMarker 模板)已经分开了。页面模板代码不会受到复杂 的程序代码影响。这种分离的思想即便对一个程序员和页面设计师是同一个人的项目来说都 是非常有用的,因为分离使得代码保持简洁而且便于维护。 尽管 FreeMarker 也有编程能力,但它也不是像 PHP 那样的一种全面的编程语言。反而, Java 程序准备的数据来显示(比如 SQL 查询),FreeMarker 仅仅使用模板生成文本页面来呈现 已经准备好的数据。 FreeMarker 不是 Web 应用框架。它是 Web 应用框架中的一个适用的组件,但是 FreeMarker 引擎本身并不知道 HTTP 协议或 Servlet。它仅仅来生成文本。即便这样,它也非 常适用于非 Web 应用环境的开发。要注意的是,我们使用 FreeMarker 作为视图层组件,是 为给如 Struts 这样的 Model 2 框架提供现成的解决方案。 FreeMarker 是免费的,基于 BSD 规则的许可。它是 OSI 认证的开源软件。OSI 认证是开 源倡议的认证标识。 我们应该阅读什么内容? 如果你是一名… 前端设计师,那么你应该阅读模板开发指南,然后如果需要的话可以阅读参考手册来获 取更多技术细节。 程序员,那么你应该先阅读模板开发指南,然后是程序开发指南,最后如果需要的话可 以阅读参考手册来获取更多技术细节。 文档规约 变量名,模板代码段,Java 类名等用如下格式书写,如:foo。 如果需要具体值来代替某些内容,那么用斜体书写,如:Hello yourName! 模板示例如下书写: 数据对象示例如下书写: 输出数据示例如下书写: 程序示例如下书写: 在面向页面设计师和程序员所编写的章节中代码段给程序员这样写:这只是对程序员而 言的。 这样来强调新名词:一些新名词 联系我们 获取最新版本的 FreeMarker,订阅邮件请访问 FreeMarker 主页:http://freemarker.org 如果你需要帮助或者有好的建议,可以使用邮件(邮件文件可以免费搜索)或者 Web 论坛。 如果你想报告一个 Bug,请使用 Web 的 Bug 跟踪系统或者是邮件。查阅这些内容请访问 http://freemarker.org。同时,要注意我们有一个 FAQ 和索引,你可以使用它们。 几点说明 因为英文版文档的作者是匈牙利人,其母语非英语,那么在这种情况的翻译过程,可能 会有错误存在,作者结合自身多年对 FreeMarker 的实践力求精准,但因个人才疏学浅,水 平有限,恳请读者批评指正。 手册的更新根据大家的反馈随时进行,但只在有阶段性成果时公开发布修正版本,并在 FreeMarker 2.4 版本研发完整后,会及时联系原作者获取新特性以便修改。 本翻译是免费的,您可以自由下载和传播,不可用于任何商业行为。但文档版权归译者 所有,原版归 FreeMarker 项目组所有,您可以引用其中的描述,但必须指明出处。如需用 于商业行为,您必须和原作者取得联系。 如果你发现英文原版任何错误(包括语法错误,错别字)或者是在文档中找到一些误导 或混淆错误,也可以是其他的建议,或是咨询 FreeMarker 中的问题,您可以联系原作者。 E-mail:ddekany@freemail.hu 关于本文档的翻译错误(包括语法错误,错别字)或中文技术交流,可以联系译者: nanlei1987@gmail.com 或 http://weibo.com/nanlei1987,我们共同研究,共同进步。 Something Something Something Something 第一部分 模板开发指南 第一章 模板开发入门 1.1 简介 本章是关于 FreeMarker 非常简略的介绍,后续章节中将会详细介绍它们。不过没关系, 只要你阅读了本章节的内容,你就能够编写简单但却很有用的 FreeMarker 模板程序。 1.2 模板 + 数据模型 = 输出 假设你在一个在线商店的应用系统中需要一个 HTML 页面,和下面这个页面相似: 比方说,用户名(所有的”Big Joe”)应该是登录这个网页访问者的名字,最新产品的数 据应该来自于数据库,这样它才可以随时更新。在这样的情况下你不能在 HTML 页面中直接 输入登录的用户名,最新产品的 URL 和名称,你不能使用静态的 HTML 代码,那样是不能即 时改变的。 对于这个问题,FreeMarker 的解决方案是使用模板来代替静态 HTML 文本。模板文件同 样是静态的HTML 代码,但是除了这些HTML 代码外,代码中还包括了一些 FreeMarker 指令, 这些指令就能够做到动态效果。 Welcome!

Welcome Big Joe!

Our latest product: green mouse! Welcome!

Welcome ${user}!

Our latest product: ${latestProduct.name}! 这个模板存放在 Web 服务器上,看上去像是静态的 HTML 页面。但是不管何时,只要 有人访问这个页面时,FreeMarker 将会介入执行,然后动态转换模板,用最新的数据内容替 换${…}中的部分(例如:用 Big Joe 或者其他的访问者的用户名来代替${user}),生成普通的 HTML 文本并发送结果到访问者的 Web 浏览器中去显示。所以访问者的 Web 浏览器会接收 到类似于第一个 HTML 示例的内容(也就是说,显示普通的 HTML 文本而没有 FreeMarker 的指令),浏览器也不会感知到 FreeMarker 在服务器端被调用了。模板文件本身(存储在 Web 服务器端的文件)在这个过程中也不会改变什么,所以这个转换过程发生在一次又一 次的访问中。这样就保证了显示的信息总是即时的。 现在,你也许已经注意到,该模板并没有包含关于如何找出当前的访问者是谁,或者是 如何去查询数据库查找最新的产品的指令。它似乎已经知道了这些数据。确实是这样, FreeMarker 背后(确切的说是 MVC 模式的背后)的重要思想就是表现逻辑和业务逻辑相分 离。在模板只是处理显示问题,也就是视觉设计问题和格式问题。所准备要显示的数据(如 用户名等)与 FreeMarker 无关,这通常是使用 Java 语言或其他目的语言来编写的。所以模 板开发者不需要关心这些数值是如何计算出来的。事实上,在模板保持不变的同时,这些数 值的计算方式可以完全发生变化。而且,除了模板外,页面外观发生的变化可以完全不触碰 其他任何东西。当模板开发者和程序员是不同一个人的时候,分离带来的好处更是显而易见 的。 FreeMarker(还有模板开发者)并不关心数据是如何计算的,FreeMarker 只是知道真实 的数据是什么。模板能用的所有数据被包装成 data-model 数据模型。数据模型的创建是通 过已经存在的程序计算得到的。至于模板开发者,数据模型像是树状结构(比如硬盘上的文 件夹和文件),正如本例中的数据模型,就可以如下形式来描述: (为了避免误解:数据模型并不是文本文件,上面所描述的只是一种数据模型的表现形式。 它来自于 Java 对象,但这会成为 Java 程序员要面对的问题。) 比较之前你在模板中看到的${user}和${latestProduct.name}。作为一种 比喻:数据模型就像计算机文件系统上的内容:根 root 和 latestProduct 对应目录 (文件夹),user,url 和 name 对应文件。url 和 name 在 latestProduct 目录 中,所以 latestProduct.name 就像是说 latestProduct 目录的 name 一样。 但是我所说的,这仅仅是个比喻,这里并没有真实的文件和目录。 概括地讲,模板和数据模型是 FreeMarker 所需,并用来生成输出内容的(比如之前展 示的 HTML):模板+数据模型=输出 1.3 数据模型一览 正如你看到的,数据模型基本结构是树状的。这棵树可以复杂而且有很大的深度,比如: (root) | +- user = "Big Joe" | +- latestProduct | +- url = "products/greenmouse.html" | +- name = "green mouse" 上图中变量扮演目录的角色(根 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 | | | +- 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" 可以使用数组的方括号方式来访问一个序列的子变量。索引从零开始(从零开始是程序 员写代码的传统习惯),那么就意味着序列第一项的索引是 0,第二项的索引是 1,并以此类 推。要得到第一个动物的名称的话,那么就应该这么写代码:animals[0].name。要 得到 whatnot.fruits(就是”banana”这个字符串)的第二项,那么就应该这么来 写:whatnot.fruits[1]。 标量可以分为如下类别: 字符串:这是文本类型,字符的任意序列,比如”m”,“o”,“u”,“s”,“e”这些,而且 name-S 和 size-S 也是字符串范畴。 (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" 数字:这是数字值类型,比如 price-S 这些。在 FreeMarker 中字符串”50”和数字 50 是两种完全不同的类型。前者只是两个字符的序列(这恰好是我们可以读的一个数字),而 后者是一个可以在算数运算中直接被使用的数值。 日期/时间:这是时间日期类型。例如动物被捕捉的日期,或商店开始营业的时间。 布尔值:对应对/错(是/否,开/关等)这样仅仅代表正反的值。比如动物可以有一个 受保护(protected,译者注)的子变量,这个变量存储这个动物是否被保护起来。 总结: 数据模型可以被看做是树状结构的。 标量存储单一的值,这种类型的值可以是字符串,数字,日期/时间或者是布尔值。 哈希表是存储变量和与其相关且有唯一标识名称变量的容器。 序列是存储有序变量的容器。存储的变量可以通过数字索引来检索,索引通常从零开始。 1.4 模板一览 1.4.1 简介 最简单的模板是普通 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 的标签(如

)和 HTML 元素(如 table 元素)中的关系是相同的。(如果现在你还不能区 分它们,那么把“FTL 标签”和“指令”看做是同义词即可。) 1.4.2 指令示例 尽管 FreeMarker 有很多指令,作为入门,在快速了解过程中我们仅仅来看三个最为常 用的指令。 1.4.2.1 if 指令 使用 if 指令可以有条件地跳过模板的一部分,这和程序语言中 if 是相似的。假设在第 一个示例中,你只想向你的老板 Big Joe(而不是其他人)问好,就可以这样做: 在这里,我们告诉 FreeMarker,我们尊敬的领导才是 if 条件中那唯一的 user 变量值, 当它和”Big Joe”相同时才显示出来。那么,当 condition 的判断结果为 false(布尔 值)时,在<#if condition>和标签之间的内容将会被略过。 我们来详细说说 condition 的使用:==是来判断在它两侧的值相等的操作符,比较的结 果是布尔值,true 或者 false。在==的左侧,是引用的变量,我们很熟悉这样的语法,它会 被变量的值来替代。右侧是指定的字符串,在模板中的字符串必须放在引号内。 当 price 是 0 的时候,下面的代码将会打印:”Pythons are free today!” 和前面的示例相似,字符串被直接指定,但是这里则是数字(0)被直接指定。注意到 数字是不用放在引号内的。如果将 0 放在引号内(”0”), FreeMarker 就会将其误判为字符 串了。 当 price 不是 0 的时候,下面的代码将会打印:”Pythons are not free today!” 你也许会猜测了,!=就是不等于。 你也可以这样来写代码(使用数据模型来描述哈希表变量): Welcome!

Welcome ${user}<#if user == "Big Joe">, our beloved leader!

Our latest product: ${latestProduct.name}! <#if animals.python.price == 0> Pythons are free today! <#if animals.python.price != 0> Pythons are free today! <#if animals.python.price < animals.elephant.price> Pythons are cheaper than elephants today. 使用<#else>标签可以指定当条件为假时程序执行的内容。例如: 如果蟒蛇的价格比大象的价格低,将会打印”Python are cheaper than elephants today.”, 否则就打印”Pythons are not cheaper than elephants today.” 如果变量本身就是布尔值(true 或者 false),那么可以直接让其作为 if 的条件 condition: 1.4.2.2 list 指令 当需要用列表来遍历集合的内容时,list 指令是非常好用的。例如,如果在模板中用前 面示例描述序列的数据模型。 那么输出结果将会是这样的: list 指令的一般格式为: <#list sequence as loopVariable>repeatThis repeatThis 部分将会在给定的 sequence 遍历时在每项中重复,从第一项开始, 一个接着一个。在所有的重复中,loopVariable 将持有当前项的值。这个循环变量仅 存在于<#list …>和标签之间。 再看一个示例,遍历示例数据模型 fruits。 <#if animals.python.price < animals.elephant.price> Pythons are cheaper than elephants today. <#else> Pythons are not cheaper than elephants today. <#if animals.python.protected> Warning! Pythons are protected animals!

We have these animals:

NamePrice <#list animals as being>
${being.name}${being.price} Euros

We have these animals:

NamePrice
mouse50 Euros
elephant5000 Euros
python4999 Euros
whatnot.fruits 表达式应该很熟悉了,我们引用了数据模型章节中示例的变量。 1.4.2.3 include 指令 使用 include 指令,我们可以在当前的模板中插入其他文件的内容。 假设要在一些页面中显示版权声明的信息。那么可以创建一个文件来单独包含版权声明, 之后在 需要它的地 方插 入即 可。比 方说 ,我 们可以将版 权信 息单独存放 在页 面文件 copyright_footer.html 中。 当需要用到这个文件时,可以使用 include 指令来实现插入。 输出的内容为:

And BTW we have these fruits:

    <#list whatnot.fruits as fruit>
  • ${fruit}

      Copyright (c) 2000 Acmee Inc,
      All Rights Reserved.
      Test page

      Test page

      Blah blah... <#include "/copyright_footer.html"> 如果改变了 copyright_footer.html 中的内容,那么访问者就会在所有页面中 看到新的版权声明信息了。 1.4.2.4 联合使用指令 在页面也可以多次使用指令,而且指令间可以相互嵌套,正如在 HTML 元素中嵌套使用 标签一样。下面的代码会遍历动物集合,用大号字体来打印大型动物的名字。 注意到 FreeMarker 并不解析 FTL 标签外的文本,插值和注释,当条件不满足时它也会 忽略所有嵌套的 font 标签。 1.4.2.5 处理不存在的变量 在实际应用中数据模型经常会有可选的变量(也就是说有时可能不存在实际值)。除了 一些典型的人为原因导致失误,FreeMarker 不能容忍引用不存在的变量,除非明确地告诉它 当变量不存在时如何处理。这里介绍两种典型的处理方法。 这部分对程序员而言:一个不存在的变量和一个是 null 的变量,对于 FreeMarker 来 Test page

      Test page

      Blah blah...


      Copyright (c) 2000 Acmee Inc,
      All Rights Reserved.

      We have these animals:

      NamePrice <#list animals as being>
      <#if being.size == "large"> ${being.name} <#if being.size == "large"> ${being.price} Euros
      说是一样的,所以这里所指的丢失包含这两种情况。 不论在哪里引用变量,都可以指定一个默认值来避免变量丢失这种情况,通过在变量名 后面跟着一个!和默认值。就像下面的例子,当 user 从数据模型中丢失时,模板将会将 user 的值表示为字符串”Anonymous”。(若 user 并没有丢失,那么模板就会表现 出”Anonymous”不存在一样): 当然也可以在变量名后面通过放置??来询问 FreeMarker 一个变量是否存在。将它和 if 指令合并,那么如果 user 变量不存在的话将会忽略整个问候代码段: 关于多级访问的变量,比如 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)??来看。

      Welcome ${user!"Anonymous"}!

      <#if user??>

      Welcome ${user}!

      第二章 数值和类型 2.1 基本内容 2.1.1 简介 注意: 这里假设你已经阅读完入门章节的内容了。 理解数值和类型的概念是理解数据模型的关键和基础。然而,数值和类型的概念并不局 限于数据模型,下面你就会看到了。 2.1.2 什么是数值? 这部分对于程序员来说可以直接跳过这,它和程序语言中的数值类型是相似的。 你所知道的来自于每天所使用的数字,比如 16,0.5 等这些用语就是数值的示例,也就 是数字。在计算机语言中,这些用语有着更广泛的含义,比如数值并不一定是数字值,看下 面这个数据模型: (root) | +- user = "Big Joe" | +- today = Jul 6, 2007 | +- todayHoliday = false | +- lotteryNumbers | | | +- (1st) = 20 | | | +- (2st) = 14 | | | +- (3rd) = 42 | | | +- (4th) = 8 | | | +- (5th) = 15 | +- cargo | +- name = "coal" | +- weight = 40 我们说变量 user 的数值是”Big Joe”(字符串),today 的数值是 Jul 6,2007(日期), todayHoilday 的数值是 false(布尔值,是/否,这样的值)。lotteryNumbers 的 数值是包含 20,14,42,8,15 的序列。在这种意义上,lotteryNumbers 是多值的, 它包含多个数值(如其中的第二项是 14),但是 lotteryNumbers 本身还是单值。它像 一个装有很多东西的盒子,整个盒子被看做是独立的。最后有一个数值 cargo,它是一个 哈希表(也可以看做是盒子)。所以数值就是存储在变量中的(在 user,cargo 或 cargo.name 中)东西。但是不需要存储的数值也可以称之为数值,比如这里的数字 100: 当模板被执行时,计算的临时结果也称为数值,比如 20+120(它会打印 120): 这最后一种的解释:两个数 40(货物的重量)和 2 相除的结果是 20,这是一个新计算 出的数值。把它和 100 相加,那么 120 就出来了,接着就打印出来了(${…}),接着模板 继续向下执行直到所有结果都计算出来。 现在你应该能体会到数值这个词的含义了,不仅仅是数字的值。 2.1.3 什么是类型? 数值中非常重 要的一个 概念就是 类型。比 方说,变量 user 的类型是字符串, lotteryNumbers 的类型是序列。数值的类型非常重要,因为它决定了这些数值可以在 哪里使用的最大限度。比如${user/2}就是错误的,但是${cargo.weight/2}就能 计算出结果 20,除法仅对数字值有效,而不能作用于字符串。仅当 cargo 是一个哈希表 时 cargo.name 可以使用。也可以用<#list …>仅仅来遍历序列。<#if …>指令的 条件 condition 只能是布尔值等。 注意: 这里说一点点术语:称 “布尔”或“布尔值”或“布尔类型”都是相同的含义。 数值同时也可以含有多种类型,尽管这样很少使用。看下面这个数据模型 mouse,就 又是字符串又是哈希表。 如果用上面的数据模型合并到模板中,就该这么来写: 它的输出内容为: <#if cargo.weight < 100>Light cargo ${cargo.weight / 2 + 100} (root) | +- mouse = "Yerri" | +- age = 12 | +- color = "brown" ${mouse} <#-- 用 mouse 作为字符串 --> ${mouse.age} <#-- 用 mouse 作为哈希表 --> ${mouse.color} <#-- 用 mouse 作为哈希表 --> 2.1.4 数据模型是哈希表 注意观察每个你已经知道的数据模型:被”(root)”标识的内容就是哈希表类型的数值。 当书写如 user 这样的代码,那就意味着想要把”user”变量存储在哈希表的根上。而如果代 码是:root.user,也没有名为”root”的变量,那么这就没有任何作用。 某些人也许会被这种数据模型的例子所困惑,也就是说,根哈希表包含更多的哈希表或 序列(如 lotteryNumbers 和 cargo)。其他就没有更特殊的了。哈希表包含其他变量,那些变 量包含数值,数值可以是字符串,数字等,当然也可以是哈希表或序列。最初我们解释过, 就像字符串和数字,序列或哈希表也是数值。 2.2 类型 2.2.1 简介 支持的类型有:  标量:  字符串  数字  布尔值  日期  容器:  哈希表  序列  集  子程序:  方法和函数  用户自定义指令  其它/很少使用:  节点 2.2.2 标量 标量是最基本,最简单的数值类型,它们可以是:  字符串:简单的文本,例如:产品的名称。 如果想在模板中直接给出字符串的值,而不是使用数据模型中的变量,那么将文本 写在引号内即可,比如”green mouse”或者’green mouse’。(关于语法 Yerri 12 brown 的更多细节请看后续章节)  数字:例如:产品的价格。整数和非整数是不区分的,只有单一的数字类型。比如 使用了计算器,计算 3/2 的结果是 1.5 而不是 1。 如果要在模板中直接给出数字的值,可以这么来写:150,-90.05,或者 0.001。 (关于语法的更多细节请看后续章节)  布尔值:布尔值代表了逻辑上的对或错(是或否)。例如:用户到底是否登录了。 典型的应用是使用布尔值作为if 指令的条件,比如<#if loggedIn>… 或者<#if price==0>…,后面这个 price==0 部分的结果就是布 尔值。 在模板中可以使用保留字 true 和 false 来指定布尔值。  日期:日期变量可以存储和日期/时间相关的数据。一共有三种变化。  精确到天的日期(通常指的是“日期”),比如 April 4, 2003  每天的时间(不包括日期部分),比如 10:19:18 PM。时间的存储精确到毫秒。  日期-时间(也称作“时间戳”),比如 April 4, 2003 10:19:18 PM。时间部分的 存储精确到毫秒。 不幸的是,受到 Java 平台的限制,FreeMarker 是不能决定日期的部哪分来使用(也就 是说,是日期-时间格式,每天的时间格式等)。这个问题的解决方法是高级主题了,后面的 章节将会讨论到。 在模板中直接定义日期数值是可以的,但这也是高级主题,后面的章节将会讨论到。 要记住,FreeMarker 区别字符串,数字和布尔值,所以字符串”150”和数字 150 是完全 不同的两种数值。数字持有的是数字的值,布尔值表达的是逻辑上的对或错。字符串可以是 任意字符的序列。 2.2.3 容器 这些值存在的目的是为了包含其他变量,它们仅仅作为容器。被包含的变量通常是子变 量。容器的类型有:  哈希表:每个子变量都可以通过一个唯一的名称来查找,这个名称是不受限制的字 符串。哈希表并不确定其中子变量的顺序,也就是说没有第一个变量,第二个变量 这样的说法,变量仅仅是通过名称来访问的。(就像 Java 语言中的 HashMap 一样, 是实现了 Hash 算法的 Map,不记录内部元素的顺序,仅仅通过名称来访问。译者 注)  序列:每个子变量通过一个整数来标识。第一个子变量的标识符是 0,第二个是 1, 第三个是 2,这样来类推,而且子变量是有顺序的。这些数字通常被称为是子变量 的索引。序列通常比较密集,也就是所有的索引,包括最后一个子变量的,它们和 子变量都是相关联的,但不是绝对必要的。子变量的数值类型也并不需要完全一致。  集: 从模板设计者角度来看,集是有限制的序列。不能获取集的大小,也不能通过 索引取出集中的子变量,但是它们仍然可以通过 list 指令来遍历。 要注意一个数值也可有多种类型,对于一个数值可能存在哈希表和序列这两种类型,这 时,该变量就支持索引和名称两种访问方式。不过容器基本是当作哈希表或者序列来使用的, 而不是两者同时使用。 尽管存储在哈希表,序列(集)中的变量可以是任意类型的,这些变量也可以是哈希表, 序列(集)。这样就可以构建任意深度的数据结构。 数据模型本身(最好说成是它的根)也是哈希表。 2.2.4 子程序 2.2.4.1 方法和函数 一个值是方法或函数的时候那么它就可以计算其他值,结果取决于传递给它的参数。 这部分是对程序员来说的:方法/函数是第一类值,就像函数化的编程语言。也就是说 函数/方法也可以是其他函数或方法的参数或者返回值,并可以把它们定义成变量。 假设程序员在数据模型中放置了一个方法变量 avg,那么它就可以被用来计算数字的 平均值。给定 3 和 5 作为参数,访问 avg 时就能得到结果 4。 方法的使用后续章节会有解释,下面这个示例会帮助我们理解方法的使用: 可以得到如下的输出: 那么方法和函数有什么区别呢?这是模板作者所关心的,它们没有关系,但也不是一点 关系都没有。方法是来自于数据模型(它们反射了 Java 对象的方法),而函数是定义在模板 内的(使用了函数指令-这也是高级主题),但二者可以用同一种方式来使用。 2.2.4.2 用户自定义指令 用户自定义指令(换句话说,就是 FreeMarker 的标签)这种类型的值也是一种子程序, 一种可以复用的模板代码段。但这也是高级主题,我们在后续章节中会详细解释。 这部分是对程序员来说的:用户自定义指令(比如宏),也是第一类值,就像函数/方法 一样。 这里仅仅对用户自定义指令有一个认识即可(如果现在还不能理解可以先忽略它)。假 设现在有一个变量,box,它的值是用户自定义的指令,用来打印一些特定的 HTML 信息, 这个指令定义了一个标题和其中的信息。 The average of 3 and 5 is: ${avg(3, 5)} The average of 6 and 10 and 20 is: ${avg(6, 10, 20)} The average of the price of a python and an elephant is: ${avg(animals.python.price, animals.elephant.price)} 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 <@box title="Attention!"> Too much copy-pasting may leads to maintenance headaches. 2.2.4.3 函数/方法和用户自定义指令的比较 这部分内容也是对高级用户来说的(如果你还不能理解可以先忽略它)。 如果要使用函 数/方法或自定义指令去实现一些东西的时候,二者之间的选择是两难的。按经验来说, 如果能够实现,请先用自定义指令而不要用函数/方法。指令的特征如下:  输出(返回值)的是标记(HTML,XML 等)。主要原因是函数的返回结果可以自动 进行 XML 转义(这是因为${…}的特性),而用户自定义指令的输出则不是(这是 因为<@...>的特性所致,它的输出假定为是标记,因此就不再转义)。  副作用也是很重要的一点,它没有返回值。例如一个指令的目的是往服务器日志中 添加一个条目。(事实上你不能得到自定义指令的返回值,但有些反馈的类型是有 可能设置非本地变量的)  会进行流程的控制(就像 list 或 if 指令那样),但是不能在函数/方法上这么做。 在模板中,FreeMarker 不知道的 Java 对象的方法通常是可以作为方法来使用的,而不 用考虑 Java 对象方法本身的特性,因为在这里没有其他的选择。 2.2.5 其它 2.2.5.1 节点 节点变量代表了树状结构中的一个节点,而且通常是配合 XML 格式来处理的,这是专 业而且更高级的主题。 这里我们仅对高级用户进行一个概要说明:节点和存储在其他节点中的序列很相似,通 常也被当作为子节点。节点存储它所在的容器节点的引用,也就是父节点。节点的主要作用 是拓扑信息。其它数据必须通过使用多类型的值来存储。就像一个值可以同时是一个节点和 一个数字,这样它存储的数字可以作为如支付额来使用。除了拓扑信息,节点也可以存储一 些元信息(即 metadata,译者注):如节点名称,它的类型(字符串),命名空间(作为字 符串)。若一个节点象征 XHTML 文档中的 h1 元素,那么它的名字可以是”h1”,类型可以 是”element”,命名空间可以是”http://www.w3.org/1999/xhtml”。但对于 数据模型设计者来说,这些元信息,还有如何来使用它们又有什么意义呢。检索拓扑信息和 元信息的方法将会在后续章节中来说明(这里你可以先不用理解它们)。 第三章 模板 注意: 这里假设你已经阅读完入门章节,数值和类型章节两部分了。 3.1 总体结构 实际上你用程序语言编写的程序就是模板,模板也被称为 FTL(代表 FreeMarker 模板语 言)。这是为编写模板设计的非常简单的编程语言。 模板(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'>... 注释可以放在 FTL 标签和插值中间。比如: [BR] [BR] Welcome![BR] [BR] [BR] <#-- Greet the user with his/her name -->[BR]

      Welcome ${user}!

      [BR]

      We have these animals:[BR]

        [BR] <#list animals as being>[BR]
      • ${being.name} for ${being.price} Euros[BR] [BR]
      [BR] [BR] 注意: 如果目前您已经自己尝试了上面所有的示例的话,那么你也许会注意一些空格、制表符 和换行符从模板输出中都不见了,尽管我们之前已经说了文本是按照原样输出的。现在不用 为此而计较,这是由于 FreeMarker 的“空格剥离”特性在起作用,它当然会自动去除一些 多余的空格,制表符和换行符了。这个特性后续也会解释到。 3.2 指令 使用 FTL 标签来调用 directives 指令,比如调用 list 指令。在语法上我们使用了两个标 签:<#list animals as being>和。 标签分为两种:  开始标签:<#directivename parametes>  结束标签: 除了标签以#开头外,其他都和 HTML,XML 的语法很相似。如果标签没有嵌套内容(在 开始标签和结束标签之内的内容),那么可以只使用开始标签 。 例 如 <#if something>... , 但是 FreeMarker 知道<#include something> 中 include 指令没有可嵌套的内容。 parameters 的格式由 directivename 来决定。 事实上,指令有两种类型:预定义指令和用户自定义指令。对于用户自定义的指令使用 @来代替#,比如<@mydirective parameters>...。更深 的区别在于如果指令没有嵌套内容,那么必须这么使用<@mydirective parameters />,这和 XML 语法很相似(例如).但是用户自定义指令 是后面要讨论的高级主题。 像 HTML 标签一样,FTL 标签必须正确的嵌套使用。下面这段示例代码就是错的,因为 if 指令在 list 指令嵌套内容的内外都有: 注意一下 FreeMarker 仅仅关心 FTL 标签的嵌套而不关心 HTML 标签的嵌套,它只会把 HTML 看做是相同的文本,不会来解释 HTML。 如果你尝试使用一个不存在的指令(比如你输错了指令的名称),FreeMarker 就会拒绝

      Welcome ${user <#-- The name of user -->}!

      [BR]

      We have these animals:[BR]

        [BR] <#list <#-- some comment... --> animals as <#-- again... --> being>[BR] ...
          <#list animals as being>
        • ${being.name} for ${being.price} Euros <#if user == "Big Joe"> (except for you) <#-- WRONG! The "if" has to be closed first. -->
        执行模板,同时抛出错误信息。 FreeMarker 会忽略 FTL 标签中的多余空白标记,所以你也可以这么来写代码: 当然,也不能在<,,就像[#if user == "Big Joe"]...[/#if]。要获取更多信息,请参考:第四章的其它/替换 (方括号)语法部分。 注意: 通过配置,FreeMarker 可以不需要#来理解预定义指令(比如...)。而我们不建议这样来使用。要获取更多信息,请参考:参考文档部 分,废弃的 FTL 结构/老式 FTL 语法。 3.3 表达式 3.3.1 简介 当需要给插值或者指令参数提供值时,可以使用变量或其他复杂的表达式。例如,我们 设 x 为 8,y 为 5,那么(x+y)/2 的值就会被处理成数字类型的值 6.5 在我们展开细节之前,先来看一些具体的例子:  当给插值提供值时:插值的使用方式为${expression},把它放到你想输出文 本的位置上然后给值就可以打印了。即${(5+8)/2}会打印”6.5”出来(如果输出 的语言不是英语,也可能打印出”6,5”)。  当给指令参数提供值时:在入门章节我们已经看到 if 指令的使用了。这个指令的语 法是:<#if expression>...。这里的表达式计算结果必须是布 尔类型的。比如<#if 2 < 3>中的 2 < 3(2 小于 3)是结果为 true 的布尔 表达式。 3.3.2 快速浏览(备忘单) 这里是给已经了解 FreeMarker 的人或有经验的程序员的一个提醒:  直接指定值  字符串:"Foo" 或者 'Foo' 或者 "It's \"quoted\"" 或者 r"C:\raw\string"  数字:123.45 <#list[BR] animals as[BR] being[BR] >[BR] ${being.name} for ${being.price} Euros[BR]  布尔值:true, false  序列:["foo", "bar", 123.45], 1..100  哈希表:{"name":"green mouse", "price":150}  检索变量  顶层变量:user  从哈希表中检索数据:user.name, user[“name”]  从序列中检索:products[5]  特殊变量:.main  字符串操作  插值(或连接): "Hello ${user}!"(或"Free" + "Marker")  获取一个字符:name[0]  序列操作  连接:users + ["guest"]  序列切分:products[10..19] 或 products[5..]  哈希表操作  连接:passwords + {"joe":"secret42"}  算数运算: (x * 1.5 + 10) / 2 - y % 100  比较运算: x == y, x != y, x < y, x > y, x >= y, x <= y, x < y, 等等  逻辑操作:!registered && (firstVisit || fromEurope)  内建函数:name?upper_case  方法调用:repeat("What", 3)  处理不存在的值  默认值:name!"unknown" 或者(user.name)!"unknown" 或者 name! 或者 (user.name)!  检测不存在的值:name?? 或者(user.name)?? 参考:运算符的优先级 3.3.3 直接确定值 通常我们喜欢是使用直接确定的值而不是计算的结果。 3.3.3.1 字符串 在文本中确定字符串值的方法是看引号和单引号,比如"some text"或'some text',这两种形式是相等的。如果文本本身包含用于字符引用的引号(双引号”或单引 号’)或反斜杠时,应该在它们的前面再加一个反斜杠,这就是转义。转义允许你直接在文 本中输入任何字符,也包括反斜杠。例如: ${"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 水平制表符(又称为标签)(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}!")。 这将在后续章节中解释。如果想要打印${,就要使用下面所说 的原生字符串。 一种特殊的字符串就是原生字符串。在原生字符串中,反斜杠和${没有特殊的含义, 它们被视为普通的字符。为了表明字符串是原生字符串,在开始的引号或单引号之前放置字 母 r,例如: 将会打印: It's "quoted" and this is a backslash: \ It's "quoted" and this is a backslash: \ ${r"${foo}"} ${r"C:\foo\bar"} ${foo} C:\foo\bar 3.3.3.2 数字 输入不带引号的数字就可以直接指定一个数字,必须使用点作为小数的分隔符而不能是 其他的分组分隔符。可以使用-或+来表明符号(+是多余的)。科学记数法暂不支持使用 (1E3 就是错误的),而且也不能在小数点之前不写 0(.5 也是错误的)。 下面的数字都是合法的:0.08, -5.013, 8, 008, 11, +11。 数值文字 08, +8, 8.00 和 8 是完全相等的,它们都是数字 8。因此${08}, ${+8}, ${8.00}和${8}打印的都是相同的。 3.3.3.3 布尔值 直接写 true 或 false 就表征一个布尔值了,不需使用引号。 3.3.3.4 序列 指定一个文字的序列,使用逗号来分隔其中的每个子变量,然后把整个列表放到方括号 中。例如: 将会打印出: 列表中的项目是表达式,那么也可以这样做:[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 等递增量直到无穷大) 3.3.3.5 哈希表 在模板中指定一个哈希表,就可以遍历用逗号分隔开的“键/值”对,把列表放到花括 号内。键和值成对出现并以冒号分隔。看这个例子:{"name":"green mouse", "price":150}。注意到名字和值都是表达式,但是用来检索的名字就必须是字符串类 <#list ["winter", "spring", "summer", "autumn"] as x> ${x} winter spring summer autumn 型的。 3.3.4 检索变量 3.3.4.1 顶层变量 为了访问顶层的变量,可以简单地使用变量名。例如,用表达式 user 就可以在根上 获取以“user”为名存储的变量值。然后就可以打印出存储在里面的内容。 如果没有顶层变量,那么 FreeMarker 在处理表达式时就会发生错误,进而终止模板的 执行(除非程序员事先配置了 FreeMarker)。 在这个表达式中变量名可以包含字母(也可以是非拉丁文),数字(也可以是非拉丁数 字),下划线(_),美元符号($),at 符号(@)和哈希表(#),此外要注意变量名命名时是不能以 数字开头的。 3.3.4.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"]。 ${user} (root) | +- book | | | +- title = "Breeding green mouses" | | | +- author | | | +- name = "Julia Smith" | | | +- info = "Biologist, 1923-1985, Canada" | +- test = "title" 当使用点式语法时,顶层变量名的命名也有相同的限制(命名时只能使用字母,数字, 下划线,$,@等),而使用方括号语法形式时命名有没有这样的限制,它可以是任意的表达 式。(为了 FreeMarker 支持 XML,如果变量名是*(星号)或者**,那么就应该使用方括号 语法格式。) 对于顶层变量来说,如果尝试访问一个不存在的变量也会引起错误导致模板解析执行的 中断(除非程序员事先配置过 FreeMarker)。 3.3.4.3 从序列中检索数据 这和从哈希表中检索是相同的,但是你只能使用方括号语法形式来进行,而且方括号内 的表达式最终必须是一个数字而不是字符串。在第一章的数据模型示例中,为了获取第一个 动物的名字(记住第一项数字索引是 0 而不是 1)可以这么来写:animals[0].name。 3.3.4.4 特殊变量 特殊变量是由 FreeMarker 引擎本身定义的,为了使用它们,可以按照如下语法形式来 进行:.variable_name。 通常情况下是不需使用特殊变量,而对专业用户来说可能用到。所有特殊变量的说明可 以参见参考手册。 3.3.5 字符串操作 3.3.5.1 插值(或连接) 如果要在字符串中插入表达式的值,可以在字符串的文字中使用${…}(#{…})。 ${...}的作用和在文本区的是相同的。假设用户是”Big Joe”,看下面的代码: 将会打印如下内容: 另外,也可以使用+号来达到类似的效果,这是比较老的方法,也叫做字符串连接。示 例如下: 这样打印的效果和多次使用${...}是一样的。 警告: 使用者在使用插值时经常犯的一个错误是:在不能使用插值的地方使用了它。插值只能 ${"Hello ${user}!"} ${"${user}${user}${user}${user}"} Hello Big Joe! Big JoeBig JoeBig JoeBig Joe ${"Hello " + user + "!"} ${user + user + user + user} 在 文本区段(

        Hello ${name}!

        ) 和字符串文字(<#include "/footer/${company}.html"> ) 中 使 用 。 一 个 典 型 的 错 误 使 用 是 <#if ${isBig}>Wow! ,这是语法上的错误。 只 能 这 么 来 写 : <#if isBig>Wow!,<#if "${isBig}">Wow!来写也是错误的。因为 if 指令的参数需要的是布尔值,而这里是字符串,那么就会引起运行时的错误。 3.3.5.2 获取一个字符 在给定索引值时可以获取字符串中的一个字符,这和 3.3.4.3 节中从序列检索数据是相 似的,比如 user[0]。这个操作执行的结果是一个长度为 1 的字符串,FTL 并没有独立的 字符类型。和序列中的子变量一样,这个索引也必须是数字,范围是从 0 到字符串的长度, 否则模板的执行将会发生错误并终止。 由于序列的子变量语法和字符的 getter 语法冲突,那么只能在变量不是序列时使用字符 的 getter 语法(因为 FTL 支持多类型值,所以它是可能的),这种情况下使用序列方式就比 较多。(为了变通,可以使用内建函数 string,比如 user?string[0]。不必担心你 不理解这个含义,内建函数将会在后续章节中讨论。) 看一个例子(假设 user 是”Big Joe”) 将会打印出(注意第一个字符的索引是 0): 注意: 可以按 照切 分序列的方式来获取一定范围内的字符,比如${user[1..4]}和 ${user[4..]}。然而现在这种使用方法已经被废弃了,作为它的替代,可以使用内建 函数 substring,内建函数将会在后续章节中讨论。 3.3.6 序列操作 3.3.6.1 连接 序列的连接可以使用+号来进行,例如: 将会打印出: ${user[0]} ${user[4]} B J <#list ["Joe", "Fred"] + ["Julia", "Kate"] as user> - ${user} - Joe - Fred - Julia - Kate 要注意不要在很多重复连接时使用序列连接操作,比如在循环中往序列上追加项目,而 这样的使用是可以的:<#list users + admins as person>。尽管序列连接的 很快,而且速度是和被连接序列的大小相独立的,但是最终的结果序列的读取却比原先的两 个序列慢那么一点。通过这种方式进行的许多重复连接最终产生的序列读取的速度会慢。 3.3.6.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"的序列。 注意: 从 FreeMarker 2.3.3 版本以后 lastindex 才能省略。 如果试图访问一个序列首变量之前的项或末变量之后的项将会引起错误,模板的执行也 会中断。 3.3.7 哈希表操作 3.3.7.1 连接 像连接字符串那样,也可以使用+号的方式来连接哈希表。如果两个哈希表含有键相同 的项,那么在+号右侧的哈希表中的项目优先。例如: 将会打印出: 注意很多项目连接时不要使用哈希表连接,比如在循环时往哈希表中添加新项。这和序 列连接的情况是一致的。 3.3.8 算数运算 算数运算包含基本的四则运算和求模运算,运算符有:  加法:+ <#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  减法:-  乘法:*  除法:/  求模(求余):% 示例如下: 假设 x 是 5,就会打印出: 要保证两个操作数都是结果为数字的表达式。下面的这个例子在运行时,FreeMarker 就会发生错误,因为是字符串”5” 而不是数字 5。 但这种情况也有一个例外,就是+号,它是用来连接字符串的,如果+号的一端是字符 串,另外一端是数字,那么数字就会自动转换为字符串类型(使用适当的格式)。示例如下: 将会输出: 通常来说,FreeMarker 不会自动将字符串转换为数字,反之会自动进行。 有时我们只想获取计算结果的整数部分,这可以使用内建函数 int 来解决。(关于内 建函数后续章节会来解释) 仍然假设 x 的值是 5,那么将会输出: 75 2.5 2 ${3 * "5"} <#-- WRONG! --> ${3 + "5"} 35 ${(x/2)?int} ${1.1?int} ${1.999?int} ${-1.1?int} ${-1.999?int} 2 1 1 -1 -1 ${100 – x*x} ${x/2} ${12%10} 3.3.9 比较运算 有时我们需要知道两个值是否相等,或者哪个数的值更大一点。 为了演示具体的例子,我们在这里使用 if 指令。if 指令 的 用法 是 :<#if expression>...,其中的表达式的值必须是布尔类型,否则将会出错,模板 执行中断。如果表达式的结果是 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,使用他们和使用不带反斜杠的效果一样。 3.3.10 逻辑操作 常用的逻辑操作符:  逻辑或:||  逻辑与:&&  逻辑非:! 逻辑操作符仅仅在布尔值之间有效,若用在其他类型将会产生错误导致模板执行中止。 例如: <#if user = "Big Joe"> It is Big Joe <#if user != "Big Joe"> It is not Big Joe <#if x <= 12> x is less or equivalent with 12 3.3.11 内建函数 正如其名,内建函数提供始终可用的内置功能。内建函数以?形式提供变量的不同形式 或者其他信息。使用内建函数的语法和访问哈希表子变量的语法很像,除了使用?号来代替 点,其他的都一样。例如得到字符串的大写形式:user?upper_case。 在参考文档中可以查到所有内建函数的资料。现在,我们只需了解一些重要的内建函数 就行了。  字符串使用的内建函数:  html: 字符串中所有的特殊 HTML 字符都需要用实体引用来代替(比如<代 替<)。  cap_first:字符串的第一个字母变为大写形式  lower_case:字符串的小写形式  upper_case:字符串的大写形式  trim:去掉字符串首尾的空格  序列使用的内建函数:  size:序列中元素的个数  数字使用的内建函数:  int:数字的整数部分(比如-1.9?int 就是-1) 示例: 假设字符串 test 存储”Tom & Jerry”,那么输出为: 注意 test?upper_case?html,内嵌函数双重使用,test?upper_case 的 结果是字符串了,但也还可以继续在其后使用 html 内建函数。 另外一个例子: 假设 seasons 存储了序列"winter", "spring", "summer", "autumn",那 么上面的输出将会是: <#if x < 12 && color = "green"> We have less than 12 things, and they are green. <#if !hot> <#-- here hot must be a boolean --> It's not hot. ${test?html} ${test?upper_case?html} Tom & Jerry TOM & JERRY ${seasons?size} ${seasons[1]?cap_first} <#-- left side can by any expression --> ${"horse"?cap_first} 3.3.12 方法调用 可以使用方法调用操作来使用一个已经定义过的方法。方法调用的语法形式是使用逗号 来分割在括号内的表达式而形成的参数列表,这些值就是参数。方法调用操作将这些值传递 给方法,然后返回一个结果,这个结果就是整个方法调用表达式的值。 假设程序员定义了一个可供调用的方法 repeat。第一个参数字符串类型,第二个参 数是数字类型。方法的返回值是字符串类型,而方法要完成是将第一个参数重复显示,显示 的次数是第二个参数的值。 将会打印出: 3.3.13 处理不存在的值 要注意这个操作是 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 中的相关内容。 注意: 如果你想知道为什么 FreeMarker 对不存在的变量如此挑剔,请阅读 FAQ 部分。 3.3.13.1 默认值 使用形式概览: unsafe_expr!default_expr 或 unsafe_expr! 或 (unsafe_expr)!default_expr 或(unsafe_expr)! 这个操作符允许你为可能不存在的变量指定一个默认值。 4 Spring Horse ${repeat("What", 3)} WhatWhatWhat 例如,假设下面展示的代码中没有名为 mouse 的变量: 将会输出 默认值可以是任何类型的表达式,也可以不必是字符串。你也可以这么写: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,则注意不能省略它。例如: 输出为: 警告: 因为语法的含糊<@something a=x! b=y />将会解释为<@something a=x!(b=y) />,那就是说 b=y 将会视为是比较运算,然后结果作为 x 的默认值。而不 是想要的参数 b 是 x 的默认值,为了避免这种情况,如下编写代码:<@something a=(x!) b=y />。 在不是顶层变量时,默认值操作符可以有两种使用方式: 如果是这样的写法,那么在 product 中,当 color 不存在时(返回”red”)将会 被处理,但是如果连 produce 都不存在时将不会处理。也就是说这样写时变量 product 必须存在,否则模板就会报错。 这时,如果当不存在时也会被处理,那就是说如果 product 不存在或者 product 存在而 color 不存在,都能显示默认值”red”而不会报错。本例和上例写法的重要区别 在于用括号时,就允许其中表达式的任意部分可以未定义。 ${mouse!"No mouse."} <#assign mouse="Jerry"> ${mouse!"No mouse."} No mouse. Jerry (${mouse!}) <#assign mouse = "Jerry"> (${mouse!}) () (Jerry) product.color!"red" (product.color)!"red" 当然,默认值操作也可以作用于序列,比如: 输出为: 如果序列索引是负数(比如 seq[-1]!'-')也会发生错误,这样默认值和其他操作 也就不起作用了。 3.3.13.2 检测不存在的值 使用形式概览:unsafe_expr??或(unsafe_expr)?? 这个操作符告诉我们一个值是否存在。基于这种情况,结果是 true 或 false。 示例如下,假设并没有名为 mouse 的变量: 输出为: 访问非顶层变量的使用规则和默认值操作符也是一样的,即 product.color??和 (product.color)?? 。 <#assign seq = ['a', 'b']> ${seq[0]!'-'} ${seq[1]!'-'} ${seq[2]!'-'} ${seq[3]!'-'} a b - - <#if mouse??> Mouse found <#else> No mouse found Creating mouse... <#assign mouse = "Jerry"> <#if mouse??> Mouse found <#else> No mouse found No mouse found Creating mouse... Mouse found 3.3.14 括号 括号可以用来给表达式分组。示例如下: 别忘了方法调用时使用的括号和给表达式分组的括号含义是完全不同的。 3.3.15 表达式中的空格 FTL 忽略表达式中的多余空格,下面两种表示是相同的: 和 还有 3.3.16 操作符的优先级 下面的表格显示了已定义操作符的优先级。表格中的运算符按照优先程度降序排列:上 面的操作符优先级高于它下面的。高优先级的运算符执行要先于优先级比它低的。表格同一 行上的两个操作符优先级相同。当有相同优先级的二元运算符(运算符有两个参数,比如+ 和-)挨着出现时,它们按照从左到右的原则运算。 运算符组 运算符 最高优先级运算符 [subvarName][subStringRange].?(methodParams) expr! expr?? 一元前缀运算符 +expr -expr !expr <#-- 输出是: --> ${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 ${x + ":" + book.title?upper_case} ${x+":"+book.title?upper_case} ${ x + ":" + book . title ? upper_case } 乘除法,求模 * / % 加减法 + - 关系运算符 < > <= >= (相当于: gt, lt, 等) 相等,不等 == (也可以是: =) != 逻辑与 && 逻辑或 || 数字范围 .. 如果你熟悉 C 语言,Java 语言或 JavaScript 语言,注意 FreeMarker 中的优先级规则和它 们是相同的,除了那些只有 FTL 本身含有的操作符。 因为编程的失误,默认值操作符(exp!exp)不在表格中,按照向后兼容的原则,在 FreeMarker 2.4 版本中将会修正它。而且它将是最高优先级的运算符,但是在 FreeMarker 2.3.x 版本中它右边的优先级由于失误就非常低。所以在默认值操作符的右边中使用复杂表达式时 可以使用括号,可以是 x!(y + 1)或(x!y) + 1,而不能是 x!y + 1。 3.4 插值 插值的使用语法是:${expression},expression 可以是所有种类的表达式 (比如${100 + x})。 插值是用来给插入具体值然后转换为文本(字符串)。插值仅仅可以在两种位置使用: 文本区(如

        Hello ${name}!

        )和字符串表达式(如<#include "/footer/${company}.html">)中。 警告: 一个常犯的错误是在不能使用插值的地方使用了它。典型的错误就是<#if ${isBig}>Wow!,这是语法上的错误。只要写为<#if isBig>Wow! 就对了,而且<#if "${isBig}">Wow!也是错误的,因为这样参数就是字符 串类型了,但是 if 指令的参数要求是布尔值,所以运行时就会发生错误。 插值表达式的结果必须是字符串,数字或日期类型的,因为只有数字和日期类型可以自 动转换为字符串类型,其他类型的值(如布尔,序列)只能手动转换为字符串类型,否则就 会发生错误导致模板执行中止。 字符串插入指南:不要忘了转义! 如果插值在文本区(也就是说,不再字符串表达式中),如果 escapse 指令起作用了, 即将被插入的字符串会被自动转义。如果你要生成 HTML,那么强烈建议你利用它来阻止跨 站脚本攻击和非格式良好的 HTML 页面。这里有一个示例: <#escape x as x?html> ...

        Title: ${book.title}

        Description: <#noescape>${book.description}

        Comments:

        <#list comments as comment>
        ${comment}
        ... 这个示例展示了当生成 HTML 时,你最好将完整的模板放入到 escape 指令中。那么, 如果 book.title 包含&,它就会在输出中被替换成&,而页面还会保持格式良好 的 HTML。如果用户注释包含如