模板引擎
1. 介绍
Groovy 支持多种动态生成文本的方式,包括 GStrings
、printf
和 MarkupBuilder 等。除此之外,还有一个专门的模板框架,非常适合需要生成的文本遵循静态模板形式的应用。
2. 模板框架
Groovy 中的模板框架由一个 TemplateEngine
抽象基类(引擎必须实现)和一个 Template
接口(它们生成的模板必须实现)组成。
Groovy 中包含以下几种模板引擎
-
SimpleTemplateEngine
- 用于基本模板 -
StreamingTemplateEngine
- 功能上等同于SimpleTemplateEngine
,但可以处理大于 64k 的字符串 -
GStringTemplateEngine
- 将模板存储为可写闭包(适用于流式处理场景) -
XmlTemplateEngine
- 当模板和输出都是有效 XML 时效果很好 -
MarkupTemplateEngine
- 一个非常完整、优化的模板引擎
3. SimpleTemplateEngine
这里展示的是 SimpleTemplateEngine
,它允许您在模板中使用类似 JSP 的脚本片段(见下面的示例)、脚本和 EL 表达式来生成参数化文本。这是一个使用该系统的示例
def text = 'Dear "$firstname $lastname",\nSo nice to meet you in <% print city %>.\nSee you in ${month},\n${signed}'
def binding = ["firstname":"Sam", "lastname":"Pullara", "city":"San Francisco", "month":"December", "signed":"Groovy-Dev"]
def engine = new groovy.text.SimpleTemplateEngine()
def template = engine.createTemplate(text).make(binding)
def result = 'Dear "Sam Pullara",\nSo nice to meet you in San Francisco.\nSee you in December,\nGroovy-Dev'
assert result == template.toString()
虽然在模板(或视图)中混合处理逻辑通常不被认为是良好的实践,但有时非常简单的逻辑可能很有用。例如,在上面的示例中,我们可以将此
$firstname
更改为(假设我们已在模板内部设置了 capitalize 的静态导入)
${firstname.capitalize()}
或此
<% print city %>
更改为
<% print city == "New York" ? "The Big Apple" : city %>
3.1. 高级用法注意事项
如果您将模板直接嵌入到脚本中(如我们上面所做的那样),则必须小心反斜杠转义。因为模板字符串本身在传递给模板框架之前将由 Groovy 解析,所以您必须转义作为 Groovy 程序一部分输入的 GString 表达式或脚本片段“代码”中的任何反斜杠。例如,如果我们想在上面的 The Big Apple 周围加上引号,我们将使用
<% print city == "New York" ? "\\"The Big Apple\\"" : city %>
类似地,如果我们需要换行符,我们将使用
\\n
在 Groovy 脚本中出现的任何 GString 表达式或脚本片段“代码”中。正常的“\n”在静态模板文本本身中或如果整个模板本身在外部模板文件中都是可以的。同样,要在文本中表示实际的反斜杠,您需要
\\\\
在外部文件中或
\\\\
在任何 GString 表达式或脚本片段“代码”中。(注意:如果我们可以找到一种简单的方法来支持这种更改,那么未来版本的 Groovy 中可能不再需要这个额外的斜杠。)
4. StreamingTemplateEngine
StreamingTemplateEngine
引擎在功能上等同于 SimpleTemplateEngine
,但它使用可写闭包创建模板,使其更适用于大型模板。具体来说,此模板引擎可以处理大于 64k 的字符串。
它使用 JSP 风格的 <% %> 脚本和 <%= %> 表达式语法或 GString 风格的表达式。变量“out”绑定到模板正在写入的写入器。
通常,模板源将是一个文件,但这里我们展示了一个将模板作为字符串提供的简单示例
def text = '''\
Dear <% out.print firstname %> ${lastname},
We <% if (accepted) out.print 'are pleased' else out.print 'regret' %> \
to inform you that your paper entitled
'$title' was ${ accepted ? 'accepted' : 'rejected' }.
The conference committee.'''
def template = new groovy.text.StreamingTemplateEngine().createTemplate(text)
def binding = [
firstname : "Grace",
lastname : "Hopper",
accepted : true,
title : 'Groovy for COBOL programmers'
]
String response = template.make(binding)
assert response == '''Dear Grace Hopper,
We are pleased to inform you that your paper entitled
'Groovy for COBOL programmers' was accepted.
The conference committee.'''
5. GStringTemplateEngine
作为使用 GStringTemplateEngine
的示例,这里是上面再次完成的示例(进行了一些更改以显示其他一些选项)。这次我们首先将模板存储在一个文件中
Dear "$firstname $lastname",
So nice to meet you in <% out << (city == "New York" ? "\\"The Big Apple\\"" : city) %>.
See you in ${month},
${signed}
请注意,我们使用 out
而不是 print
来支持 GStringTemplateEngine
的流式特性。因为我们将模板放在一个单独的文件中,所以无需转义反斜杠。这是我们调用它的方式
def f = new File('test.template')
def engine = new groovy.text.GStringTemplateEngine()
def template = engine.createTemplate(f).make(binding)
println template.toString()
这是输出
Dear "Sam Pullara", So nice to meet you in "The Big Apple". See you in December, Groovy-Dev
6. XmlTemplateEngine
XmlTemplateEngine
用于模板场景,其中模板源和预期输出都旨在为 XML。模板可以使用普通的 ${expression}
和 $variable
符号将任意表达式插入到模板中。此外,还支持特殊标签:<gsp:scriptlet>
(用于插入代码片段)和 <gsp:expression>
(用于生成输出的代码片段)。
注释和处理指令将在处理过程中被删除,并且特殊的 XML 字符,如 <
、>
、"
和 '
将使用相应的 XML 符号进行转义。输出还将使用标准的 XML 美化打印进行缩进。
gsp: 标签的 xmlns 命名空间定义将被删除,但其他命名空间定义将被保留(但可能会更改为 XML 树中的等效位置)。
通常,模板源将位于文件中,但这里是一个简单的示例,将 XML 模板作为字符串提供
def binding = [firstname: 'Jochen', lastname: 'Theodorou', nickname: 'blackdrag', salutation: 'Dear']
def engine = new groovy.text.XmlTemplateEngine()
def text = '''\
<document xmlns:gsp='http://groovy.codehaus.org/2005/gsp' xmlns:foo='baz' type='letter'>
<gsp:scriptlet>def greeting = "${salutation}est"</gsp:scriptlet>
<gsp:expression>greeting</gsp:expression>
<foo:to>$firstname "$nickname" $lastname</foo:to>
How are you today?
</document>
'''
def template = engine.createTemplate(text).make(binding)
println template.toString()
此示例将生成以下输出
<document type='letter'>
Dearest
<foo:to xmlns:foo='baz'>
Jochen "blackdrag" Theodorou
</foo:to>
How are you today?
</document>
7. MarkupTemplateEngine
此模板引擎主要用于生成类似 XML 的标记(XML、XHTML、HTML5 等),但也可用于生成任何基于文本的内容。与传统模板引擎不同,它依赖于使用构建器语法的 DSL。这是一个示例模板
xmlDeclaration()
cars {
cars.each {
car(make: it.make, model: it.model)
}
}
如果您提供以下模型
model = [cars: [new Car(make: 'Peugeot', model: '508'), new Car(make: 'Toyota', model: 'Prius')]]
它将被渲染为
<?xml version='1.0'?>
<cars><car make='Peugeot' model='508'/><car make='Toyota' model='Prius'/></cars>
此模板引擎的主要特点是
-
标记构建器 般的语法
-
模板被编译成字节码
-
快速渲染
-
可选的模型类型检查
-
包含
-
国际化支持
-
片段/布局
7.1. 模板格式
7.1.1. 基础
模板由 Groovy 代码组成。让我们更深入地探讨第一个示例
xmlDeclaration() (1)
cars { (2)
cars.each { (3)
car(make: it.make, model: it.model) (4)
} (5)
}
1 | 渲染 XML 声明字符串。 |
2 | 打开一个 cars 标签 |
3 | cars 是在模板模型中找到的变量,它是 Car 实例的列表 |
4 | 对于每个项,我们创建一个 car 标签,其中包含 Car 实例的属性 |
5 | 关闭 cars 标签 |
如您所见,常规 Groovy 代码可以在模板中使用。这里,我们正在对一个列表(从模型中检索)调用 each
,允许我们为每个条目渲染一个 car
标签。
以类似的方式,渲染 HTML 代码同样简单
yieldUnescaped '<!DOCTYPE html>' (1)
html(lang:'en') { (2)
head { (3)
meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"') (4)
title('My page') (5)
} (6)
body { (7)
p('This is an example of HTML contents') (8)
} (9)
} (10)
1 | 渲染 HTML 文档类型特殊标签 |
2 | 打开带属性的 html 标签 |
3 | 打开 head 标签 |
4 | 渲染带有一个 http-equiv 属性的 meta 标签 |
5 | 渲染 title 标签 |
6 | 关闭 head 标签 |
7 | 打开 body 标签 |
8 | 渲染 p 标签 |
9 | 关闭 body 标签 |
10 | 关闭 html 标签 |
输出是直截了当的
<!DOCTYPE html><html lang='en'><head><meta http-equiv='"Content-Type" content="text/html; charset=utf-8"'/><title>My page</title></head><body><p>This is an example of HTML contents</p></body></html>
通过一些 配置,您可以让输出美观打印,自动添加换行符和缩进。 |
7.1.2. 支持方法
在前面的示例中,文档类型声明是使用 yieldUnescaped
方法渲染的。我们也看到了 xmlDeclaration
方法。模板引擎提供了几种支持方法,可以帮助您适当地渲染内容
方法 | 描述 | 示例 |
---|---|---|
yield |
渲染内容,但在渲染前进行转义 |
模板:
输出:
|
yieldUnescaped |
渲染原始内容。参数按原样渲染,不进行转义。 |
模板:
输出:
|
xmlDeclaration |
渲染 XML 声明字符串。如果配置中指定了编码,则会写入声明中。 |
模板:
输出:
如果 输出:
|
comment |
在 XML 注释内渲染原始内容 |
模板:
输出:
|
newLine |
渲染一个新行。另请参阅 |
模板:
输出:
|
pi |
渲染 XML 处理指令。 |
模板:
输出:
|
tryEscape |
如果对象是 |
模板:
输出:
|
7.1.3. 包含
MarkupTemplateEngine
支持包含来自另一个文件的内容。包含的内容可以是
-
另一个模板
-
原始内容
-
要转义的内容
包含另一个模板可以使用
include template: 'other_template.tpl'
将文件作为原始内容包含,而不对其进行转义,可以这样做
include unescaped: 'raw.txt'
最终,包含在渲染前应转义的文本可以使用
include escaped: 'to_be_escaped.txt'
或者,您可以使用以下辅助方法代替
-
includeGroovy(<name>)
包含另一个模板 -
includeEscaped(<name>)
包含另一个文件并转义 -
includeUnescaped(<name>)
包含另一个文件而不转义
调用这些方法而不是 include xxx:
语法在要包含的文件名是动态的(例如存储在变量中)时可能很有用。要包含的文件(无论其类型,模板或文本)都可以在 classpath 上找到。这是 MarkupTemplateEngine
接受可选 ClassLoader
作为构造函数参数的原因之一(另一个原因是您可以在模板中包含引用其他类的代码)。
如果您不希望模板位于 classpath 上,MarkupTemplateEngine
提供了一个方便的构造函数,允许您定义模板所在的目录。
7.1.4. 片段
片段是嵌套模板。它们可用于在单个模板中提供改进的组合。片段由一个字符串、内部模板和一个用于渲染此模板的模型组成。考虑以下模板
ul {
pages.each {
fragment "li(line)", line:it
}
}
fragment
元素创建一个嵌套模板,并使用特定于此模板的模型进行渲染。在这里,我们有 li(line)
片段,其中 line
绑定到 it
。由于 it
对应于 pages
的迭代,我们将为模型中的每个页面生成一个 li
元素
<ul><li>Page 1</li><li>Page 2</li></ul>
片段对于模板元素的因子化很有趣。它们的代价是每个模板的片段编译,并且它们不能被外部化。
7.1.5. 布局
布局,与片段不同,指的是其他模板。它们可用于组合模板并共享通用结构。如果您有一个通用的 HTML 页面设置,并且只想替换主体,这通常很有趣。这可以通过布局轻松完成。首先,您需要创建一个布局模板
html {
head {
title(title) (1)
}
body {
bodyContents() (2)
}
}
1 | title 变量(在 title 标签内)是一个布局变量 |
2 | bodyContents 调用将渲染主体 |
然后你需要一个包含布局的模板
layout 'layout-main.tpl', (1)
title: 'Layout example', (2)
bodyContents: contents { p('This is the body') } (3)
1 | 使用 main-layout.tpl 布局文件 |
2 | 设置 title 变量 |
3 | 设置 bodyContents |
如您所见,由于布局文件中的 bodyContents()
调用,bodyContents
将在布局内部渲染。因此,模板将渲染为
<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>
对 contents
方法的调用用于告诉模板引擎,该代码块实际上是模板的规范,而不是要直接渲染的辅助函数。如果您在规范之前不添加 contents
,那么内容将被渲染,但您也会看到一个生成的随机字符串,对应于该块的结果值。
布局是一种在多个模板之间共享通用元素的强大方式,无需重写所有内容或使用包含。
布局默认使用与它们所使用的页面的模型无关的模型。但是,可以使它们继承自父模型。想象一下模型定义如下
model = new HashMap<String,Object>();
model.put('title','Title from main model');
以及以下模板
layout 'layout-main.tpl', true, (1)
bodyContents: contents { p('This is the body') }
1 | 注意使用 true 来启用模型继承 |
那么就不需要像 上一个示例 那样将 title
值传递给布局。结果将是
<html><head><title>Title from main model</title></head><body><p>This is the body</p></body></html>
但也可以覆盖父模型中的值
layout 'layout-main.tpl', true, (1)
title: 'overridden title', (2)
bodyContents: contents { p('This is the body') }
1 | true 表示继承自父模型 |
2 | 但 title 被覆盖 |
然后输出将是
<html><head><title>overridden title</title></head><body><p>This is the body</p></body></html>
7.2. 渲染内容
7.2.1. 模板引擎创建
在服务器端,渲染模板需要一个 groovy.text.markup.MarkupTemplateEngine
实例和一个 groovy.text.markup.TemplateConfiguration
TemplateConfiguration config = new TemplateConfiguration(); (1)
MarkupTemplateEngine engine = new MarkupTemplateEngine(config); (2)
Template template = engine.createTemplate("p('test template')"); (3)
Map<String, Object> model = new HashMap<>(); (4)
Writable output = template.make(model); (5)
output.writeTo(writer); (6)
1 | 创建模板配置 |
2 | 使用此配置创建模板引擎 |
3 | 从 String 创建模板实例 |
4 | 创建要在模板中使用的模型 |
5 | 将模型绑定到模板实例 |
6 | 渲染输出 |
有几种可能的选项来解析模板
-
从
String
,使用createTemplate(String)
-
从
Reader
,使用createTemplate(Reader)
-
从
URL
,使用createTemplate(URL)
-
给定模板名称,使用
createTemplateByPath(String)
通常首选最后一种方式
Template template = engine.createTemplateByPath("main.tpl");
Writable output = template.make(model);
output.writeTo(writer);
7.2.2. 配置选项
引擎的行为可以通过 TemplateConfiguration
类中的几个配置选项进行调整
选项 | 默认值 | 描述 | 示例 |
---|---|---|---|
declarationEncoding |
null |
确定调用 |
模板:
输出:
如果 输出:
|
expandEmptyElements |
false |
如果为 true,则空标签以其展开形式渲染。 |
模板:
输出:
如果 输出:
|
useDoubleQuotes |
false |
如果为 true,则属性使用双引号而不是单引号 |
模板:
输出:
如果 输出:
|
newLineString |
系统默认值(系统属性 |
允许选择渲染新行时使用的字符串 |
模板:
如果 输出:
|
autoEscape |
false |
如果为 true,则模型中的变量在渲染前自动转义。 |
参阅 自动转义部分 |
autoIndent |
false |
如果为 true,则在新行后执行自动缩进 |
参阅 自动格式化部分 |
autoIndentString |
四 (4) 个空格 |
用作缩进的字符串。 |
参阅 自动格式化部分 |
autoNewLine |
false |
如果为 true,则根据模板源的原始格式自动插入新行 |
参阅 自动格式化部分 |
baseTemplateClass |
|
设置编译模板的超类。这可用于提供特定于应用程序的模板。 |
参阅 自定义模板部分 |
locale |
默认区域设置 |
设置模板的默认区域设置。 |
参阅 国际化部分 |
模板引擎创建后,更改配置是不安全的。 |
7.2.3. 自动格式化
默认情况下,模板引擎将不进行任何特定格式化地渲染输出。一些 配置选项 可以改善这种情况
-
autoIndent
负责在新行插入后自动缩进 -
autoNewLine
负责根据模板源的原始格式自动插入新行
通常,如果您想要可读的、美观的输出,建议将 autoIndent
和 autoNewLine
都设置为 true
config.setAutoNewLine(true);
config.setAutoIndent(true);
使用以下模板
html {
head {
title('Title')
}
}
输出现在将是
<html>
<head>
<title>Title</title>
</head>
</html>
我们可以稍微修改模板,使 title
指令与 head
指令在同一行
html {
head { title('Title')
}
}
输出将反映这一点
<html>
<head><title>Title</title>
</head>
</html>
新行只在标签的{}(大括号)处插入,并且插入位置与嵌套内容的位置相对应。这意味着另一个标签主体中的标签不会触发新行,除非它们自己使用{}(大括号)
html {
head {
meta(attr:'value') (1)
title('Title') (2)
newLine() (3)
meta(attr:'value2') (4)
}
}
1 | 插入新行,因为 meta 不在与 head 同一行 |
2 | 不插入新行,因为我们与上一个标签在同一深度 |
3 | 我们可以通过显式调用 newLine 强制渲染新行 |
4 | 此标签将在单独的行上渲染 |
这次,输出将是
<html>
<head>
<meta attr='value'/><title>Title</title>
<meta attr='value2'/>
</head>
</html>
默认情况下,渲染器使用四个(4)个空格作为缩进,但您可以通过设置 TemplateConfiguration#autoIndentString
属性来更改它。
7.2.4. 自动转义
默认情况下,从模型中读取的内容将按原样渲染。如果此内容来自用户输入,则它可能很敏感,您可能希望默认对其进行转义,例如避免 XSS 注入。为此,模板配置提供了一个选项,该选项将自动转义模型中的对象,只要它们继承自 CharSequence
(通常是 `String`s)。
我们来设想以下设置
config.setAutoEscape(false);
model = new HashMap<String,Object>();
model.put("unsafeContents", "I am an <html> hacker.");
以及以下模板
html {
body {
div(unsafeContents)
}
}
那么您就不希望 unsafeContents
中的 HTML 按原样渲染,因为存在潜在的安全问题
<html><body><div>I am an <html> hacker.</div></body></html>
自动转义将解决此问题
config.setAutoEscape(true);
现在输出已正确转义
<html><body><div>I am an <html> hacker.</div></body></html>
请注意,使用自动转义并不妨碍您从模型中包含未转义的内容。为此,您的模板应该明确指出模型变量不应转义,方法是在其前面加上 unescaped.
,如下例所示
html {
body {
div(unescaped.unsafeContents)
}
}
7.2.5. 常见陷阱
包含标记的字符串
假设您想生成一个包含标记字符串的 <p>
标签
p {
yield "This is a "
a(href:'target.html', "link")
yield " to another page"
}
并生成
<p>This is a <a href='target.html'>link</a> to another page</p>
这不能写得更短吗?一个幼稚的替代方案是
p {
yield "This is a ${a(href:'target.html', "link")} to another page"
}
但结果不会像预期那样
<p><a href='target.html'>link</a>This is a to another page</p>
原因是标记模板引擎是一个流式引擎。在原始版本中,第一个 yield
调用生成一个字符串并将其流式传输到输出,然后生成 a
链接并流式传输,然后流式传输最后一个 yield
调用,从而导致按顺序执行。但对于上面的字符串版本,执行顺序不同
-
yield
调用需要一个参数,一个 string -
该参数需要在 yield 调用生成之前进行评估
因此,评估字符串会导致 a(href:...)
调用在 yield
本身被调用之前执行。这不是您想要做的。相反,您想要生成一个包含标记的字符串,然后将其传递给 yield
调用。可以这样做
p("This is a ${stringOf {a(href:'target.html', "link")}} to another page")
注意 stringOf
调用,它基本上告诉标记模板引擎底层标记需要单独渲染并导出为字符串。请注意,对于简单的表达式,stringOf
可以替换为以美元符号开头的替代标签符号
p("This is a ${$a(href:'target.html', "link")} to another page")
值得注意的是,使用 stringOf 或特殊的 $tag 符号会触发创建单独的字符串写入器,然后用于渲染标记。这比使用直接流式传输标记的 yield 调用版本要慢。 |
7.2.6. 国际化
模板引擎原生支持国际化。为此,当您创建 TemplateConfiguration
时,您可以提供一个 Locale
,它是用于模板的默认区域设置。每个模板可能具有不同的版本,每个区域设置一个。模板的名称有所不同
-
file.tpl
: 默认模板文件 -
file_fr_FR.tpl
: 模板的法语版本 -
file_en_US.tpl
: 模板的美式英语版本 -
…
当模板被渲染或包含时,则
-
如果模板名称或包含名称明确设置了区域设置,则包含特定版本,如果未找到则包含默认版本
-
如果模板名称不包含区域设置,则使用
TemplateConfiguration
区域设置的版本,如果未找到则使用默认版本
例如,假设默认区域设置为 Locale.ENGLISH
且主模板包含
include template: 'locale_include_fr_FR.tpl'
然后模板使用特定模板渲染
Texte en français
使用不带区域设置的 include 将使模板引擎查找具有配置区域设置的模板,如果未找到,则回退到默认模板,如下所示
include template: 'locale_include.tpl'
Default text
然而,将模板引擎的默认区域设置为 Locale.FRANCE
将改变输出,因为模板引擎现在将查找具有 fr_FR
区域的文件
Texte en français
此策略允许您通过依赖默认模板(文件名中未设置区域设置的模板)来逐个翻译您的模板。
7.2.7. 自定义模板类
默认情况下,创建的模板继承 groovy.text.markup.BaseTemplate
类。为应用程序提供不同的模板类可能很有趣,例如提供与应用程序相关的额外辅助方法,或自定义渲染基元(例如用于 HTML)。
模板引擎通过在 TemplateConfiguration
中设置替代的 baseTemplateClass
来提供此功能
config.setBaseTemplateClass(MyTemplate.class);
自定义基类必须像这个例子一样扩展 BaseClass
public abstract class MyTemplate extends BaseTemplate {
private List<Module> modules
public MyTemplate(
final MarkupTemplateEngine templateEngine,
final Map model,
final Map<String, String> modelTypes,
final TemplateConfiguration configuration) {
super(templateEngine, model, modelTypes, configuration)
}
List<Module> getModules() {
return modules
}
void setModules(final List<Module> modules) {
this.modules = modules
}
boolean hasModule(String name) {
modules?.any { it.name == name }
}
}
此示例显示了一个提供了名为 hasModule
的额外方法的类,该方法可以直接在模板中使用
if (hasModule('foo')) {
p 'Found module [foo]'
} else {
p 'Module [foo] not found'
}
7.3. 类型检查模板
7.3.1. 可选类型检查
即使模板未进行类型检查,它们也是静态编译的。这意味着一旦模板被编译,性能应该非常好。对于某些应用程序,最好确保模板在实际渲染之前是有效的。这意味着如果模型变量上的方法不存在,例如,模板编译将失败。
MarkupTemplateEngine
提供了这样的功能。模板可以选择性地进行类型检查。为此,开发人员必须在模板创建时提供额外的信息,即模型中变量的类型。想象一个模型公开了一个页面列表,其中页面定义为
public class Page {
Long id
String title
String body
}
然后可以在模型中公开页面列表,如下所示
Page p = new Page();
p.setTitle("Sample page");
p.setBody("Page body");
List<Page> pages = new LinkedList<>();
pages.add(p);
model = new HashMap<String,Object>();
model.put("pages", pages);
模板可以轻松使用它
pages.each { page -> (1)
p("Page title: $page.title") (2)
p(page.text) (3)
}
1 | 遍历模型中的页面 |
2 | page.title 有效 |
3 | page.text 无效(应该是 page.body ) |
如果没有类型检查,模板的编译会成功,因为模板引擎在页面实际渲染之前并不知道模型。这意味着问题只会在运行时,即页面渲染时才出现
No such property: text
在某些情况下,这可能很难解决甚至很难发现。通过向模板引擎声明 pages
的类型,我们现在能够在编译时失败
modelTypes = new HashMap<String,String>(); (1)
modelTypes.put("pages", "List<Page>"); (2)
Template template = engine.createTypeCheckedModelTemplate("main.tpl", modelTypes) (3)
1 | 创建一个用于保存模型类型的映射 |
2 | 声明 pages 变量的类型(注意类型使用字符串) |
3 | 使用 createTypeCheckedModelTemplate 而不是 createTemplate |
这次,当模板在最后一行编译时,会发生错误
[Static type checking] - No such property: text for class: Page
这意味着您不需要等到页面渲染才能看到错误。使用 createTypeCheckedModelTemplate
是强制性的。
7.3.2. 替代类型声明
或者,如果开发者也是模板的编写者,则可以直接在模板中声明预期变量的类型。在这种情况下,即使您调用 createTemplate
,它也会进行类型检查
modelTypes = { (1)
List<Page> pages (2)
}
pages.each { page ->
p("Page title: $page.title")
p(page.text)
}
1 | 类型需要在 modelTypes 头部声明 |
2 | 为模型中的每个对象声明一个变量 |
7.3.3. 类型检查模板的性能
使用类型检查模型还有一个额外的优点是性能应该会提高。通过告诉类型检查器预期的类型是什么,您还允许编译器为此生成优化的代码,因此如果您正在寻找最佳性能,请考虑使用类型检查模板。
8. 其他解决方案
此外,还有其他可与 Groovy 一起使用的模板解决方案,例如 FreeMarker、Velocity、StringTemplate 等。