Thymeleaf
翻译自:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
up主用google虾译的,和up主完全无关,强裂建议直接看原文。
主要就是几个符号啦:
消息:#{}
表达式有作用域:${}
表达式无作用域:*{}
url:@{}
相对地址:~{}
js内转义特殊字符:[[${})]
js内原生格式:[(${})],
dom属性:th:each th:object th:href th:url
内置函数:#ctx #request ......
Thymeleaf也没有什么大不了的。
目录
th:insert th:replace (and th:include)区别
13 文本模式
1简介
1.1什么是Thymeleaf?
Thymeleaf是一个用于web和独立环境的现代服务器端Java模板引擎,能够处理HTML、XML、JavaScript、CSS甚至纯文本。
Thymeleaf的主要目标是提供一种优雅且易于维护的创建模板的方法。为了实现这一点,它基于自然模板的概念,以一种不影响模板作为设计原型使用的方式将其逻辑注入模板文件中。这改善了设计的交流,并弥合了设计和开发团队之间的差距。
从一开始,Thymeleaf的设计就考虑到了Web标准——尤其是HTML5——如果需要的话,它允许您创建完全验证模板。
1.2 Thymeleaf可以处理什么样的模板?
开箱即用,Thymeleaf允许您处理六种模板,每一种都被称为模板模式:
- HTML
- XML
- TEXT
- JAVASCRIPT
- CSS
- RAW
有两种标记模板模式(HTML和XML)、三种文本模板模式(文本、JAVASCRIPT和CSS)和无操作模板模式(RAW)。
HTML模板模式将允许任何类型的HTML输入,包括HTML5、HTML 4和XHTML。不执行验证或格式良好性检查,模板代码/结构将在输出中得到最大程度的尊重。
XML模板模式将允许XML输入。在这种情况下,代码应该是格式良好的—没有未关闭的标记,没有未引用的属性,等等—如果发现违反格式良好的情况,解析器将抛出异常。注意,不会执行(针对DTD或XML模式)验证。
文本模板模式将允许对非标记性质的模板使用特殊语法。此类模板的示例可能是文本电子邮件或模板化文档。注意,HTML或XML模板也可以作为文本处理,在这种情况下,它们不会被解析为标记,并且每个标记、DOCTYPE、注释等都将被视为纯文本。
JAVASCRIPT模板模式将允许在Thymeleaf应用程序中处理JAVASCRIPT文件。这意味着能够像在HTML文件中那样在JavaScript文件中使用模型数据,但是使用特定于JavaScript的集成,如专门转义或自然脚本。JAVASCRIPT模板模式被认为是文本模式,因此使用与文本模板模式相同的特殊语法。
CSS模板模式将允许处理Thymeleaf应用程序中涉及的CSS文件。与JAVASCRIPT模式类似,CSS模板模式也是文本模式,使用来自文本模板模式的特殊处理语法。
原始模板模式将完全不处理模板。它用于将未接触的资源(文件、URL响应等)插入正在处理的模板中。例如,可以将HTML格式的外部不受控制的资源包含到应用程序模板中,并且安全地知道这些资源可能包含的任何Thymeleaf代码都不会被执行。
1.3方言:标准方言
略
2 设计良好的虚拟杂货店
本文所示示例的源代码以及本指南的后续章节可以在Good中找到
Thymes Virtual Grocery GitHub repository.
2.1杂货店网站
略
我们的应用程序还有一个非常简单的服务层,由包含如下方法的服务对象组成:
public class ProductService {
...
public List<Product> findAll() {
return ProductRepository.getInstance().findAll();
}
public Product findById(Integer id) {
return ProductRepository.getInstance().findById(id);
}
}
在web层,我们的应用程序将有一个过滤器,根据请求URL将执行委托给支持thymeleaf的命令:
private boolean process(HttpServletRequest request, HttpServletResponse response)
throws ServletException {
try {
// This prevents triggering engine executions for resource URLs
if (request.getRequestURI().startsWith("/css") ||
request.getRequestURI().startsWith("/images") ||
request.getRequestURI().startsWith("/favicon")) {
return false;
}
/*
* Query controller/URL mapping and obtain the controller
* that will process the request. If no controller is available,
* return false and let other filters/servlets process the request.
*/
IGTVGController controller = this.application.resolveControllerForRequest(request);
if (controller == null) {
return false;
}
/*
* Obtain the TemplateEngine instance.
*/
ITemplateEngine templateEngine = this.application.getTemplateEngine();
/*
* Write the response headers
*/
response.setContentType("text/html;charset=UTF-8");
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
/*
* Execute the controller and process view template,
* writing the results to the response writer.
*/
controller.process(
request, response, this.servletContext, templateEngine);
return true;
} catch (Exception e) {
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} catch (final IOException ignored) {
// Just ignore this
}
throw new ServletException(e);
}
}
这是我们的IGTVGController接口:
我们现在要做的就是创建IGTVGController接口的实现,从服务中检索数据,并使用ITemplateEngine对象处理模板
最后,它会是这样的:
但是首先让我们看看模板引擎是如何初始化的。
2.2创建和配置模板引擎
我们过滤器中的process(…)方法包含这一行:
ITemplateEngine templateEngine = this.application.getTemplateEngine();
这意味着GTVGApplication类负责创建和配置Thymeleaf应用程序中最重要的对象之一:TemplateEngine实例(ITemplateEngine接口的实现)。
我们的org.thymeleaf。TemplateEngine对象是这样初始化的:
public class GTVGApplication {
...
private final TemplateEngine templateEngine;
...
public GTVGApplication(final ServletContext servletContext) {
super();
ServletContextTemplateResolver templateResolver =
new ServletContextTemplateResolver(servletContext);
// HTML is the default mode, but we set it anyway for better understanding of code
templateResolver.setTemplateMode(TemplateMode.HTML);
// This will convert "home" to "/WEB-INF/templates/home.html"
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
// Template cache TTL=1h. If not set, entries would be cached until expelled
templateResolver.setCacheTTLMs(Long.valueOf(3600000L));
// Cache is set to true by default. Set to false if you want templates to
// be automatically updated when modified.
templateResolver.setCacheable(true);
this.templateEngine = new TemplateEngine();
this.templateEngine.setTemplateResolver(templateResolver);
...
}
}
有很多方法可以配置TemplateEngine对象,但是现在这几行代码可以告诉我们所需的步骤
模板解析器
让我们从模板解析器开始:
ServletContextTemplateResolver templateResolver =
new ServletContextTemplateResolver(servletContext);
模板解析器是实现Thymeleaf API接口的对象,该接口名为org.thymeleaf.templateresolver.ITemplateResolver:
public interface ITemplateResolver {
...
/*
* Templates are resolved by their name (or content) and also (optionally) their
* owner template in case we are trying to resolve a fragment for another template.
* Will return null if template cannot be handled by this template resolver.
*/
public TemplateResolution resolveTemplate(
final IEngineConfiguration configuration,
final String ownerTemplate, final String template,
final Map<String, Object> templateResolutionAttributes);
}
这些对象负责决定如何访问我们的模板,在这个GTVG应用程序中,是org.thymeleaf.templateresolver。servletcontext意味着我们将从Servlet上下文中检索模板文件作为资源:一个应用程序范围的javax.servlet。ServletContext对象,它存在于每个Java web应用程序中,并解析来自web应用程序根目录的资源。
但这并不是关于模板解析器的全部内容,因为我们可以在其上设置一些配置参数。第一,模板模式:
templateResolver.setTemplateMode(TemplateMode.HTML);
HTML是servlet context ttemplateresolver的默认模板模式,但是无论如何建立它都是一个很好的实践,这样我们的代码就可以清楚地记录发生了什么。
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
前缀和后缀修改我们将传递给引擎的模板名称,以获得要使用的实际资源名称。
使用此配置,模板名称“product/list”对应于:
servletContext.getResourceAsStream("/WEB-INF/templates/product/list.html")
可选地,通过cacheTTLMs属性在模板解析器上配置已解析模板可以驻留在缓存中的时间:
templateResolver.setCacheTTLMs(3600000L);
如果达到最大缓存大小,并且它是当前缓存的最古老的条目,那么在到达TTL之前,仍然可以从缓存中驱逐模板。
用户可以通过实现ICacheManager接口或修改StandardCacheManager对象来管理默认缓存来定义缓存行为和大小。
关于模板解析器还有很多要了解的,但是现在让我们来看看模板引擎对象的创建。
模板引擎
模板引擎对象是org.thymeleaf的实现。ITemplateEngine接口。这些实现之一是由Thymeleaf核心提供的:org.thymeleaf。TemplateEngine,我们在这里创建一个实例:
templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
很简单,不是吗?我们只需要创建一个实例并将模板解析器设置为它。
模板解析器是TemplateEngine需要的惟一必需参数,不过后面还将介绍其他许多参数(消息解析器、缓存大小等)。现在,这就是我们所需要的。
我们的模板引擎现在已经准备好了,我们可以开始使用Thymeleaf创建页面。
3 文本
3.1多语言的欢迎
使用th:文本和外部化文本
略
上下文
略
执行模板引擎
略
3.2更多关于文本和变量的信息
非转义的文本
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
使用变量
<body>
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
<p>Today is: <span th:text="${today}">13 February 2011</span></p>
</body>
4标准表达式语法
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
<p>Today is: <span th:text="${today}">13 february 2011</span></p>
Simple expressions:
- Variable Expressions: ${...}
- Selection Variable Expressions: *{...}
- Message Expressions: #{...}
- Link URL Expressions: @{...}
- Fragment Expressions: ~{...}
Literals 常量
- Text literals: 'one text', 'Another one!',…
- Number literals: 0, 34, 3.0, 12.3,…
- Boolean literals: true, false
- Null literal: null
- Literal tokens: one, sometext, main,…
Text operations:
- String concatenation: +
- Literal substitutions: |The name is ${name}|
Arithmetic operations:
- Binary operators: +, -, *, /, %
- Minus sign (unary operator): -
Boolean operations:
- Binary operators: and, or
- Boolean negation (unary operator): !, not
Comparisons and equality:
- Comparators: >, <, >=, <= (gt, lt, ge, le)
- Equality operators: ==, != (eq, ne)
Conditional operators:
- If-then: (if) ? (then)
- If-then-else: (if) ? (then) : (else)
- Default: (value) ?: (defaultvalue)
Special tokens:
- No-Operation: _
所有这些功能可以组合和嵌套:
'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))
4.1 消息
如我们所知,……消息表达式允许我们链接这个:
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
消息键本身可以来自一个变量:
<p th:utext="#{${welcomeMsgKey}(${session.user.name})}">
Welcome to our grocery store, Sebastian Pepper!
</p>
4.2 变量
<p>Today is: <span th:text="${today}">13 february 2011</span>.</p>
实际上等于
ctx.getVariable("today");
但是OGNL允许我们创建更强大的表达式,这就是它的原理:
<p th:utext="#{home.welcome(${session.user.name})}">
Welcome to our grocery store, Sebastian Pepper!
</p>
…通过执行以下命令获得用户名:
((User) ctx.getVariable("session").get("user")).getName();
但是getter方法导航只是OGNL的特性之一。让我们再看一些:
/*
* Access to properties using the point (.). Equivalent to calling property getters.
*/
${person.father.name}
/*
* Access to properties can also be made by using brackets ([]) and writing
* the name of the property as a variable or between single quotes.
*/
${person['father']['name']}
/*
* If the object is a map, both dot and bracket syntax will be equivalent to
* executing a call on its get(...) method.
*/
${countriesByCode.ES}
${personsByName['Stephen Zucchini'].age}
/*
* Indexed access to arrays or collections is also performed with brackets,
* writing the index without quotes.
*/
${personsArray[0].name}
/*
* Methods can be called, even with arguments.
*/
${person.createCompleteName()}
${person.createCompleteNameWithSeparator('-')}
基本对象表达式
当在上下文变量上计算OGNL表达式时,为了获得更高的灵活性,表达式可以使用一些对象。这些对象将被引用(按照OGNL标准),从#符号开始:
- #ctx: the context object.
- #vars: the context variables.
- #locale: the context locale.
- #request: (only in Web Contexts) the HttpServletRequest object.
- #response: (only in Web Contexts) the HttpServletResponse object.
- #session: (only in Web Contexts) the HttpSession object.
- #servletContext: (only in Web Contexts) the ServletContext object.
我们可以这样做:
Established locale country: <span th:text="${#locale.country}">US</span>.
扩展表达式对象
除了这些基本对象之外,Thymeleaf还将提供一组实用程序对象,帮助我们在表达式中执行常见的任务。
- #execInfo: information about the template being processed.
- #messages: methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.
- #uris: methods for escaping parts of URLs/URIs
- #conversions: methods for executing the configured conversion service (if any).
- #dates: methods for java.util.Date objects: formatting, component extraction, etc.
- #calendars: analogous to #dates, but for java.util.Calendar objects.
- #numbers: methods for formatting numeric objects.
- #strings: methods for String objects: contains, startsWith, prepending/appending, etc.
- #objects: methods for objects in general.
- #bools: methods for boolean evaluation.
- #arrays: methods for arrays.
- #lists: methods for lists.
- #sets: methods for sets.
- #maps: methods for maps.
- #aggregates: methods for creating aggregates on arrays or collections.
- #ids: methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).
4.3表达式的作用域(星号语法)
变量表达式不仅可以写成${…},也可以写成*{…}。
但是有一个重要的区别:星号语法对所选对象的表达式求值,而不是对整个上下文求值。也就是说,只要没有选择对象,美元和星号语法的作用是完全相同的。
当对象选择就绪时,所选对象也将作为#对象表达式变量提供给美元表达式:
<div th:object="${session.user}">
<p>Name: <span th:text="${#object.firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>
如前所述,如果没有th:object,则美元语法和星号语法是等效的。
<div>
<p>Name: <span th:text="*{session.user.name}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{session.user.surname}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{session.user.nationality}">Saturn</span>.</p>
</div>
4.4 URL链接
由于它们的重要性,url在web应用程序模板中是一等公民,Thymeleaf标准方言为它们提供了一种特殊的语法,即@语法:@{…}
有不同类型的URL:
绝对url:http://www.thymeleaf.org
相对url,可以是:
- Page-relative: user/login.html
- Context-relative: /itemdetails?id=3 (context name in server will be added automatically)
- Server-relative: ~/billing/processInvoice (allows calling URLs in another context (= application) in the same server.
- Protocol-relative URLs: //code.jquery.com/jquery-2.0.3.min.js
这些表达式的实际处理以及它们对将要输出的url的转换是由org.thymeleaf.linkbuilder的实现完成的。注册到正在使用的ITemplateEngine对象中的ILinkBuilder接口。
默认情况下,该接口的单个实现注册为org.thymeleaf.linkbuilder类。StandardLinkBuilder,这对于离线(非web)和基于Servlet API的web场景都足够了。其他场景(如与非servletapi web框架的集成)可能需要link builder接口的特定实现。
让我们使用这个新语法。满足th:href属性:
<!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) -->
<a href="details.html"
th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>
<!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) -->
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
<!-- Will produce '/gtvg/order/3/details' (plus rewriting) -->
<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>
这里需要注意的是:
th:href是一个修饰符属性:一旦处理,它将计算要使用的链接URL,并将该值设置为标记的href属性。
我们可以为URL参数使用表达式(正如您可以在orderId=${o.id}中看到的那样)。还将自动执行所需的url参数编码操作。
如果需要几个参数,则用逗号分隔:@{/order/process(execId=${execId},execType='FAST')}
URL路径中也允许使用变量模板:@{/order/{orderId}/details(orderId=${orderId})}
以/(例如:/order/details)开头的相对url将自动以应用程序上下文名称作为前缀。
如果没有启用cookie,或者还不知道,可以向相对url添加一个“;jsessionid=…”后缀,以便保留会话。这称为URL重写,Thymeleaf允许您通过使用Servlet API中的response.encodeURL(…)机制为每个URL插入自己的重写过滤器。
th:href属性允许我们(可选地)在模板中有一个可工作的静态href属性,这样在直接为原型目的打开模板链接时,浏览器就可以继续导航。
与消息语法(#{…})一样,URL基也可以是计算另一个表达式的结果:
<a th:href="@{${url}(orderId=${o.id})}">view</a>
<a th:href="@{'/details/'+${user.login}(orderId=${o.id})}">view</a>
4.5 模板
最常见的用法是使用th:insert或th:replace插入片段(在后面的小节中将详细介绍):
<div th:insert="~{commons :: main}">...</div>
在本教程的后面,有一个完整的部分专门讨论模板布局,包括对片段表达式的更深入的解释。
4.6 常量
略。
4.7 追加文字
文本,无论它们是文本还是变量或消息表达式的计算结果,都可以使用+运算符轻松地附加:
<span th:text="'The name of the user is ' + ${user.name}">
4.8 常量替换
文字替换允许对包含变量值的字符串进行简单的格式化,而不需要在字符串后面加上'…”+“…”。
这些替换必须被竖条(|)包围,例如:
<span th:text="|Welcome to our application, ${user.name}!|">
等于:
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
文字替代可以与其他类型的表达式结合:
<span th:text="${onevar} + ' ' + |${twovar}, ${threevar}|">
只有变量/消息表达式(${…},{…}, #{…})允许在|…|文字替换。没有其他的文字('…'),布尔/数字符号,条件表达式等。
4.9 数学计算
还有一些算术运算:+、-、*、/和%。
<div th:with="isEven=(${prodStat.count} % 2 == 0)">
注意,这些运算符也可以应用于OGNL变量表达式本身(在这种情况下,将由OGNL而不是Thymeleaf标准表达式引擎执行):
4.10 比较和相等
表达式中的值可以与>、<、>=和<=符号进行比较,并且可以使用==和!=操作符检查是否相等(或是否不相等)。注意,XML规定不应该在属性值中使用<和>符号,因此应该用<
和>。
<div th:if="${prodStat.count} > 1">
<span th:text="'Execution mode is ' + ( (${execMode} == 'dev')? 'Development' : 'Production')">
一个更简单的选择可能是使用这些操作符中存在的文本别名:gt(>)、lt(<)、ge(>=)、le(<=),而不是(!)。eq (==), neq/ne (!=)
4.11 条件表达式
条件表达式的作用是仅对两个表达式中的一个求值,这取决于求值条件的结果(条件本身也是另一个表达式)。
让我们看一个示例片段(引入另一个属性修饰符th:class):
<tr th:class="${row.even}? 'even' : 'odd'">
...
</tr>
条件表达式的所有三个部分(condition, then和else)本身都是表达式,这意味着它们可以是变量(${…)}*{…}),消息(# {…}),url(@{…})或文本(“…”)。
条件表达式也可以嵌套使用括号:
<tr th:class="${row.even}? (${row.first}? 'first' : 'even') : 'odd'">
...
</tr>
Else表达式也可以省略,如果条件为false,则返回null值:
<tr th:class="${row.even}? 'alt'">
...
</tr>
4.12 默认值
<div th:object="${session.user}">
...
<p>Age: <span th:text="*{age}?: '(no age specified)'">27</span>.</p>
</div>
4.13 无操作
无操作令牌由下划线符号(_)表示。
这个令牌背后的思想是指定表达式的期望结果是什么都不做,也就是说,就像根本不存在可处理属性(例如th:text)一样。
在其他可能性中,这允许开发人员使用原型文本作为默认值。例如,代替:
<span th:text="${user.name} ?: 'no user authenticated'">...</span>
……我们可以直接使用“no user authenticated”作为原型文本,这使得代码从设计角度来说更加简洁和通用:
<span th:text="${user.name} ?: _">no user authenticated</span>
4.14 数据转换和格式化
Thymeleaf为变量(${…})和选择(*{…})表达式定义了一个双括号语法,它允许我们通过配置的转换服务应用数据转换。
基本上是这样的:
<td th:text="${{user.lastAccessDate}}">...</td>
注意到双括号了吗?:$ {{…}}。它指示Thymeleaf传递用户的结果。转换服务的lastAccessDate表达式,并要求它在写入结果之前执行格式化操作(转换为字符串)。
假设用户。lastAccessDate类型为java.util.Calendar,如果一个转换服务(IStandardConversionService的实现)已经注册,并且包含一个有效的日历->字符串转换,那么它将被应用。
IStandardConversionService (StandardConversionService类)的默认实现只是在任何转换为字符串的对象上执行. tostring()。有关如何注册自定义转换服务实现的更多信息,请参阅配置部分的更多信息。
官方的Thymeleaf -spring3和Thymeleaf -spring4集成包透明地将Thymeleaf的转换服务机制与Spring自己的转换服务基础设施集成在一起,这样在Spring配置中声明的转换服务和格式化程序将自动提供给${{…}}和{{…}}表达式。
4.15 预处理
除了这些用于表达处理的特性外,Thymeleaf还具有预处理表达式的特性。
<p th:text="${__#{article.text('textVar')}__}">Some text here...</p>
注意,法语语言环境的预处理步骤将创建以下等价的代码:
<p th:text="${@myapp.translator.Translator@translateToFrench(textVar)}">Some text here...</p>
预处理字符串可以使用\_\_在属性中转义。
5 设置属性
本章将解释如何在标记中设置(或修改)属性值
5.1 设置任意属性
然后输入th:attr属性,以及它改变所设置的标签属性值的能力:
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
但是如果我们想一次设置多个属性呢?XML规则不允许在标记中两次设置属性,因此th:attr将采用逗号分隔的赋值列表,如:
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
给定所需的消息文件,这将输出:
<img src="/gtgv/images/gtvglogo.png" title="Logo de Good Thymes" alt="Logo de Good Thymes" />
5.2特定属性赋值
现在,你可能会想:
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
是一段相当难看的标记。在属性值中指定赋值可能非常实用,但如果必须一直这样做,那么它不是创建模板的最优雅的方法。
Thymeleaf同意您的观点,这就是为什么th:attr很少用于模板中。通常,您将使用其他th:*属性,其任务是设置特定的标记属性(而不是像th:attr这样的任何属性)。
例如,要设置value属性,可以使用th:value:
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
这看起来好多了!让我们尝试对表单标记中的action属性执行相同的操作:
<form action="subscribe.html" th:action="@{/subscribe}">
5.3 一次性设置多个值
有两个非常特殊的属性叫做th:alt-title和th:lang-xmllang,可以用于将两个属性同时设置为相同的值。具体地说:
- th:alt-title will set alt and title.
- th:lang-xmllang will set lang and xml:lang.
5.4 追加和追加在前面
Thymeleaf还提供th:attrappend和th:attrprepend属性,它们将对现有属性值求值的结果追加(后缀)或prepend(前缀)。
例如,您可能想要将要添加(不是设置,只是添加)的CSS类的名称存储在上下文变量中的一个按钮中,因为要使用的特定CSS类将取决于用户以前所做的事情:
<input type="button" value="Do it!" class="btn" th:attrappend="class=${' ' + cssStyle}" />
5.5 布尔值属性
HTML有布尔属性的概念,没有值的属性,一个值的前缀表示值为“true”。在XHTML中,这些属性只取1个值,也就是它本身。
<input type="checkbox" name="option2" checked /> <!-- HTML -->
标准方言包括允许您通过计算条件来设置这些属性的属性,因此,如果计算为true,属性将被设置为其固定值,如果计算为false,属性将不会被设置:
<input type="checkbox" name="active" th:checked="${user.active}" />
5.6 设置任意属性的值
Thymeleaf提供了一个默认的属性处理器,允许我们设置任何属性的值,即使在标准方言中没有为它定义特定的th:*处理器。
<span th:whatever="${user.name}">...</span>
将会导致
<span whatever="John Apricot">...</span>
5.7 支持HTML5属性
还可以使用一种完全不同的语法,以一种更加html友好的方式将处理器应用到模板中。
<table>
<tr data-th-each="user : ${users}">
<td data-th-text="${user.login}">...</td>
<td data-th-text="${user.name}">...</td>
</tr>
</table>
data-{prefix}-{name}语法是HTML5中编写定制属性的标准方法,不要求开发人员使用任何名称空间名称,如th:*。Thymeleaf使这种语法自动适用于所有方言(不仅仅是标准方言)。
还有一种语法可以指定定制标记:{prefix}-{name},它遵循W3C定制元素规范(W3C Web组件规范的一部分)。例如,这可以用于th:block元素(或者也可以用于th块),后面的部分将对此进行解释。
重要提示:这个语法是对名称空间th:* one的补充,它不会替换它。将来完全不打算废弃带有名称空间的语法。
6 迭代
到目前为止,我们已经创建了一个主页、一个用户个人资料页面以及一个允许用户订阅我们的时事通讯的页面……但是我们的产品呢?为此,我们将需要一种迭代集合中的项的方法来构建我们的产品页面。
6.1 基础迭代
th:each
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
上面看到的prod: ${prods}属性值的意思是“对于计算${prods}的结果中的每个元素,使用名为prod的变量中的当前元素重复这个模板片段”。让我们给我们看到的每样东西命名:
- We will call ${prods} the iterated expression or iterated variable.
- We will call prod the iteration variable or simply iter variable.
可以被迭代的值
java.util。List类不是Thymeleaf中惟一可以用于迭代的值。有一个相当完整的对象集,被th:each属性认为是可迭代的:
- Any object implementing java.util.Iterable
- Any object implementing java.util.Enumeration.
- Any object implementing java.util.Iterator, whose values will be used as they are returned by the iterator, without the need to cache all values in memory.
- Any object implementing java.util.Map. When iterating maps, iter variables will be of class java.util.Map.Entry.
- Any array.
- Any other object will be treated as if it were a single-valued list containing the object itself.
6.2 迭代状态
在使用th:each时,Thymeleaf提供了一种用于跟踪迭代状态的机制:状态变量。
状态变量定义在th:每个属性中,包含以下数据:
- The current iteration index, starting with 0. This is the index property.
- The current iteration index, starting with 1. This is the count property.
- The total amount of elements in the iterated variable. This is the size property.
- The iter variable for each iteration. This is the current property.
- Whether the current iteration is even or odd. These are the even/odd boolean properties.
- Whether the current iteration is the first one. This is the first boolean property.
- Whether the current iteration is the last one. This is the last boolean property.
让我们看看如何在前面的例子中使用它:
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
状态变量(本例中为iterStat)在th:each属性中定义,方法是在iter变量本身后面写入其名称,中间用逗号分隔。就像iter变量一样,status变量的作用域也限定在包含th:each属性的标记所定义的代码片段中。
如果您没有显式地设置状态变量,Thymeleaf将始终为您创建一个状态变量,方法是将Stat添加到迭代变量的名称后面:
6.3 延迟加载
有时我们可能希望优化数据集合的检索(例如从数据库中检索),以便只有在真正要使用这些集合时才检索这些集合。
实际上,这可以应用于任何数据块,但是考虑到内存集合可能具有的大小,检索要迭代的集合是本场景中最常见的情况。
为了支持这一点,Thymeleaf提供了一种延迟加载上下文变量的机制。实现ILazyContextVariable接口的上下文变量——很可能是通过扩展其LazyContextVariable默认实现实现的——将在执行时解析。例如:
context.setVariable(
"users",
new LazyContextVariable<List<User>>() {
@Override
protected List<User> loadValue() {
return databaseRepository.findAllUsers();
}
});
这个变量可以在不知道它是否懒惰的情况下使用,代码如下:
<li th:each="u : ${users}" th:text="${u.name}">user name</li>
但同时,如果条件在代码中计算为false,则永远不会被初始化(它的loadValue()方法将永远不会被调用),例如:
<li th:each="u : ${users}" th:text="${u.name}">user name</li>
7 条件表达式
7.1 简单条件:if 和 unless
有时候,您需要模板的一个片段只出现在满足特定条件的结果中。
例如,假设我们希望在product表中显示一列,其中包含每个产品的评论数量,如果有任何评论,则显示到该产品的评论详细信息页面的链接。
为了做到这一点,我们将使用th:if属性:
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
这里有很多东西值得一看,所以让我们来看看这条重要的线:
这将创建一个到comments页面的链接(带有URL /product/comments),并将prodId参数设置为产品的id,但只有在产品有任何注释时才会这样做。
让我们来看看结果标记:
<a href="/gtvg/product/comments?prodId=2">view</a>
请注意,th:if属性不仅计算布尔条件。它的功能稍微超出了这一范围,它将按照以下规则计算指定的表达式为true:
- If value is a boolean and is true.
- If value is a number and is non-zero
- If value is a character and is non-zero
- If value is a String and is not “false”, “off” or “no”
- If value is not a boolean, a number, a character or a String.
(If value is null, th:if will evaluate to false).
同样,th:if有一个逆属性,th:unless,我们可以在前面的例子中使用,而不是在OGNL表达式中使用not:
th:unless="${#lists.isEmpty(prod.comments)}">view</a>
7.2 Switch表达式
还有一种方法可以有条件地显示内容,它使用Java中等价的交换结构:th:switch / th:case属性集。
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
</div>
请注意,一旦第th:case属性的值为true,同一切换上下文中的每一个其他th:case属性的值都为false。
默认选项指定为th:case="*":
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>
8 布局模板
8.1 引用模板
定义和引用模板
在我们的模板中,我们通常希望包含来自其他模板的部分,比如页脚、页眉、菜单……
为了做到这一点,Thymeleaf需要我们为包含定义这些部分,“fragments”,这可以使用th:fragment属性来完成。
假设我们想为所有的杂货页面添加一个标准的版权页脚,因此我们创建了一个/WEB-INF/templates/footer.html文件,其中包含以下代码:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
</html>
上面的代码定义了一个名为copy的片段,我们可以使用th:insert或th:replace属性(以及th:include,不过自Thymeleaf 3.0以来不再推荐使用它)轻松地将其包含在主页中:
<body>
...
<div th:insert="~{footer :: copy}"></div>
</body>
注意,th:insert需要一个片段表达式(~{…}),这是一个产生片段的表达式。在上面的例子中,这是一个非复杂的片段表达式,(~{,})的封装是完全可选的,所以上面的代码相当于:
<body>
...
<div th:insert="footer :: copy"></div>
</body>
模板规格
片段表达式的语法非常简单。有三种不同的格式:
"~{templatename::selector}"
包含在名为templatename的模板上应用指定标记选择器生成的片段。注意,选择器可以仅仅是一个片段名称,所以您可以像上面的~{footer:: copy}中那样指定像~{templatename::fragmentname}这样简单的东西。
标记选择器语法由底层的AttoParser解析库定义,类似于XPath表达式或CSS选择器。有关更多信息,请参见附录C。
"~{templatename}"
包含名为templatename的完整模板。
注意,在th:insert/th:replace标记中使用的模板名称必须由模板引擎当前使用的模板解析器解析。
~{::selector}"
or "~{this::selector}"
插入来自相同模板的片段,匹配选择器。如果在表达式出现的模板上没有找到,则会将模板调用(插入)的堆栈遍历到原始处理的模板(根),直到选择器在某个级别上匹配为止。
上面例子中的templatename和selector都可以是全功能的表达式(甚至是条件语句!)
<div th:insert="footer :: (${user.isAdmin}? #{footer.admin} : #{footer.normaluser})"></div>
片段可以包括任何th:*属性。一旦片段被包含到目标模板中(具有th:insert/th:replace属性的那个),这些属性将被计算,并且它们将能够引用在这个目标模板中定义的任何上下文变量。
这种片段处理方法的一大优点是,您可以将片段编写在浏览器完全可显示的页面中,页面具有完整甚至有效的标记结构,同时仍然能够使Thymeleaf将它们包含到其他模板中。
不使用 th:fragment
引用模板
由于标记选择器的强大功能,我们可以包含不使用任何th:fragment属性的片段。它甚至可以是来自完全不了解Thymeleaf的另一个应用程序的标记代码:
<div id="copy-section">
© 2011 The Good Thymes Virtual Grocery
</div>
我们可以使用上面的片段简单地引用它的id属性,以类似于CSS选择器的方式:
<body>
...
<div th:insert="~{footer :: #copy-section}"></div>
</body>
th:insert
th:replace
(and th:include
)区别
th:insert和th:replace(以及th:include, 3.0以后就不推荐了)之间的区别是什么?
th:insert
是最简单的:它将简单地插入指定的片段作为其宿主标记的主体。
T,h:replace 实际上用指定的片段替换其主机标记。
th:include 类似于th:insert,但它只插入片段的内容,而不是插入片段。
像这样的HTML片段:
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
Insert
<div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
Replace
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
Include
<div>
© 2011 The Good Thymes Virtual Grocery
</div>
8.2 模板参数
为了为模板片段创建更类似函数的机制,使用th:fragment定义的片段可以指定一组参数:
<div th:fragment="frag (onevar,twovar)">
<p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>
这需要使用这两种语法中的一种来调用来自th:insert或th:replace的片段:
<div th:replace="::frag (${value1},${value2})">...</div>
<div th:replace="::frag (onevar=${value1},twovar=${value2})">...</div>
注意最后一个选项中的顺序并不重要:
没有片段参数的片段局部变量
即使没有这样的参数来定义片段:
<div th:fragment="frag">
...
</div>
我们可以使用上面指定的第二种语法来调用它们(只有第二种):
<div th:replace="::frag (onevar=${value1},twovar=${value2})">
这将等价于th:replace和th:with:的组合:
<div th:replace="::frag" th:with="onevar=${value1},twovar=${value2}">
请注意,片段的这种局部变量规范——无论它是否具有参数签名——不会导致在执行之前清空上下文。片段仍然能够像当前一样访问调用模板中使用的每个上下文变量。
模板内断言
断言属性可以指定一个以逗号分隔的表达式列表,这些表达式应该被求值,并为每个求值生成true,如果不是,则引发异常。
<div th:assert="${onevar},(${twovar} != 43)">...</div>
这对于验证片段签名的参数很方便:
<header th:fragment="contentheader(title)" th:assert="${!#strings.isEmpty(title)}">...</header>
8.3 灵活的布局:不仅仅是片段插入
多亏了片段表达式,我们可以为片段指定参数,这些片段不是文本、数字、bean对象……而是标记的片段。
这允许我们以这样一种方式创建片段,通过调用模板中的标记来丰富片段,从而形成非常灵活的模板布局机制。
注意下面片段中标题和链接变量的使用:
<head th:fragment="common_header(title,links)">
<title th:replace="${title}">The awesome application</title>
<!-- Common styles and scripts -->
<link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
<link rel="shortcut icon" th:href="@{/images/favicon.ico}">
<script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>
<!--/* Per-page placeholder for additional links */-->
<th:block th:replace="${links}" />
</head>
We can now call this fragment like:
...
<head th:replace="base :: common_header(~{::title},~{::link})">
<title>Awesome - Main</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>
...
结果将使用我们调用模板中实际的
...
<head>
<title>Awesome - Main</title>
<!-- Common styles and scripts -->
<link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
<link rel="shortcut icon" href="/awe/images/favicon.ico">
<script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>
<link rel="stylesheet" href="/awe/css/bootstrap.min.css">
<link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css">
</head>
...
使用空模板
一个特殊的片段表达式,空片段(~{}),可以用来指定没有标记。使用前面的例子:
<head th:replace="base :: common_header(~{::title},~{})">
<title>Awesome - Main</title>
</head>
注意片段(链接)的第二个参数是如何设置为空片段的,因此没有为块编写任何内容:
...
<head>
<title>Awesome - Main</title>
<!-- Common styles and scripts -->
<link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
<link rel="shortcut icon" href="/awe/images/favicon.ico">
<script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>
</head>
...
使用_操作符
如果我们只想让片段使用其当前标记作为默认值,那么no-op也可以用作片段的参数。再次使用common_header示例:
...
<head th:replace="base :: common_header(_,~{::link})">
<title>Awesome - Main</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>
...
查看title参数(common_header片段的第一个参数)是如何设置为no-op(_)的,这将导致片段的这一部分根本不执行(title = no-operation):
<title th:replace="${title}">The awesome application</title>
所以结果是:
...
<head>
<title>The awesome application</title>
<!-- Common styles and scripts -->
<link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
<link rel="shortcut icon" href="/awe/images/favicon.ico">
<script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>
<link rel="stylesheet" href="/awe/css/bootstrap.min.css">
<link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css">
</head>
...
模板的高级条件引用
emtpy片段和无操作标记的可用性允许我们以一种非常简单和优雅的方式执行片段的条件插入。
例如,我们可以只在用户是管理员的情况下插入我们的common:: adminhead片段,如果用户不是管理员,则不插入任何内容(emtpy片段):
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : ~{}">...</div>
无操作标记,以便仅在满足指定条件时插入片段,但如果不满足条件,则保留标记不做任何修改:
...
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : _">
Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support.
</div>
...
此外,如果我们配置了模板解析器来检查模板资源是否存在——通过它们的checkExistence标志——我们可以使用片段本身的存在作为默认操作的条件:
...
<!-- The body of the <div> will be used if the "common :: salutation" fragment -->
<!-- does not exist (or is empty). -->
<div th:insert="~{common :: salutation} ?: _">
Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support.
</div>
...
8.4 删除多余的静态html代码
回到示例应用程序,让我们重温产品列表模板的最后一个版本:
<tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
这段代码作为模板很好,但是作为静态页面(当浏览器直接打开而不需要Thymeleaf处理它时)就不是一个很好的原型。
为什么?因为,尽管浏览器可以完美地显示该表,但该表只有一行,而这一行有模拟数据。作为一个原型,它看起来不够真实……我们应该有多个产品,我们需要更多行。
所以让我们添加一些:
我们需要一种方法在模板处理期间删除这两行。
让我们使用th:remove第二个和第三个<tr>标记上的属性:
<tr class="odd" th:remove="all">
属性中的所有值是什么意思呢?th:remove可以有五种不同的行为,这取决于它的值:
- all: Remove both the containing tag and all its children.
- body: Do not remove the containing tag, but remove all its children.
- tag: Remove the containing tag, but do not remove its children.
- all-but-first: Remove all children of the containing tag except the first one.
- none : Do nothing. This value is useful for dynamic evaluation.
这个几乎是最重要的价值有什么用呢?它将让我们保存一些th:remove="all"当原型:
第th:remove属性可以接受任何Thymeleaf标准表达式,只要它返回一个允许的字符串值(all、tag、body、all-but-first或none)。
这意味着移除可以是有条件的,比如:
<a href="/something" th:remove="${condition}? tag : none">Link text not to be removed</a>
还要注意,th:remove认为null是none的同义词,所以下面的工作与上面的示例相同:
<a href="/something" th:remove="${condition}? tag">Link text not to be removed</a>
在这种情况下,如果${condition}为false,将返回null,因此不会执行删除操作。
8.5 布局继承
为了能够使用单个文件作为布局,可以使用片段。一个简单的布局的例子,标题和内容使用th:fragment和th:replace:
<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
<title th:replace="${title}">Layout Title</title>
</head>
<body>
<h1>Layout H1</h1>
<div th:replace="${content}">
<p>Layout content</p>
</div>
<footer>
Layout footer
</footer>
</body>
</html>
这个示例声明了一个名为layout的片段,其中标题和内容作为参数。下面的示例中提供的片段表达式将在继承它的页面上替换这两个表达式。
<!DOCTYPE html>
<html th:replace="~{layoutFile :: layout(~{::title}, ~{::section})}">
<head>
<title>Page Title</title>
</head>
<body>
<section>
<p>Page content</p>
<div>Included on page</div>
</section>
</body>
</html>
在这个文件中,html标记将被layout替换,但是在布局中标题和内容将分别被title和section块替换。
如果需要,布局可以由几个片段组成,如页眉和页脚。
9 本地参数
Thymeleaf调用局部变量,这些变量是为模板的特定片段定义的,并且仅可用于该片段内的求值。
我们已经看到的一个例子是产品列表页面中的prod iter变量:
<tr th:each="prod : ${prods}">
...
</tr>
该prod变量仅在<tr>标记的范围内可用。明确地:
对于在该标记中执行的优先级低于th:each的任何其他th:*属性,它都是可用的(这意味着它们将在th:each之后执行)。
该prod变量仅在<tr>标记的范围内可用。明确地:
Thymeleaf提供了一种无需迭代就可以声明局部变量的方法,使用th:with属性,其语法类似于属性值赋值:
<
<div th:with="firstPer=${persons[0]}">
<p>
The name of the first person is <span th:text="${firstPer.name}">Julius Caesar</span>.
</p>
</div>
当th:with被处理时,firstPer变量被创建为一个局部变量,并添加到来自上下文的变量映射中,这样它就可以与上下文中声明的任何其他变量一起进行计算,但只能在包含标记的范围内。
您可以使用通常的多重赋值语法同时定义多个变量:
<div th:with="firstPer=${persons[0]},secondPer=${persons[1]}">
<p>
The name of the first person is
<span th:text="${firstPer.name}">Julius Caesar</span>.
</p>
<p>
But the name of the second person is
<span th:text="${secondPer.name}">Marcus Antonius</span>.
</p>
</div>
第th:with属性允许重用在同一个属性中定义的变量:
<div th:with="company=${user.company + ' Co.'},account=${accounts[company]}">...</div>
让我们用这个在我们的杂货店的主页上!还记得我们编写的输出格式化日期的代码吗?
<p>
Today is:
<span th:text="${#calendars.format(today,'dd MMMM yyyy')}">13 february 2011</span>
</p>
现在,让我们使用th:with将本地化的日期格式转换为变量,然后在th:text表达式中使用它:
<p th:with="df=#{date.format}">
Today is: <span th:text="${#calendars.format(today,df)}">13 February 2011</span>
</p>
这既干净又简单。事实上,考虑到th:with的优先级高于th:text,我们可以在span标记中解决所有问题:
<p>
Today is:
<span th:with="df=#{date.format}"
th:text="${#calendars.format(today,df)}">13 February 2011</span>
</p>
你可能会想:优先?我们还没说呢!别担心,这正是下一章的内容。
10属性优先级
当您在同一个标记中编写多个th:*属性时会发生什么?例如:
<li th:each="item : ${items}" th:text="${item.description}">Item description here...</li>
我们希望th:每个属性执行之前th:文本,这样我们得到我们想要的结果,但考虑到HTML / XML标准不给任何意义的顺序编写一个标签的属性,属性的优先级机制必须建立自己为了确保这将正常工作。
因此,所有的Thymeleaf属性都定义了一个数字优先级,它确定了它们在标记中执行的顺序。这个顺序是:
Order | Feature | Attributes |
1 | Fragment inclusion | th:insert |
2 | Fragment iteration | th:each |
3 | Conditional evaluation | th:if |
4 | Local variable definition | th:object |
5 | General attribute modification | th:attr |
6 | Specific attribute modification | th:value |
7 | Text (tag body modification) | th:text |
8 | Fragment specification | th:fragment |
9 | Fragment removal | th:remove |
这种优先机制意味着,如果属性位置颠倒,上面的迭代片段将给出完全相同的结果(尽管它的可读性稍微差一些):
11 注释
11.1. 标志HTML/XML注释
标准HTML/XML注释<!——……——>可以在Thymeleaf模板的任何地方使用。这些评论中的任何内容都不会被Thymeleaf处理,并将逐字复制到结果:
<!-- User info follows -->
<div th:text="${...}">
...
</div>
11.2. Thymeleaf 注释模块
解析器级别的注释块是在Thymeleaf解析模板时从模板中简单删除的代码。它们是这样的:
<!--/* This code will be removed at Thymeleaf parsing time! */-->
Thymeleaf将删除<!——/*和*/——>,所以这些注释块也可以用于静态打开模板时显示代码,知道当Thymeleaf处理它时,它会被删除:
<!--/*-->
<div>
you can see me only before Thymeleaf processes me!
</div>
<!--*/-->
11.3. Thymeleaf原型注释
Thymeleaf允许在静态打开模板时(即作为原型)将特殊的注释块定义为注释,但在执行模板时将其视为普通标记。
<span>hello!</span>
<!--/*/
<div th:text="${...}">
...
</div>
/*/-->
<span>goodbye!</span>
Thymeleaf的解析系统将简单地删除<!—/*/和/*/—>标记,但不包括其内容,因此将不注释这些内容。所以在执行模板时,Thymeleaf会看到:
<span>hello!</span>
<div th:text="${...}">
...
</div>
<span>goodbye!</span>
与解析器级别的注释块一样,这个特性与方言无关。
11.4.th:block
标准方言中包含的惟一元素处理器(不是属性)是th:block。
block仅仅是一个属性容器,它允许模板开发人员指定他们想要的任何属性。Thymeleaf将执行这些属性,然后使块(而不是其内容)消失。
因此,它可能很有用,例如,当创建迭代表时,每个元素需要一个以上的<tr>
<table>
<th:block th:each="user : ${users}">
<tr>
<td th:text="${user.login}">...</td>
<td th:text="${user.name}">...</td>
</tr>
<tr>
<td colspan="2" th:text="${user.address}">...</td>
</tr>
</th:block>
</table>
当与仅原型的注释块结合使用时尤其有用:
<table>
<!--/*/ <th:block th:each="user : ${users}"> /*/-->
<tr>
<td th:text="${user.login}">...</td>
<td th:text="${user.name}">...</td>
</tr>
<tr>
<td colspan="2" th:text="${user.address}">...</td>
</tr>
<!--/*/ </th:block> /*/-->
</table>
注意这个解决方案是如何允许模板是有效的HTML(不需要在中添加禁止的
块),并且在浏览器中作为原型静态打开时仍然可以正常工作!
12 Thymeleaf嵌入
12.1 表达式嵌入
尽管标准方言允许我们使用标记属性做几乎所有的事情,但是在某些情况下,我们更喜欢直接将表达式写入HTML文本。例如,我们可以这样写:
<p>Hello, [[${session.user.name}]]!</p>
……不是这样的:
<p>Hello, <span th:text="${session.user.name}">Sebastian</span>!</p>
之间的表达式[[…]]或[(…)]在Thymeleaf中被认为是内联表达式,在它们内部,我们可以使用在th:text或th:utext属性中也有效的任何类型的表达式。
注意,然而[[……]]对应于th:text(即结果为html转义),[(…)]对应于th:utext,不执行任何html转义。因此,对于msg = '这是这样的变量,很好!',给定这个片段:
<p>The message is "[(${msg})]"</p>
嵌入vs非嵌入
如果您来自以这种方式输出文本为标准的其他模板引擎,您可能会问:为什么我们不从一开始就这样做呢?它的代码比所有那些th:text属性都要少!
那么,在这里要小心,因为尽管您可能会发现内联非常有趣,但是您应该始终记住,当静态打开内联表达式时,它们将在HTML文件中逐字显示,所以您可能再也不能将它们用作设计原型了!
浏览器如何在不使用内联的情况下静态显示我们的代码片段……
Hello, Sebastian!
- 并利用这
Hello, [[${session.user.name}]]!
就设计的实用性而言,……是相当清楚的。
禁止嵌入
但是可以禁用此机制,因为在某些情况下,我们确实希望输出[[…]]或[(…)]序列,但不将其内容作为表达式处理。为此,我们将使用th:inline="none":
<p th:inline="none">A double array looks like this: [[1, 2, 3], [4, 5]]!</p>
12.2 文本嵌入
文本内联非常类似于我们刚才看到的表达式内联功能,但是它实际上增加了更多的功能。它必须显式启用th:inline="text"。
文本内联不仅允许我们使用我们刚才看到的相同的内联表达式,而且实际上还可以像处理文本模板模式中的模板那样处理标记体,这允许我们执行基于文本的模板逻辑(不仅仅是输出表达式)。
我们将在下一章的文本模板模式中看到更多关于这一点的内容。
12.3 JavaScript 嵌入
JavaScript内联允许在HTML模板模式下处理的模板中更好地集成JavaScript
与文本内联一样,这实际上相当于在JAVASCRIPT模板模式下像处理模板一样处理脚本内容,因此文本模板模式的所有功能(参见下一章)就在手边。然而,在本节中,我们将重点介绍如何使用它将Thymeleaf表达式的输出添加到JavaScript块中。
这个模式必须显式启用使用th:inline="javascript":
<script th:inline="javascript">
...
var username = [[${session.user.name}]];
...
</script>
这将导致
<script th:inline="javascript">
...
var username = "Sebastian \"Fruity\" Applejuice";
...
</script>
在上面的代码中需要注意两件重要的事情:
首先,JavaScript内联不仅输出所需的文本,还用引号和JavaScript—转义其内容—将表达式结果作为格式良好的JavaScript文本输出。
其次,这是因为我们将${session.user.name}表达式作为转义输出,即使用一个双尖括号表达式:[[${session.user.name}]]。如果我们使用unescape:
<script th:inline="javascript">
...
var username = [(${session.user.name})];
...
</script>
结果会是这样的:
<script th:inline="javascript">
...
var username = Sebastian "Fruity" Applejuice;
...
</script>
这是格式错误的JavaScript代码。但是,如果要通过追加内联表达式来构建脚本的某些部分,输出未转义的内容可能是我们所需要的,所以手边有这个工具是很好的。
JavaScript 非嵌入写法
前面提到的JavaScript内联机制的智能远远不止应用JavaScript特有的转义和将表达式结果作为有效的文本输出。
例如,我们可以将(转义)内联表达式包装在JavaScript注释中,如:
<script th:inline="javascript">
...
var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
...
</script>
而Thymeleaf会忽略我们在注释后面和分号之前写的所有东西(在本例中是“Gertrud Kiwifruit”),所以执行这个的结果看起来就像我们没有使用包装注释的时候:
<script th:inline="javascript">
...
var username = "Sebastian \"Fruity\" Applejuice";
...
</script>
但是再仔细看看原始模板代码:
<script th:inline="javascript">
...
var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
...
</script>
注意这是有效的JavaScript代码。当您以静态方式(而不是在服务器上执行)打开模板文件时,它将完美地执行。
所以我们这里有一种做JavaScript自然模板的方法!
嵌入求值和js序列号
关于JavaScript内联需要注意的一件重要事情是,这个表达式的求值是智能的,不限于字符串。Thymeleaf将正确地用JavaScript语法编写以下类型的对象:
- Strings
- Numbers
- Booleans
- Arrays
- Collections
- Maps
- Beans (objects with getter and setter methods)
例如,如果我们有以下代码:
<script th:inline="javascript">
...
var user = /*[[${session.user}]]*/ null;
...
</script>
$ {session.user}表达式将求值为user对象,Thymeleaf将其正确转换为Javascript语法:
<script th:inline="javascript">
...
var user = {"age":null,"firstName":"John","lastName":"Apricot",
"name":"John Apricot","nationality":"Antarctica"};
...
</script>
这种JavaScript序列化的实现方式是通过org.thymeleaf.standard.serializer的实现实现的。IStandardJavaScriptSerializer接口,可以在模板引擎使用的标准方言的实例中配置该接口。
这个JS序列化机制的默认实现将在类路径中查找Jackson库,如果存在,将使用它。如果不是,它将应用一个内置的序列化机制,该机制覆盖大多数场景的需求,并产生类似的结果(但灵活性较差)。
12.4 CSS嵌入
Thymeleaf还允许在CSS
<style th:inline="css">
...
</style>
例如,我们有两个变量被设置为两个不同的字符串值:
classname = 'main elems'
align = 'center'
我们可以这样使用它们:
<style th:inline="css">
.[[${classname}]] {
text-align: [[${align}]];
}
</style>
结果会是:
<style th:inline="css">
.main\ elems {
text-align: center;
}
</style>
注意CSS内联也有一些智能,就像JavaScript一样。具体地说,通过转义表达式(如[[${classname}]])输出的表达式将转义为CSS标识符。这就是为什么我们的classname = 'main elems'在上面的代码片段中变成了main\ elems。
高级特性
与之前对javascript的解释类似,CSS内联还允许我们的<style>标记静态和动态地工作,即通过将内联表达式包装在注释中作为CSS自然模板。见:
<style th:inline="css">
.main\ elems {
text-align: /*[[${align}]]*/ left;
}
</style>
13 文本模式
13.1 文本语法
三种Thymeleaf模板模式被认为是文本模式:文本、JAVASCRIPT和CSS。这将它们与标记模板模式(HTML和XML)区分开来。
文本模板模式和标记模式之间的关键区别在于,在文本模板中没有以属性形式插入逻辑的标记,因此我们必须依赖其他机制。
第一个也是最基本的机制是内联,我们在上一章已经详细介绍过了。内联语法是在文本模板模式下输出表达式结果的最简单方法,因此这是一个非常有效的文本电子邮件模板。
Dear [(${name})],
Please find attached the results of the report you requested
with name "[(${report.name})]".
Sincerely,
The Reporter.
即使没有标记,上面的示例也是一个完整而有效的Thymeleaf模板,可以在文本模板模式下执行。
但是为了包含比输出表达式更复杂的逻辑,我们需要一种新的非基于标记的语法:
[# th:each="item : ${items}"]
- [(${item})]
[/]
这实际上是更详细的压缩版本:
[#th:block th:each="item : ${items}"]
- [#th:block th:utext="${item}" /]
[/th:block]
注意这个新语法是如何基于声明为[#element…而不是。元素是开放的,比如[#元素…]和像[/element]一样关闭的标签,独立标签可以通过使用/最小化打开的元素来声明,其方式几乎等同于XML标签:[#element…/)。
标准方言只包含这些元素之一的处理器:已知的th:块,尽管我们可以在方言中扩展它并以通常的方式创建新元素。另外,第th:block元素([#th:block…]))……[/th:block])可以缩写为空字符串([#…)……[/]),所以上面的block实际上等于:
[# th:each="item : ${items}"]
- [# th:utext="${item}" /]
[/]
假设[# th:utext="${item}" /]相当于一个内联的未转义表达式,我们可以使用它来减少代码。这样我们就得到了上面看到的第一个代码片段:
[# th:each="item : ${items}"]
- [(${item})]
[/]
注意,文本语法需要完全的元素平衡(没有未关闭的标记)和引号属性——它更像xml风格,而不是html风格。
让我们来看看一个更完整的文本模板的例子,一个纯文本电子邮件模板:
Dear [(${customer.name})],
This is the list of our products:
[# th:each="prod : ${products}"]
- [(${prod.name})]. Price: [(${prod.price})] EUR/kg
[/]
Thanks,
The Thymeleaf Shop
执行之后,结果可能是这样的:
Dear Mary Ann Blueberry,
This is the list of our products:
- Apricots. Price: 1.12 EUR/kg
- Bananas. Price: 1.78 EUR/kg
- Apples. Price: 0.85 EUR/kg
- Watermelon. Price: 1.91 EUR/kg
Thanks,
The Thymeleaf Shop
另一个JAVASCRIPT模板模式的例子是一个greeting .js文件,我们将其作为文本模板处理,并从HTML页面调用结果。注意,这不是HTML模板中的
var greeter = function() {
var username = [[${session.user.name}]];
[# th:each="salut : ${salutations}"]
alert([[${salut}]] + " " + username);
[/]
};
执行之后,结果可能是这样的:
var greeter = function() {
var username = "Bertrand \"Crunchy\" Pear";
alert("Hello" + " " + username);
alert("Ol\u00E1" + " " + username);
alert("Hola" + " " + username);
};
忽略元素属性
为了避免与可能以其他模式(例如HTML模板中的文本模式内联)处理的模板部分进行交互,Thymeleaf 3.0允许转义文本语法中元素的属性。所以:
- Attributes in TEXT template mode will be HTML-unescaped.
- Attributes in JAVASCRIPT template mode will be JavaScript-unescaped.
- Attributes in CSS template mode will be CSS-unescaped.
所以这在文本模式模板中是完全可以的(注意>):
[# th:if="${120<user.age}"]
Congratulations!
[/]
当然了。这在实际的文本模板中是没有意义的,但是如果我们使用th:inline="text"块处理HTML模板(其中包含上述代码),并且希望确保浏览器不接受
13.2扩展
这种语法的优点之一是它与标记语法一样具有可扩展性。开发人员仍然可以使用自定义元素和属性定义自己的方言,对它们应用前缀(可选),然后在文本模板模式中使用它们:
[#myorg:dosomething myorg:importantattr="211"]some text[/myorg:dosomething]
13.3 原型注释
JAVASCRIPT和CSS模板模式(文本不可用)允许在特殊的注释语法/*[+…+]*/使Thymeleaf在处理模板时自动取消注释:
var x = 23;
/*[+
var msg = "This is a working application";
+]*/
var f = function() {
...
13.4 文本解析器级注释块:删除代码
与仅使用原型的注释块类似,所有三种文本模板模式(文本、JAVASCRIPT和CSS)都可以指示Thymeleaf删除特殊/*[- */和/* -]*/标记之间的代码,如下所示:
13.5 JavaScript and CSS 模板
如上一章所述,JavaScript和CSS内联提供了在JavaScript/CSS注释中包含内联表达式的可能性,例如:
…这是有效的JavaScript,一旦执行可能像:
...
var username = "John Apricot";
...
在注释中封装内联表达式的相同技巧实际上可以用于整个文本模式语法:
/*[# th:if="${user.admin}"]*/
alert('Welcome admin');
/*[/]*/
当模板静态打开时(因为它是100%有效的JavaScript),以及当模板运行时(如果用户是管理员),上面代码中的警告将显示出来。它等于:
[# th:if="${user.admin}"]
alert('Welcome admin');
[/]
…这实际上是在模板解析期间将初始版本转换为的代码。
但是请注意,在注释中包装元素并不会清除它们所在的行(在a;如内联输出表达式所做的那样。这种行为只保留给内联输出表达式。
因此,Thymeleaf 3.0允许以自然模板的形式开发复杂的JavaScript脚本和CSS样式表,作为原型和工作模板都有效。
14杂货店(略)
15 配置
15.1 模板解析器
为了我们的Thymes虚拟杂货店,我们选择了一个名为servletcontext的ITemplateResolver实现,它允许我们从Servlet上下文中获取模板作为资源。
除了让我们能够通过实现ITemplateResolver来创建自己的模板解析器之外,Thymeleaf还提供了四个开箱即用的实现:
org.thymeleaf.templateresolver。ClassLoaderTemplateResolver,它将模板解析为类加载器资源,如:
return Thread.currentThread().getContextClassLoader().getResourceAsStream(template);
org.thymeleaf.templateresolver。FileTemplateResolver,它将模板解析为文件系统中的文件,如:
return new FileInputStream(new File(template));
org.thymeleaf.templateresolver。UrlTemplateResolver,它将模板解析为url(甚至是非本地的),比如:
return (new URL(template)).openStream();
org.thymeleaf.templateresolver。StringTemplateResolver,它直接将模板解析为指定为模板的字符串(或模板名称,在本例中显然不仅仅是一个名称):
return new StringReader(templateName);
ITemplateResolver的所有预绑定实现都允许相同的一组配置参数,其中包括:
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
允许使用与文件名不直接对应的模板名称的emplate别名。如果前缀/前缀和别名都存在,则在前缀/后缀之前应用别名:
templateResolver.addTemplateAlias("adminHome","profiles/admin/home");
templateResolver.setTemplateAliases(aliasesMap);
读取模板时应用的编码:
templateResolver.setEncoding("UTF-8");
使用的模板模式:
// Default is HTML
templateResolver.setTemplateMode("XML");
模板缓存的默认模式,以及定义特定模板是否可缓存的模式:
// Default is true
templateResolver.setCacheable(false);
templateResolver.getCacheablePatternSpec().addPattern("/users/*");
TTL(以毫秒为单位)表示源自此模板解析器的已解析模板缓存项。如果没有设置,从缓存中删除条目的唯一方法是超过缓存的最大大小(最老的条目将被删除)。
// Default is no TTL (only cache size exceeded would remove entries)
templateResolver.setCacheTTLMs(60000L);
Thymeleaf + Spring集成包提供了一个SpringResourceTemplateResolver实现,它使用所有Spring基础设施来访问和读取应用程序中的资源,这是支持Spring的应用程序中推荐的实现。
模板解析器链
此外,模板引擎可以指定多个模板解析器,在这种情况下,可以在它们之间建立一个模板解析顺序,这样,如果第一个解析器无法解析模板,就会请求第二个解析器,以此类推:
ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
ServletContextTemplateResolver servletContextTemplateResolver =
new ServletContextTemplateResolver(servletContext);
servletContextTemplateResolver.setOrder(Integer.valueOf(2));
templateEngine.addTemplateResolver(classLoaderTemplateResolver);
templateEngine.addTemplateResolver(servletContextTemplateResolver);
当应用多个模板解析器时,建议为每个模板解析器指定模式,以便Thymeleaf能够快速丢弃那些不打算解析模板的模板解析器,从而提高性能。这样做不是一种要求,而是一种建议:
ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
// This classloader will not be even asked for any templates not matching these patterns
classLoaderTemplateResolver.getResolvablePatternSpec().addPattern("/layout/*.html");
classLoaderTemplateResolver.getResolvablePatternSpec().addPattern("/menu/*.html");
ServletContextTemplateResolver servletContextTemplateResolver =
new ServletContextTemplateResolver(servletContext);
servletContextTemplateResolver.setOrder(Integer.valueOf(2));
如果没有指定这些可解析模式,我们将依赖于所使用的ITemplateResolver实现的特定功能。请注意,并不是所有的实现都能够在解析之前确定模板的存在,因此总是可以认为模板是可解析的,并打破解析链(不允许其他解析器检查相同的模板),但随后无法读取真正的资源。
所有包含在核心Thymeleaf中的ITemplateResolver实现都包含一种机制,它允许我们在考虑可解析资源之前,让解析器真正检查资源是否存在。它是checkExistence标志,工作方式如下:
ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
classLoaderTempalteResolver.setCheckExistence(true);
这个checkExistence标志强制解析器在解析阶段对资源存在性执行真正的检查(如果exist check返回false,则调用链中的以下解析器)。虽然这听起来可能在所有情况下,在大多数情况下,这将意味着双重访问资源本身(一次检查存在,另一个时间阅读),和可能是一个性能问题在某些场景中,例如远程url模板资源——一个潜在的性能问题,可能无论如何被使用在很大程度上减轻模板缓存(在这种情况下, ,模板只能解决他们第一次访问)
15.2 消息解析器
我们没有为杂货应用程序显式地指定消息解析器实现,正如前面所解释的,这意味着所使用的实现是一个org.thymeleaf.messageresolver。StandardMessageResolver对象。
StandardMessageResolver是IMessageResolver接口的标准实现,但是我们可以根据应用程序的特定需求创建自己的接口。
默认情况下,Thymeleaf + Spring集成包提供一个IMessageResolver实现,它使用标准的Spring方式检索外部化的消息,方法是使用在Spring应用程序上下文中声明的MessageSource bean。
标准消息解析器
那么StandardMessageResolver如何查找特定模板中请求的消息呢?
如果模板名称是home并且位于/WEB-INF/templates/home中。,而请求的语言环境是gl_ES,那么这个解析器将在以下文件中查找消息,顺序如下:
- /WEB-INF/templates/home_gl_ES.properties
- /WEB-INF/templates/home_gl.properties
- /WEB-INF/templates/home.properties
有关完整的消息解析机制如何工作的详细信息,请参阅StandardMessageResolver类的JavaDoc文档。
配置消息解析器
如果我们想向模板引擎中添加消息解析器(或更多),该怎么办?容易:
// For setting only one
templateEngine.setMessageResolver(messageResolver);
// For setting more than one
templateEngine.addMessageResolver(messageResolver);
为什么我们希望有多个消息解析器?与模板解析器的原因相同:消息解析器是有序的,如果第一个解析器不能解析特定的消息,将询问第二个解析器,然后询问第三个解析器,等等。
15.3 转换服务
使我们能够通过双大括号语法(${{…}})执行数据转换和格式化操作的转换服务实际上是标准方言的一个特性,而不是Thymeleaf模板引擎本身的特性。
因此,配置它的方法是将IStandardConversionService接口的自定义实现直接设置到正在配置到模板引擎中的StandardDialect实例中。如:
IStandardConversionService customConversionService = ...
StandardDialect dialect = new StandardDialect();
dialect.setConversionService(customConversionService);
templateEngine.setDialect(dialect);
请注意,Thymeleaf -spring3和Thymeleaf -spring4包包含SpringStandardDialect,并且该方言已经预先配置了IStandardConversionService的实现,该实现将Spring自己的转换服务基础结构集成到Thymeleaf中。
15.4 日志
Thymeleaf非常关注日志记录,并且总是试图通过它的日志接口提供最大数量的有用信息。
所使用的日志库是slf4j,它实际上充当到我们可能希望在应用程序中使用的任何日志实现(例如log4j)的桥梁。
Thymeleaf类将记录跟踪、调试和信息级别的信息,这取决于我们想要的详细程度,除了一般的日志记录,它还将使用三个与TemplateEngine类相关联的特殊日志记录器,我们可以为不同的目的分别配置它们:
- org.thymeleaf.TemplateEngine.CONFIG will output detailed configuration of the library during initialization.
- org.thymeleaf.TemplateEngine.TIMER will output information about the amount of time taken to process each template (useful for benchmarking!)
- org.thymeleaf.TemplateEngine.cache is the prefix for a set of loggers that output specific information about the caches. Although the names of the cache loggers are configurable by the user and thus could change, by default they are:
- org.thymeleaf.TemplateEngine.cache.TEMPLATE_CACHE
- org.thymeleaf.TemplateEngine.cache.EXPRESSION_CACHE
使用log4j的Thymeleaf日志记录基础结构的一个示例配置可以是:
log4j.logger.org.thymeleaf=DEBUG
log4j.logger.org.thymeleaf.TemplateEngine.CONFIG=TRACE
log4j.logger.org.thymeleaf.TemplateEngine.TIMER=TRACE
log4j.logger.org.thymeleaf.TemplateEngine.cache.TEMPLATE_CACHE=TRACE
16 模板缓存
Thymeleaf工作由于一组解析器——标记和文本模板解析成的事件序列(开放标签、文本结束标记、评论等)和一系列的处理器——一个为每个类型的行为,需要应用,修改模板解析事件序列为了创建我们期待的结果。通过结合原始模板与我们的数据。
默认情况下,它还包括存储已解析模板的缓存;在处理模板文件之前读取和解析模板文件所产生的事件序列。这在web应用程序中尤其有用,它基于以下概念:
17 离线模板
17.1 离线模板的概念(分离thymeleaf和dom)
到目前为止,我们在杂货店中使用的模板都是按照通常的方式完成的,将逻辑以属性的形式插入到模板中。
但是,Thymeleaf还允许我们将模板标记与其逻辑完全解耦,允许在HTML和XML模板模式中创建完全没有逻辑的标记模板。
其主要思想是模板逻辑将在一个单独的逻辑文件中定义(更确切地说,是一个逻辑资源,因为它不需要是一个文件)。默认情况下,该逻辑资源将是与模板文件位于相同位置(例如文件夹)的另一个文件,具有相同的名称,但是.th.xml扩展名:
/templates
+->/home.html
+->/home.th.xml
因此home.html文件可以完全没有逻辑。它可能是这样的:
<!DOCTYPE html>
<html>
<body>
<table id="usersTable">
<tr>
<td class="username">Jeremy Grapefruit</td>
<td class="usertype">Normal User</td>
</tr>
<tr>
<td class="username">Alice Watermelon</td>
<td class="usertype">Administrator</td>
</tr>
</table>
</body>
</html>
绝对没有Thymeleaf代码。这是一个没有Thymeleaf或模板知识的设计人员可以创建、编辑和/或理解的模板文件。或者由完全没有Thymeleaf钩子的外部系统提供的HTML片段。
现在让我们通过创建额外的home.th.xml文件将home.html模板转换为Thymeleaf模板,如下所示:
<?xml version="1.0"?>
<thlogic>
<attr sel="#usersTable" th:remove="all-but-first">
<attr sel="/tr[0]" th:each="user : ${users}">
<attr sel="td.username" th:text="${user.name}" />
<attr sel="td.usertype" th:text="#{|user.type.${user.type}|}" />
</attr>
</attr>
</thlogic>
在这里,我们可以在thlogic块中看到许多标记。这些标记通过它们的sel属性对原始模板的节点执行属性注入,这些sel属性包含Thymeleaf标记选择器(实际上是AttoParser标记选择器)。
还请注意,标记可以嵌套,以便追加它们的选择器。例如,上面的sel="/tr[0]"将被处理为sel="#usersTable/tr[0]"。用户名的选择器将被处理为sel="#usersTable/tr[0]//td.username"。
因此,一旦合并,上述两个文件将是相同的:
<!DOCTYPE html>
<html>
<body>
<table id="usersTable" th:remove="all-but-first">
<tr th:each="user : ${users}">
<td class="username" th:text="${user.name}">Jeremy Grapefruit</td>
<td class="usertype" th:text="#{|user.type.${user.type}|}">Normal User</td>
</tr>
<tr>
<td class="username">Alice Watermelon</td>
<td class="usertype">Administrator</td>
</tr>
</table>
</body>
</html>
这看起来更熟悉,而且实际上比创建两个单独的文件更简单。但是解耦模板的优点是,我们可以使模板完全独立于Thymeleaf,因此从设计的角度来看,具有更好的可维护性。
当然,设计人员或开发人员之间仍然需要一些契约—例如,用户需要id="usersTable"—但是在许多场景中,纯html模板将是设计和开发团队之间更好的通信工件。
17.2 配置离线模板
开启离线模板
默认情况下,不会期望每个模板都具有解耦逻辑。相反,配置的模板解析器(ITemplateResolver的实现)将需要专门将它们解析的模板标记为使用解耦逻辑。
除了StringTemplateResolver(它不支持解耦逻辑)之外,ITemplateResolver的所有其他开箱即用实现都将提供一个名为usedeconpledlogic的标志,该标志将把该解耦器解析的所有模板标记为所有或部分逻辑都可能位于单独的资源中:
final ServletContextTemplateResolver templateResolver =
new ServletContextTemplateResolver(servletContext);
...
templateResolver.setUseDecoupledLogic(true);
混用在线/离线模板
在启用时,不需要解耦模板逻辑。当启用时,这意味着引擎将查找包含解耦逻辑的资源,如果它存在,则解析它并将其与原始模板合并。如果解耦逻辑资源不存在,则不会抛出错误。
同样,在同一个模板中,我们可以混合耦合和解耦逻辑,例如在原始模板文件中添加一些Thymeleaf属性,而将其他属性留给单独的解耦逻辑文件。最常见的情况是使用新的th:ref属性(在v3.0中)。
17.3 ref属性
ref只是一个标记属性。从处理的角度来看,它什么也不做,只是在处理模板时消失,但它的有用之处在于它作为标记引用,即它可以像标记名称或片段(th:fragment)一样通过标记选择器中的名称解析。
如果我们有一个选择器
<attr sel="whatever" .../>
这将匹配:
- Any <whatever> tags.
- Any tags with a th:fragment="whatever" attribute.
- Any tags with a th:ref="whatever" attribute.
例如,使用纯html id属性th:ref的优点是什么?仅仅是因为我们可能不希望向标记中添加这么多id和类属性来充当逻辑锚,这最终可能会污染我们的输出。
同样的道理,th:ref的缺点是什么?显然,我们需要在模板中添加一些Thymeleaf逻辑(“逻辑”)。
请注意th:ref属性的这种适用性不仅适用于解耦的逻辑模板文件:它在其他类型的场景中也同样适用,比如片段表达式(~{…})。
17.4 离线模板的性能
影响非常小。当一个已解析的模板被标记为使用解耦逻辑而没有被缓存时,模板逻辑资源将首先被解析、解析并处理为内存中指令的安全:基本上是要注入到每个标记选择器中的属性列表。
但这是惟一需要的额外步骤,因为在此之后,将解析实际的模板,在解析模板时,解析器本身将动态地注入这些属性,这要感谢AttoParser中的节点选择高级功能。因此,解析后的节点将从解析器中出来,就好像它们的注入属性写在原始模板文件中一样。
这最大的好处是什么?当将模板配置为缓存时,它将被缓存,其中已经包含注入的属性。因此,对可缓存模板使用解耦模板的开销,一旦它们被缓存,将绝对为零。
17.5 何时分离逻辑代码和样式(离线模板)
Thymeleaf解析与每个模板对应的解耦逻辑资源的方式可由用户配置。它由一个扩展点确定
ideconpledtemplatelogicresolver,它提供了一个默认的实现:standarddeconpledtemplatelogicresolver。
这个标准实现做什么?
首先,它对模板资源的基名称应用前缀和后缀(通过ITemplateResource#getBaseName()方法获得)。前缀和后缀都可以配置,默认情况下,前缀为空,后缀为.th.xml。
其次,它要求模板资源通过ITemplateResource#relative(String relativeLocation)方法解析具有计算名称的相对资源。
使用的ideconpledtemplatelogicresolver的具体实现可以在TemplateEngine上轻松配置:
final StandardDecoupledTemplateLogicResolver decoupledresolver =
new StandardDecoupledTemplateLogicResolver();
decoupledResolver.setPrefix("../viewlogic/");
...
templateEngine.setDecoupledTemplateLogicResolver(decoupledResolver);