Thymeleaf官方文档(中文版)
本文基于官方文档翻译,如有错误欢迎指正。
Project version: 3.1.3.RELEASE
Project web site: https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html (opens new window)
# 1 介绍 Thymeleaf
# 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
),三种“文本”模板模式(TEXT
、JAVASCRIPT
和 CSS
)和一种“无操作”模板模式(RAW
)。
HTML
模板模式将允许任何类型的 HTML 输入,包括 HTML5、HTML 4 和 XHTML。不会执行验证或格式正确性检查,并且在输出中会最大程度地尊重模板代码/结构。
XML
模板模式将允许 XML 输入。在这种情况下,代码应该格式良好 - 没有未关闭的标签,没有未引用的属性等 - 如果发现格式不正确,解析器将抛出异常。请注意,不会执行(针对 DTD 或 XML 模式)的“验证”。
TEXT
模板模式将允许使用特殊语法来创建非标记性质的模板。此类模板的示例可能是文本电子邮件或模板化文档。请注意,HTML 或 XML 模板也可以作为 TEXT
进行处理,在这种情况下,它们不会被解析为标记,并且每个标签、DOCTYPE、注释等都将被视为纯文本。
JAVASCRIPT
模板模式将允许在 Thymeleaf 应用程序中处理 JavaScript 文件。这意味着能够在 JavaScript 文件中使用模型数据,就像在 HTML 文件中一样,但是具有特定于 JavaScript 的集成,例如专门的转义或“自然脚本”。JAVASCRIPT
模板模式被认为是“文本”模式,因此使用与 TEXT
模板模式相同的特殊语法。
CSS
模板模式将允许处理 Thymeleaf 应用程序中涉及的 CSS 文件。与 JAVASCRIPT
模式类似,CSS
模板模式也是“文本”模式,并使用 TEXT
模板模式的特殊处理语法。
RAW
模板模式根本不会处理模板。它旨在用于将未修改的资源(文件、URL 响应等)插入到正在处理的模板中。例如,可以将 HTML 格式的外部、不受控制的资源安全地包含在应用程序模板中,因为知道这些资源可能包含的任何 Thymeleaf 代码都不会被执行。
# 1.3 方言:标准方言
Thymeleaf 是一个极其可扩展的模板引擎(实际上它可以被称为“模板引擎框架”),允许您定义和自定义模板的处理方式,达到非常精细的细节级别。
将某些逻辑应用于标记工件(标签、一些文本、注释或如果模板不是标记则为单纯的占位符)的对象称为“处理器”,一组这些处理器 - 加上可能的一些额外工件 - 通常构成了一个“方言”。开箱即用,Thymeleaf 的核心库提供了一种称为“标准方言”的方言,这对于大多数用户来说应该足够了。
本教程涵盖标准方言。在以下页面中您将学习的每个属性和语法特性都是由这种方言定义的,即使没有明确提及。
当然,如果用户想要定义自己的处理逻辑,同时利用库的高级功能,他们可以创建自己的方言(甚至扩展标准方言)。Thymeleaf 也可以配置为同时使用多种方言。
官方的 thymeleaf-spring5 和 thymeleaf-spring6 集成包都定义了一种称为“SpringStandard Dialect”的方言,它与标准方言大部分相同,但进行了一些小的调整,以便更好地利用 Spring 框架中的某些功能(例如,通过使用 Spring Expression Language 而不是 OGNL)。因此,如果您是 Spring MVC 用户,您不会浪费时间,因为您在这里学到的几乎所有内容都将在您的 Spring 应用程序中有用。
标准方言的大多数处理器都是“属性处理器”。这允许浏览器在处理之前正确显示 HTML 模板文件,因为它们只会忽略附加的属性。例如,当使用标签库的 JSP 可能包含一段浏览器无法直接显示的代码,如:
<form:inputText name="userName" value="${user.name}" />
...Thymeleaf 标准方言将允许我们使用以下方式实现相同的功能:
<input type="text" name="userName" value="James Carrot" th:value="${user.name}" />
不仅浏览器会正确显示此内容,而且这还允许我们(可选)在其中指定一个值属性(在这种情况下为“James Carrot”),当在浏览器中静态打开原型时将显示该值,并且在处理模板期间将被 ${user.name}
的评估结果替换。
这有助于您的设计师和开发人员处理相同的模板文件,并减少将静态原型转换为工作模板文件所需的工作量。能够做到这一点的功能称为“自然模板”。
# 2 好时光虚拟杂货店
本指南此章节及后续章节所展示示例的源代码,可以在 好时光虚拟杂货店(GTVG) 示例应用中找到,该应用有两个(等效的)版本:
javax.*
基础版:https://github.com/thymeleaf/thymeleaf/tree/3.1-master/examples/core/thymeleaf-examples-gtvg-javaxjakarta.*
基础版:https://github.com/thymeleaf/thymeleaf/tree/3.1-master/examples/core/thymeleaf-examples-gtvg-jakarta
# 2.1 一家杂货店的网站
为了更好地解释用 Thymeleaf 处理模板所涉及的概念,本教程将使用一个演示应用程序,您可以从项目网站下载。
此应用程序是一个虚构的虚拟杂货店的网站,将为我们提供许多场景来展示 Thymeleaf 的众多功能。
首先,我们的应用程序需要一组简单的模型实体:通过创建Orders
出售给Customers
的Products
。我们还将管理关于这些Products
的Comments
:
我们的应用程序还将有一个非常简单的服务层,由包含如下方法的Service
对象组成:
public class ProductService {
//...
public List<Product> findAll() {
return ProductRepository.getInstance().findAll();
}
public Product findById(Integer id) {
return ProductRepository.getInstance().findById(id);
}
}
在网络层,我们的应用程序将有一个过滤器,它将根据请求 URL 将执行委托给启用 Thymeleaf 的命令:
/*
* 应用程序对象需要首先声明(实现 IWebApplication)
* 在这种情况下,将使用基于 Jakarta 的版本。
*/
public void init(final FilterConfig filterConfig) throws ServletException {
this.application =
JakartaServletWebApplication.buildApplication(
filterConfig.getServletContext());
// 稍后我们将看到 TemplateEngine 对象是如何构建和配置的
this.templateEngine = buildTemplateEngine(this.application);
}
/*
* 每个请求将通过创建一个交换对象(对请求、其响应和此过程所需的所有数据进行建模)
* 然后调用相应的控制器进行处理。
*/
private boolean process(HttpServletRequest request, HttpServletResponse response)
throws ServletException {
try {
final IWebExchange webExchange =
this.application.buildExchange(request, response);
final IWebRequest webRequest = webExchange.getRequest();
// 这可以防止为资源 URL 触发引擎执行
if (request.getRequestURI().startsWith("/css") ||
request.getRequestURI().startsWith("/images") ||
request.getRequestURI().startsWith("/favicon")) {
return false;
}
/*
* 查询控制器/URL 映射并获取将处理请求的控制器。
* 如果没有可用的控制器,返回 false 并让其他过滤器/服务处理请求。
*/
final IGTVGController controller =
ControllerMappings.resolveControllerForRequest(webRequest);
if (controller == null) {
return false;
}
/*
* 写入响应头
*/
response.setContentType("text/html;charset=UTF-8");
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
/*
* 获取响应写入器
*/
final Writer writer = response.getWriter();
/*
* 执行控制器并处理视图模板,将结果写入响应写入器。
*/
controller.process(webExchange, this.templateEngine, writer);
return true;
} catch (Exception e) {
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} catch (final IOException ignored) {
// 只需忽略此异常
}
throw new ServletException(e);
}
}
这是我们的IGTVGController
接口:
public interface IGTVGController {
public void process(
final IWebExchange webExchange,
final ITemplateEngine templateEngine,
final Writer writer)
throws Exception;
}
现在我们要做的就是创建IGTVGController
接口的实现,从服务中检索数据并使用ITemplateEngine
对象处理模板。
最后,它将看起来像这样:
但首先让我们看看那个模板引擎是如何初始化的。
# 2.2 创建和配置模板引擎
我们过滤器中的init(...)
方法包含这一行:
this.templateEngine = buildTemplateEngine(this.application);
现在让我们看看我们的org.thymeleaf.TemplateEngine
对象是如何初始化的:
private static ITemplateEngine buildTemplateEngine(final IWebApplication application) {
// 模板将作为应用程序(ServletContext)资源解析
final WebApplicationTemplateResolver templateResolver =
new WebApplicationTemplateResolver(application);
// HTML 是默认模式,但我们还是要设置它,以便更好地理解代码
templateResolver.setTemplateMode(TemplateMode.HTML);
// 这将把 "home" 转换为 "/WEB-INF/templates/home.html"
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
// 将模板缓存 TTL 设置为 1 小时。如果未设置,条目将在缓存中保留,直到被 LRU 逐出
templateResolver.setCacheTTLMs(Long.valueOf(3600000L));
// 缓存默认设置为 true。如果希望模板在修改时自动更新,则设置为 false
templateResolver.setCacheable(true);
final TemplateEngine templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
有很多配置TemplateEngine
对象的方法,但现在这几行代码将让我们充分了解所需的步骤。
# 模板解析器
让我们从模板解析器开始:
final WebApplicationTemplateResolver templateResolver =
new WebApplicationTemplateResolver(application);
模板解析器是实现 Thymeleaf API 中称为org.thymeleaf.templateresolver.ITemplateResolver
接口的对象:
public interface ITemplateResolver {
//...
/*
* 模板通过其名称(或内容)以及在我们尝试为另一个模板解析片段的情况下的所有者模板进行解析。
* 如果此模板解析器无法处理模板,则返回 null。
*/
public TemplateResolution resolveTemplate(
final IEngineConfiguration configuration,
final String ownerTemplate, final String template,
final Map<String, Object> templateResolutionAttributes);
}
这些对象负责确定我们如何访问模板,在这个 GTVG 应用程序中,使用org.thymeleaf.templateresolver.WebApplicationTemplateResolver
意味着我们将从IWebApplication
对象中检索我们的模板文件作为资源:Thymeleaf 的抽象,在基于 Servlet 的应用程序中,基本上围绕 Servlet API 的[javax|jakarta].servlet.ServletContext
对象,并从 Web 应用程序根目录解析资源。
但这并不是关于模板解析器的全部,因为我们可以在其上设置一些配置参数。首先,模板模式:
templateResolver.setTemplateMode(TemplateMode.HTML);
HTML 是WebApplicationTemplateResolver
的默认模板模式,但无论如何明确设置它是一个好的实践,以便我们的代码清楚地记录正在发生的事情。
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
prefix
和suffix
修改了我们将传递给引擎以获取要使用的实际资源名称的模板名称。
使用此配置,模板名称“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 多语言欢迎页面
我们的第一个任务是为我们的杂货店网站创建一个主页。
这个页面的第一个版本将非常简单:只有一个标题和一条欢迎信息。这是我们的 /WEB-INF/templates/home.html
文件:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Good Thymes Virtual Grocery</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" media="all"
href="../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
</head>
<body>
<p th:text="#{home.welcome}">Welcome to our grocery store!</p>
</body>
</html>
你首先会注意到的是,这个文件是 HTML5 格式的,可以在任何浏览器中正确显示,因为它不包含任何非 HTML 标签(浏览器会忽略所有它们不理解的属性,比如 th:text
)。
但你可能还会注意到,这个模板并不是一个真正有效的 HTML5 文档,因为我们使用的这些非标准属性(以 th:*
形式)在 HTML5 规范中是不允许的。事实上,我们甚至在我们的 <html>
标签中添加了一个 xmlns:th
属性,这绝对不符合 HTML5 规范:
<html xmlns:th="http://www.thymeleaf.org">
这在模板处理中没有任何影响,但它作为一种“咒语”可以防止我们的 IDE 抱怨缺少这些 th:*
属性的命名空间定义。
那么,如果我们想让这个模板成为有效的 HTML5 文档呢?很简单:切换到 Thymeleaf 的数据属性语法,使用 data-
前缀作为属性名,并使用连字符(-
)而不是分号(:
)作为分隔符:
<!DOCTYPE html>
<html>
<head>
<title>Good Thymes Virtual Grocery</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" media="all"
href="../../css/gtvg.css" data-th-href="@{/css/gtvg.css}" />
</head>
<body>
<p data-th-text="#{home.welcome}">Welcome to our grocery store!</p>
</body>
</html>
HTML5 规范允许自定义的 data-
前缀属性,因此,使用上面的代码,我们的模板将是一个有效的 HTML5 文档。
两种表示法完全等价且可以互换,但为了代码示例的简洁性和紧凑性,本教程将使用命名空间表示法(
th:*
)。此外,th:*
表示法更通用,允许在所有 Thymeleaf 模板模式(XML
、TEXT
等)中使用,而data-
表示法仅在HTML
模式下允许。
# 使用 th:text
和外部化文本
外部化文本是将模板代码片段提取到模板文件之外,以便它们可以保存在单独的文件中(通常是 .properties
文件),并且可以轻松地用其他语言编写的等效文本替换(这个过程称为国际化或简称 i18n
)。外部化的文本片段通常称为“消息”。
消息总是有一个标识它们的键,Thymeleaf 允许你使用 #{...}
语法指定文本应对应于特定消息:
<p th:text="#{home.welcome}">Welcome to our grocery store!</p>
我们在这里看到的实际上是 Thymeleaf 标准方言的两个不同特性:
th:text
属性,它评估其值表达式并将结果设置为宿主标签的主体,有效地替换我们在代码中看到的“Welcome to our grocery store!”文本。#{home.welcome}
表达式,在标准表达式语法中指定,指示th:text
属性使用的文本应该是与处理模板时使用的语言环境对应的home.welcome
键的消息。
那么,这个外部化的文本在哪里呢?
Thymeleaf 中外部化文本的位置是完全可配置的,它将取决于所使用的特定 org.thymeleaf.messageresolver.IMessageResolver
实现。通常,将使用基于 .properties
文件的实现,但如果我们愿意,我们可以创建自己的实现,例如从数据库中获取消息。
然而,我们在初始化期间没有为模板引擎指定消息解析器,这意味着我们的应用程序使用的是由 org.thymeleaf.messageresolver.StandardMessageResolver
实现的标准消息解析器。
标准消息解析器期望在与模板相同的文件夹中找到 /WEB-INF/templates/home.html
的消息,并且文件名与模板相同,例如:
/WEB-INF/templates/home_en.properties
用于英文文本。/WEB-INF/templates/home_es.properties
用于西班牙语文本。/WEB-INF/templates/home_pt_BR.properties
用于葡萄牙语(巴西)文本。/WEB-INF/templates/home.properties
用于默认文本(如果未匹配到语言环境)。
让我们看看我们的 home_es.properties
文件:
home.welcome=¡Bienvenido a nuestra tienda de comestibles!
这就是我们让 Thymeleaf 处理模板所需的全部内容。接下来,让我们创建我们的 Home 控制器。
# 上下文
为了处理我们的模板,我们将创建一个实现之前看到的 IGTVGController
接口的 HomeController
类:
public class HomeController implements IGTVGController {
public void process(
final IWebExchange webExchange,
final ITemplateEngine templateEngine,
final Writer writer)
throws Exception {
WebContext ctx = new WebContext(webExchange, webExchange.getLocale());
templateEngine.process("home", ctx, writer);
}
}
我们首先看到的是上下文的创建。Thymeleaf 上下文是实现 org.thymeleaf.context.IContext
接口的对象。上下文应包含模板引擎执行所需的所有数据,这些数据存储在变量映射中,并且还引用必须用于外部化消息的语言环境。
public interface IContext {
public Locale getLocale();
public boolean containsVariable(final String name);
public Set<String> getVariableNames();
public Object getVariable(final String name);
}
这个接口有一个专门的扩展,org.thymeleaf.context.IWebContext
,用于 Web 应用程序。
public interface IWebContext extends IContext {
public IWebExchange getExchange();
}
Thymeleaf 核心库提供了这些接口的实现:
org.thymeleaf.context.Context
实现IContext
org.thymeleaf.context.WebContext
实现IWebContext
正如你在控制器代码中看到的,我们使用的是 WebContext
。事实上,我们必须这样做,因为使用 WebApplicationTemplateResolver
要求我们使用实现 IWebContext
的上下文。
WebContext ctx = new WebContext(webExchange, webExchange.getLocale());
WebContext
构造函数需要包含在 IWebExchange
抽象对象中的信息,该对象是在表示此基于 Web 的交换(即请求 + 响应)的过滤器处创建的。如果未指定语言环境,则将使用系统的默认语言环境(尽管在实际应用程序中你永远不应该让这种情况发生)。
在我们的模板中,我们将能够使用一些专门的表达式从 WebContext
中获取请求参数以及请求、会话和应用程序属性。例如:
${x}
将返回存储在 Thymeleaf 上下文或作为交换属性(在 Servlet 术语中称为“请求属性”)的变量x
。${param.x}
将返回名为x
的请求参数(可能是多值的)。${session.x}
将返回名为x
的会话属性。${application.x}
将返回名为x
的应用程序属性(在 Servlet 术语中称为“servlet 上下文属性”)。
# 执行模板引擎
准备好上下文对象后,我们现在可以告诉模板引擎使用上下文处理模板(通过其名称),并传递一个响应写入器,以便可以将响应写入其中:
templateEngine.process("home", ctx, writer);
让我们看看使用西班牙语言环境的结果:
<!DOCTYPE html>
<html>
<head>
<title>Good Thymes Virtual Grocery</title>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<link rel="stylesheet" type="text/css" media="all" href="/gtvg/css/gtvg.css" />
</head>
<body>
<p>¡Bienvenido a nuestra tienda de comestibles!</p>
</body>
</html>
# 3.2 更多关于文本和变量的内容
# 未转义的文本
我们的主页的最简单版本似乎已经准备好了,但我们还没有考虑到一些事情……如果我们有这样一条消息怎么办?
home.welcome=Welcome to our <b>fantastic</b> grocery store!
如果我们像以前一样执行这个模板,我们将得到:
<p>Welcome to our <b>fantastic</b> grocery store!</p>
这并不是我们所期望的,因为我们的 <b>
标签已经被转义,因此它将在浏览器中显示。
这是 th:text
属性的默认行为。如果我们希望 Thymeleaf 尊重我们的 HTML 标签而不转义它们,我们将不得不使用不同的属性:th:utext
(用于“未转义的文本”):
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
这将输出我们想要的消息:
<p>Welcome to our <b>fantastic</b> grocery store!</p>
# 使用和显示变量
现在让我们为主页添加更多内容。例如,我们可能希望在欢迎信息下方显示日期,如下所示:
Welcome to our fantastic grocery store!
Today is: 12 july 2010
首先,我们必须修改我们的控制器,以便将日期作为上下文变量添加:
public void process(
final IWebExchange webExchange,
final ITemplateEngine templateEngine,
final Writer writer)
throws Exception {
SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy");
Calendar cal = Calendar.getInstance();
WebContext ctx = new WebContext(webExchange, webExchange.getLocale());
ctx.setVariable("today", dateFormat.format(cal.getTime()));
templateEngine.process("home", ctx, writer);
}
我们已经在上下文中添加了一个名为 today
的 String
变量,现在我们可以在模板中显示它:
<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>
正如你所看到的,我们仍然使用 th:text
属性来完成这项工作(这是正确的,因为我们想要替换标签的主体),但这次语法有点不同,我们使用的是 ${...}
表达式值,而不是 #{...}
表达式值。这是一个变量表达式,它包含一种称为 OGNL(对象图导航语言)的语言中的表达式,该表达式将在我们之前讨论的上下文变量映射上执行。
${today}
表达式简单地表示“获取名为 today 的变量”,但这些表达式可能更复杂(例如 ${user.name}
表示“获取名为 user 的变量,并调用其 getName()
方法”)。
属性值中有很多可能性:消息、变量表达式……还有很多。下一章将向我们展示所有这些可能性。
# 4 标准表达式语法
我们将暂时中断虚拟杂货店的开发,来学习 Thymeleaf 标准方言中最重要的部分之一:Thymeleaf 标准表达式语法。
我们已经看到了两种用这种语法表示的有效属性值:消息表达式和变量表达式:
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
<p>Today is: <span th:text="${today}">13 february 2011</span></p>
但还有更多类型的表达式,以及我们已经知道的表达式的更多有趣细节。
首先,让我们快速总结一下标准表达式的特性:
- 简单表达式:
- 变量表达式:
${...}
- 选择变量表达式:
*{...}
- 消息表达式:
#{...}
- 链接 URL 表达式:
@{...}
- 片段表达式:
~{...}
- 变量表达式:
- 字面量:
- 文本字面量:
'one text'
,'Another one!'
,… - 数字字面量:
0
,34
,3.0
,12.3
,… - 布尔字面量:
true
,false
- Null 字面量:
null
- 字面量标记:
one
,sometext
,main
,…
- 文本字面量:
- 文本操作:
- 字符串连接:
+
- 字面量替换:
|The name is ${name}|
- 字符串连接:
- 算术操作:
- 二元运算符:
+
,-
,*
,/
,%
- 负号(一元运算符):
-
- 二元运算符:
- 布尔操作:
- 二元运算符:
and
,or
- 布尔否定(一元运算符):
!
,not
- 二元运算符:
- 比较和相等:
- 比较器:
>
,<
,>=
,<=
(gt
,lt
,ge
,le
) - 相等运算符:
==
,!=
(eq
,ne
)
- 比较器:
- 条件运算符:
- If-then:
(if) ? (then)
- If-then-else:
(if) ? (then) : (else)
- 默认值:
(value) ?: (defaultvalue)
- If-then:
- 特殊标记:
- 无操作:
_
- 无操作:
所有这些特性都可以组合和嵌套:
'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))
# 4.1 消息
正如我们已经知道的,#{...}
消息表达式允许我们将以下内容链接起来:
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
……到以下内容:
home.welcome=¡Bienvenido a nuestra tienda de comestibles!
但我们还没有考虑到一个方面:如果消息文本不是完全静态的怎么办?例如,如果我们的应用程序知道当前访问网站的用户是谁,并且我们想通过名字来问候他们?
<p>¡Bienvenido a nuestra tienda de comestibles, John Apricot!</p>
这意味着我们需要为我们的消息添加一个参数。就像这样:
home.welcome=¡Bienvenido a nuestra tienda de comestibles, {0}!
参数根据 java.text.MessageFormat
标准语法指定,这意味着你可以按照 java.text.*
包中的 API 文档格式化数字和日期。
为了为我们的参数指定一个值,并且给定一个名为 user
的 HTTP 会话属性,我们可以这样做:
<p th:utext="#{home.welcome(${session.user.name})}">
Welcome to our grocery store, Sebastian Pepper!
</p>
注意:这里使用
th:utext
意味着格式化后的消息不会被转义。此示例假设user.name
已经被转义。
可以指定多个参数,用逗号分隔。
消息键本身也可以来自变量:
<p th:utext="#{${welcomeMsgKey}(${session.user.name})}">
Welcome to our grocery store, Sebastian Pepper!
</p>
# 4.2 变量
我们已经提到,${...}
表达式实际上是 OGNL(对象图导航语言)表达式,在上下文包含的变量映射上执行。
有关 OGNL 语法和功能的详细信息,你应该阅读 OGNL 语言指南 (opens new window)。
在启用 Spring MVC 的应用程序中,OGNL 将被 SpringEL 替换,但其语法与 OGNL 非常相似(实际上,在大多数常见情况下完全相同)。
从 OGNL 的语法中,我们知道以下表达式:
<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 的特性之一。让我们看看更多:
/*
* 使用点(.)访问属性。等同于调用属性 getter。
*/
${person.father.name}
/*
* 也可以使用方括号([])访问属性,并将属性名称作为变量或单引号之间的字符串写入。
*/
${person['father']['name']}
/*
* 如果对象是映射,点语法和方括号语法都等同于调用其 get(...) 方法。
*/
${countriesByCode.ES}
${personsByName['Stephen Zucchini'].age}
/*
* 数组或集合的索引访问也使用方括号,索引不带引号。
*/
${personsArray[0].name}
/*
* 可以调用方法,甚至可以带参数。
*/
${person.createCompleteName()}
${person.createCompleteNameWithSeparator('-')}
# 表达式基本对象
在上下文变量上评估 OGNL 表达式时,一些对象会被提供给表达式以增加灵活性。这些对象将以 #
符号开头引用:
#ctx
:上下文对象。#vars
:上下文变量。#locale
:上下文语言环境。
所以我们可以这样做:
Established locale country: <span th:text="${#locale.country}">US</span>.
你可以在 附录 A 中阅读这些对象的完整参考。
# 表达式实用对象
除了这些基本对象外,Thymeleaf 还提供了一组实用对象,帮助我们执行表达式中的常见任务。
#execInfo
:有关正在处理的模板的信息。#messages
:用于在变量表达式中获取外部化消息的方法,与使用#{...}
语法相同。#uris
:用于转义 URL/URI 部分的方法。#conversions
:用于执行配置的转换服务(如果有)的方法。#dates
:用于java.util.Date
对象的方法:格式化、组件提取等。#calendars
:类似于#dates
,但用于java.util.Calendar
对象。#temporals
:用于在 JDK8+ 中使用java.time
API 处理日期和时间。#numbers
:用于格式化数字对象的方法。#strings
:用于String
对象的方法:包含、以…开头、前置/追加等。#objects
:用于一般对象的方法。#bools
:用于布尔评估的方法。#arrays
:用于数组的方法。#lists
:用于列表的方法。#sets
:用于集合的方法。#maps
:用于映射的方法。#aggregates
:用于在数组或集合上创建聚合的方法。#ids
:用于处理可能重复的 id 属性的方法(例如,作为迭代的结果)。
你可以在 附录 B 中查看这些实用对象提供的功能。
# 在主页中重新格式化日期
现在我们知道了这些实用对象,我们可以使用它们来改变我们在主页中显示日期的方式。而不是在 HomeController
中这样做:
SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy");
Calendar cal = Calendar.getInstance();
WebContext ctx = new WebContext(webExchange, webExchange.getLocale());
ctx.setVariable("today", dateFormat.format(cal.getTime()));
templateEngine.process("home", ctx, writer);
……我们可以这样做:
WebContext ctx = new WebContext(webExchange, webExchange.getLocale());
ctx.setVariable("today", Calendar.getInstance());
templateEngine.process("home", ctx, writer);
……然后在视图层本身执行日期格式化:
<p>
Today is: <span th:text="${#calendars.format(today,'dd MMMM yyyy')}">13 May 2011</span>
</p>
# 4.3 选择表达式(星号语法)
变量表达式不仅可以写成 ${...}
,还可以写成 *{...}
。
但有一个重要的区别:星号语法在选定对象上评估表达式,而不是在整个上下文上。也就是说,只要没有选定对象,美元和星号语法完全相同。
那么什么是选定对象?使用 th:object
属性的表达式的结果。让我们在用户配置文件(userprofile.html
)页面中使用一个:
<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>
这完全等同于:
<div>
<p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div>
当然,美元和星号语法可以混合使用:
<div th:object="${session.user}">
<p>Name: <span th:text="*{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>
当选定对象存在时,选定对象也将作为 #object
表达式变量提供给美元表达式:
<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>
如前所述,如果没有执行对象选择,美元和星号语法是等价的。
<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,可以是:
- 页面相对:
user/login.html
- 上下文相对:
/itemdetails?id=3
(服务器上的上下文名称将自动添加) - 服务器相对:
~/billing/processInvoice
(允许调用同一服务器上另一个上下文(= 应用程序)中的 URL) - 协议相对 URL:
//code.jquery.com/jquery-2.0.3.min.js
- 页面相对:
这些表达式的实际处理及其转换为将输出的 URL 由注册到正在使用的 ITemplateEngine
对象的 org.thymeleaf.linkbuilder.ILinkBuilder
接口的实现完成。
默认情况下,注册了此接口的单个实现,即 org.thymeleaf.linkbuilder.StandardLinkBuilder
类,它足以满足基于 Servlet API 的离线(非 Web)和 Web 场景。其他场景(如与非 ServletAPI Web 框架的集成)可能需要特定的链接构建器接口实现。
让我们使用这个新语法。认识一下 th:href
属性:
<!-- 将生成 'http://localhost:8080/gtvg/order/details?orderId=3'(加上重写) -->
<a href="details.html"
th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>
<!-- 将生成 '/gtvg/order/details?orderId=3'(加上重写) -->
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
<!-- 将生成 '/gtvg/order/3/details'(加上重写) -->
<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>
这里有一些需要注意的事项:
th:href
是一个修饰属性:一旦处理,它将计算要使用的链接 URL 并将该值设置为<a>
标签的href
属性。- 我们允许使用表达式作为 URL 参数(如
orderId=${o.id}
)。所需的 URL 参数编码操作也将自动执行。 - 如果需要多个参数,这些参数将用逗号分隔:
@{/order/process(execId=${execId},execType='FAST')}
- URL 路径中也允许使用变量模板:
@{/order/{orderId}/details(orderId=${orderId})}
- 以
/
开头的相对 URL(例如:/order/details
)将自动添加应用程序上下文名称。 - 如果未启用 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>
# 主页的菜单
现在我们知道如何创建链接 URL,那么在我们的主页中添加一个小菜单来链接到网站中的其他页面呢?
<p>Please select an option</p>
<ol>
<li><a href="product/list.html" th:href="@{/product/list}">Product List</a></li>
<li><a href="order/list.html" th:href="@{/order/list}">Order List</a></li>
<li><a href="subscribe.html" th:href="@{/subscribe}">Subscribe to our Newsletter</a></li>
<li><a href="userprofile.html" th:href="@{/userprofile}">See User Profile</a></li>
</ol>
# 服务器根相对 URL
可以使用额外的语法来创建服务器根相对(而不是上下文根相对)URL,以便链接到同一服务器中的不同上下文。这些 URL 将指定为 @{~/path/to/something}
。
# 4.5 片段
片段表达式是一种表示标记片段并围绕模板移动它们的简单方法。这允许我们复制它们,将它们作为参数传递给其他模板,等等。
最常见的用途是使用 th:insert
或 th:replace
进行片段插入(稍后将在有关模板布局的部分中详细介绍):
<div th:insert="~{commons :: main}">...</div>
但它们可以像任何其他变量一样在任何地方使用:
<div th:with="frag=~{footer :: #main/text()}">
<p th:insert="${frag}">
</div>
在本教程的后面部分,有一个专门介绍模板布局的部分,包括对片段表达式的更深入解释。
# 4.6 字面量
# 文本字面量
文本字面量只是用单引号括起来的字符字符串。它们可以包含任何字符,但你应该使用 \'
转义其中的任何单引号。
<p>
Now you are looking at a <span th:text="'working web application'">template file</span>.
</p>
# 数字字面量
数字字面量就是数字。
<p>The year is <span th:text="2013">1492</span>.</p>
<p>In two years, it will be <span th:text="2013 + 2">1494</span>.</p>
# 布尔字面量
布尔字面量是 true
和 false
。例如:
<div th:if="${user.isAdmin()} == false"> ...</div>
在这个例子中,== false
写在括号外,因此由 Thymeleaf 处理。如果写在括号内,则由 OGNL/SpringEL 引擎处理:
<div th:if="${user.isAdmin() == false}"> ...</div>
# Null 字面量
null
字面量也可以使用:
<div th:if="${variable.something} == null"> ...</div>
# 字面量标记
数字、布尔值和空值字面量实际上是文字标记的一种特殊情况。
这些标记在标准表达式中允许进行一些简化。它们的工作方式与文本字面量('...'
)完全相同,但它们只允许字母(A-Z
和a-z
)、数字(0-9
)、括号([
和]
)、点(.
)、连字符(-
)和下划线(_
)。所以不允许有空格、逗号等。
好的部分是?标记不需要任何引号包围。所以我们可以这样做:
<div th:class="content">...</div>
而不是:
<div th:class="'content'">...</div>
# 4.7 文本拼接
文本,无论是字面量还是评估变量或消息表达式的结果,都可以使用 +
操作符轻松拼接:
<span th:text="'The name of the user is ' + ${user.name}"></span>
# 4.8 字面量替换
字面量替换允许将包含变量值的字符串进行格式化,而无需使用 '...' + '...'
进行拼接。
这些替换必须用竖线(|
)括起来,例如:
<span th:text="|Welcome to our application, ${user.name}!|"></span>
这等同于:
<span th:text="'Welcome to our application, ' + ${user.name} + '!'"></span>
字面量替换可以与其他类型的表达式结合使用:
<span th:text="${onevar} + ' ' + |${twovar}, ${threevar}|"></span>
注意:只有变量/消息表达式(
${...}
、*{...}
、#{...}
)允许在|...|
字面量替换中使用。不允许其他字面量('...'
)、布尔/数字标记、条件表达式等。
# 4.9 算术操作
一些算术操作也是可用的:+
、-
、*
、/
和 %
。
<div th:with="isEven=(${prodStat.count} % 2 == 0)"></div>
请注意,这些操作符也可以在 OGNL 变量表达式中使用(在这种情况下将由 OGNL 而不是 Thymeleaf 标准表达式引擎执行):
<div th:with="isEven=${prodStat.count % 2 == 0}"></div>
注意,这些操作符有一些文本别名:div
(/
)、mod
(%
)。
# 4.10 比较器和相等
表达式中的值可以使用 >
、<
、>=
和 <=
符号进行比较,==
和 !=
运算符可用于检查相等性(或不等性)。请注意,XML 规定 <
和 >
符号不应在属性值中使用,因此应替换为 <
和 >
。
<div th:if="${prodStat.count} > 1">
<span th:text="'Execution mode is ' + ( (${execMode} == 'dev')? 'Development' : 'Production')"></span>
</div>
更简单的替代方案是使用这些操作符的文本别名:gt
(>
)、lt
(<
)、ge
(>=
)、le
(<=
)、not
(!
)。还有 eq
(==
)、neq
/ne
(!=
)。
# 4.11 条件表达式
条件表达式用于根据条件(本身是另一个表达式)的评估结果来评估两个表达式中的一个。
让我们看一个示例片段(引入另一个属性修饰符,th:class
):
<tr th:class="${row.even}? 'even' : 'odd'">
...
</tr>
条件表达式的所有三个部分(条件
、then
和 else
)本身都是表达式,这意味着它们可以是变量(${...}
、*{...}
)、消息(#{...}
)、URL(@{...}
)或字面量('...'
)。
条件表达式也可以使用括号嵌套:
<tr th:class="${row.even}? (${row.first}? 'first' : 'even') : 'odd'">
...
</tr>
else
表达式也可以省略,在这种情况下,如果条件为假,则返回空值:
<tr th:class="${row.even}? 'alt'">
...
</tr>
# 4.12 默认表达式(Elvis 操作符)
默认表达式是一种没有 then
部分的特殊条件值。它等同于 Groovy 等语言中的 Elvis 操作符,允许你指定两个表达式:如果第一个表达式评估为非空值,则使用它;否则使用第二个表达式。
让我们在用户资料页面中看看它的作用:
<div th:object="${session.user}">
...
<p>Age: <span th:text="*{age}?: '(no age specified)'">27</span>.</p>
</div>
如你所见,操作符是 ?:
,我们在这里用它来指定年龄的默认值(在此例中是一个字面量值),仅当 *{age}
的评估结果为 null
时。因此,这等同于:
<p>Age: <span th:text="*{age != null}? *{age} : '(no age specified)'">27</span>.</p>
与条件值一样,它们可以包含括号中的嵌套表达式:
<p>
Name:
<span th:text="*{firstName}?: (*{admin}? 'Admin' : #{default.username})">Sebastian</span>
</p>
# 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 将 user.lastAccessDate
表达式的结果传递给 转换服务,并要求它在写入结果之前执行 格式化操作(转换为 String
)。
假设 user.lastAccessDate
的类型为 java.util.Calendar
,如果已注册 转换服务(IStandardConversionService
的实现)并包含 Calendar -> String
的有效转换,则将应用该转换。
IStandardConversionService
的默认实现(StandardConversionService
类)只是对任何转换为 String
的对象执行 .toString()
。有关如何注册自定义 转换服务 实现的更多信息,请参阅 更多配置 部分。
官方的
thymeleaf-spring5
和thymeleaf-spring6
集成包将 Thymeleaf 的转换服务机制与 Spring 的 Conversion Service 基础设施无缝集成,因此 Spring 配置中声明的转换服务和格式化程序将自动提供给${{ ... }}
和*{{ ... }}
表达式。
# 4.15 预处理
除了所有这些表达式处理功能外,Thymeleaf 还具有 预处理 表达式的功能。
预处理是在正常表达式执行之前对表达式执行的,允许修改最终将执行的表达式。
预处理的表达式与普通表达式完全相同,但用双下划线符号括起来(如 __${expression}__
)。
让我们假设我们有一个 Messages_fr.properties
条目,其中包含调用特定于语言的静态方法的 OGNL 表达式,例如:
article.text=@myapp.translator.Translator@translateToFrench({0})
……和一个 Messages_es.properties
的等效条目:
article.text=@myapp.translator.Translator@translateToSpanish({0})
我们可以创建一段标记,根据语言环境评估一个表达式或另一个表达式。为此,我们首先选择表达式(通过预处理),然后让 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 设置任何属性的值
假设我们的网站发布了一则新闻通讯,我们希望用户能够订阅它,因此我们创建了一个 /WEB-INF/templates/subscribe.html
模板,并添加了一个表单:
<form action="subscribe.html">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" />
</fieldset>
</form>
与 Thymeleaf 一样,这个模板最初更像是一个静态原型,而不是一个 Web 应用程序的模板。首先,表单中的 action
属性静态链接到模板文件本身,因此没有地方进行有用的 URL 重写。其次,提交按钮中的 value
属性使其显示英文文本,但我们希望它是国际化的。
因此,我们引入了 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>
概念非常简单:th:attr
只需接受一个表达式,该表达式将值分配给属性。创建相应的控制器和消息文件后,处理此文件的结果将是:
<form action="/gtvg/subscribe">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="¡Suscríbe!" />
</fieldset>
</form>
除了新的属性值之外,你还可以看到应用程序上下文名称已自动前缀到 URL 基础 /gtvg/subscribe
,如前一章所述。
但如果我们想一次设置多个属性怎么办?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}" />
这看起来好多了!让我们尝试对 form
标签中的 action
属性做同样的操作:
<form action="subscribe.html" th:action="@{/subscribe}"></form>
还记得我们之前在 home.html
中放置的那些 th:href
吗?它们正是这种类型的属性:
<li><a href="product/list.html" th:href="@{/product/list}">Product List</a></li>
有许多类似的属性,每个属性都针对特定的 HTML5 属性。
# 5.3 一次设置多个值
有两个相当特殊的属性叫做 th:alt-title
和 th:lang-xmllang
,它们可以同时将两个属性设置为相同的值。具体来说:
th:alt-title
将设置alt
和title
。th:lang-xmllang
将设置lang
和xml:lang
。
对于我们的 GTVG 主页,这将允许我们替换以下代码:
<img src="../../images/gtvglogo.png"
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
或者这个等效的代码:
<img src="../../images/gtvglogo.png"
th:src="@{/images/gtvglogo.png}" th:title="#{logo}" th:alt="#{logo}" />
替换为以下代码:
<img src="../../images/gtvglogo.png"
th:src="@{/images/gtvglogo.png}" th:alt-title="#{logo}" />
# 5.4 追加和前置
Thymeleaf 还提供了 th:attrappend
和 th:attrprepend
属性,它们将评估结果附加(后缀)或前置(前缀)到现有属性值。
例如,你可能希望将 CSS 类的名称存储在一个上下文变量中,以便添加到你的按钮中,因为要使用的特定 CSS 类将取决于用户之前执行的操作:
<input type="button" value="Do it!" class="btn" th:attrappend="class=${' ' + cssStyle}" />
如果你使用 cssStyle
变量设置为 "warning"
处理此模板,你将得到:
<input type="button" value="Do it!" class="btn warning" />
标准方言中还有两个特定的 "追加属性":th:classappend
和 th:styleappend
属性,它们用于向元素添加 CSS 类或样式片段,而不覆盖现有的类或样式:
<tr th:each="prod : ${prods}" class="row" th:classappend="${prodStat.odd}? 'odd'"></tr>
# 5.5 固定值布尔属性
HTML 有布尔属性的概念,这些属性没有值,它们的存在意味着值为 "true"。在 XHTML 中,这些属性只接受一个值,即它们本身。
例如,checked
:
<input type="checkbox" name="option2" checked /> <!-- HTML -->
<input type="checkbox" name="option1" checked="checked" /> <!-- XHTML -->
标准方言包括允许你通过评估条件来设置这些属性的属性,因此,如果评估为 true,则将该属性设置为其固定值,如果评估为 false,则不设置该属性:
<input type="checkbox" name="active" th:checked="${user.active}" />
标准方言中存在的固定值布尔属性如下:
th:async | th:autofocus | th:autoplay |
th:checked | th:controls | th:declare |
th:default | th:defer | th:disabled |
th:formnovalidate | th:hidden | th:ismap |
th:loop | th:multiple | th:novalidate |
th:nowrap | th:open | th:pubdate |
th:readonly | th:required | th:reversed |
th:scoped | th:seamless | th:selected |
# 5.6 设置任何属性的值(默认属性处理器)
Thymeleaf 提供了一个默认属性处理器,它允许我们设置任何属性的值,即使在标准方言中没有定义特定的 th:*
处理器。
例如:
<span th:whatever="${user.name}">...</span>
将产生:
<span whatever="John Apricot">...</span>
# 5.7 支持 HTML5 友好的属性和元素名称
还可以使用完全不同的语法以更符合 HTML5 的方式将处理器应用于你的模板。
<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 Components 规范的一部分)。例如,这可以用于 th:block
(或 th-block
)元素,这将在后面的部分中解释。
重要提示:此语法是对命名空间 th:*
的补充,它不会取代它。目前没有计划在未来弃用命名空间语法。
# 6 迭代
到目前为止,我们已经创建了一个主页、一个用户个人资料页面以及一个让用户订阅我们新闻通讯的页面……但我们的产品呢?为此,我们需要一种方法来迭代集合中的项目,以构建我们的产品页面。
# 6.1 迭代基础
为了在 /WEB-INF/templates/product/list.html
页面中显示产品,我们将使用一个表格。每个产品将显示在一行(一个 <tr>
元素)中,因此对于我们的模板,我们需要创建一个模板行——一个展示我们想要如何显示每个产品的示例——然后指示 Thymeleaf 重复它,每个产品一次。
标准方言为我们提供了一个专门用于此目的的属性:th:each
。
# 使用 th:each
对于我们的产品列表页面,我们需要一个控制器方法,该方法从服务层检索产品列表并将其添加到模板上下文中:
public void process(
final IWebExchange webExchange,
final ITemplateEngine templateEngine,
final Writer writer)
throws Exception {
final ProductService productService = new ProductService();
final List<Product> allProducts = productService.findAll();
final WebContext ctx = new WebContext(webExchange, webExchange.getLocale());
ctx.setVariable("prods", allProducts);
templateEngine.process("product/list", ctx, writer);
}
然后,我们将在模板中使用 th:each
来迭代产品列表:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Good Thymes Virtual Grocery</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" media="all"
href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
</head>
<body>
<h1>Product list</h1>
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
</tr>
<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>
</table>
<p>
<a href="../home.html" th:href="@{/}">Return to home</a>
</p>
</body>
</html>
你上面看到的 prod : ${prods}
属性值的意思是“对于 ${prods}
的结果中的每个元素,重复此模板片段,并使用当前元素作为名为 prod
的变量”。让我们为每个我们看到的元素命名:
- 我们将
${prods}
称为迭代表达式或迭代变量。 - 我们将
prod
称为迭代变量或简称为迭代变量。
请注意,prod
迭代变量的范围是 <tr>
元素,这意味着它可用于内部标签,如 <td>
。
# 可迭代的值
java.util.List
类并不是唯一可以用于 Thymeleaf 中迭代的值。th:each
属性认为以下对象是可迭代的:
- 任何实现
java.util.Iterable
的对象 - 任何实现
java.util.Enumeration
的对象 - 任何实现
java.util.Iterator
的对象,其值将按迭代器返回的顺序使用,而无需将所有值缓存在内存中 - 任何实现
java.util.Map
的对象。在迭代映射时,迭代变量将是java.util.Map.Entry
类 - 任何实现
java.util.stream.Stream
的对象 - 任何数组
- 任何其他对象将被视为包含对象本身的单值列表
# 6.2 保持迭代状态
当使用 th:each
时,Thymeleaf 提供了一种有用的机制来跟踪迭代的状态:状态变量。
状态变量在 th:each
属性中定义,并包含以下数据:
- 当前迭代索引,从 0 开始。这是
index
属性。 - 当前迭代索引,从 1 开始。这是
count
属性。 - 迭代变量中的元素总数。这是
size
属性。 - 每次迭代的迭代变量。这是
current
属性。 - 当前迭代是偶数还是奇数。这是
even/odd
布尔属性。 - 当前迭代是否是第一次迭代。这是
first
布尔属性。 - 当前迭代是否是最后一次迭代。这是
last
布尔属性。
让我们看看如何在前面的示例中使用它:
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<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>
</table>
状态变量(在本例中为 iterStat
)通过在迭代变量本身后写入其名称来在 th:each
属性中定义,两者之间用逗号分隔。与迭代变量一样,状态变量的范围也是由包含 th:each
属性的标签定义的代码片段。
让我们看看处理模板的结果:
<!DOCTYPE html>
<html>
<head>
<title>Good Thymes Virtual Grocery</title>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
<link rel="stylesheet" type="text/css" media="all" href="/gtvg/css/gtvg.css" />
</head>
<body>
<h1>Product list</h1>
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
</tr>
<tr class="odd">
<td>Fresh Sweet Basil</td>
<td>4.99</td>
<td>yes</td>
</tr>
<tr>
<td>Italian Tomato</td>
<td>1.25</td>
<td>no</td>
</tr>
<tr class="odd">
<td>Yellow Bell Pepper</td>
<td>2.50</td>
<td>yes</td>
</tr>
<tr>
<td>Old Cheddar</td>
<td>18.75</td>
<td>yes</td>
</tr>
</table>
<p>
<a href="/gtvg/" shape="rect">Return to home</a>
</p>
</body>
</html>
请注意,我们的迭代状态变量工作得非常完美,仅为奇数行设置了 odd
CSS 类。
如果你没有显式设置状态变量,Thymeleaf 将始终通过将 Stat
后缀添加到迭代变量的名称来为你创建一个:
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
</tr>
<tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
<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>
</table>
# 6.3 通过延迟检索数据优化
有时我们可能希望优化数据集合(例如从数据库中检索)的检索,以便仅在真正使用这些集合时才检索它们。
实际上,这可以应用于任何数据,但考虑到内存中集合可能的大小,检索用于迭代的集合是这种场景中最常见的情况。
为了支持这一点,Thymeleaf 提供了一种延迟加载上下文变量的机制。实现 ILazyContextVariable
接口的上下文变量——最可能通过扩展其默认实现 LazyContextVariable
——将在执行时解析。例如:
context.setVariable(
"users",
new LazyContextVariable<List<User>>() {
@Override
protected List<User> loadValue() {
return databaseRepository.findAllUsers();
}
});
这个变量可以在不知道其延迟性的情况下使用,例如在以下代码中:
<ul>
<li th:each="u : ${users}" th:text="${u.name}">user name</li>
</ul>
但同时,如果 condition
在以下代码中评估为 false
,则该变量永远不会初始化(其 loadValue()
方法永远不会被调用):
<ul th:if="${condition}">
<li th:each="u : ${users}" th:text="${u.name}">user name</li>
</ul>
# 7 条件评估
# 7.1 简单条件判断:if
和 unless
有时你需要模板中的某段内容仅在满足特定条件时才显示。
例如,假设我们希望在产品表中显示一列,展示每个产品的评论数量,并且如果有评论,则提供一个链接到该产品的评论详情页面。
为了实现这一点,我们可以使用 th:if
属性:
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
<td>
<span th:text="${#lists.size(prod.comments)}">2</span> comment/s
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
</td>
</tr>
</table>
这里有很多内容要看,所以我们重点关注重要的一行:
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
这将创建一个指向评论页面的链接(URL 为 /product/comments
),并将 prodId
参数设置为产品的 id
,但仅当产品有评论时才会显示。
让我们看看生成的标记:
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<tr>
<td>Fresh Sweet Basil</td>
<td>4.99</td>
<td>yes</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr class="odd">
<td>Italian Tomato</td>
<td>1.25</td>
<td>no</td>
<td>
<span>2</span> comment/s
<a href="/gtvg/product/comments?prodId=2">view</a>
</td>
</tr>
<tr>
<td>Yellow Bell Pepper</td>
<td>2.50</td>
<td>yes</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr class="odd">
<td>Old Cheddar</td>
<td>18.75</td>
<td>yes</td>
<td>
<span>1</span> comment/s
<a href="/gtvg/product/comments?prodId=4">view</a>
</td>
</tr>
</table>
完美!这正是我们想要的。
需要注意的是,th:if
属性不仅会评估布尔条件。它的功能还稍微超出这个范围,它将按照以下规则评估指定的表达式为 true
:
- 如果值不为空:
- 如果值是布尔值且为
true
。 - 如果值是数字且非零。
- 如果值是字符且非零。
- 如果值是字符串且不是
false
、off
或no
。 - 如果值不是布尔值、数字、字符或字符串。
- 如果值是布尔值且为
- (如果值为空,
th:if
将评估为false
)。
此外,th:if
还有一个反向属性 th:unless
,我们可以在前面的例子中使用它,而不是在 OGNL 表达式中使用 not
:
<a href="comments.html"
th:href="@{/comments(prodId=${prod.id})}"
th:unless="${#lists.isEmpty(prod.comments)}">view</a>
# 7.2 Switch 语句
还有一种方式可以使用 th:switch
/ th:case
属性集来显示条件内容,类似于 Java 中的 switch
结构:
<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
,同一 switch 上下文中的其他 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 需要定义这些部分,即“片段”,可以使用 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
属性将其包含在我们的主页中:
<body>
...
<div th:insert="~{footer :: copy}"></div>
</body>
注意,th:insert
需要一个片段表达式(~{...}
),即一个结果为片段的表达式。
# 片段指定语法
片段表达式的语法非常简单,有三种不同的格式:
"~{templatename::selector}"
:包含在指定模板上应用标记选择器所得到的片段。注意,selector
可以是一个片段名,因此你可以指定像~{footer :: copy}
这样的简单内容。"~{templatename}"
:包含指定名称的完整模板。"~{::selector}"
或"~{this::selector}"
:从同一模板中插入一个片段,匹配selector
。如果在当前模板中找不到,将向最初处理的模板(根模板)遍历模板调用堆栈,直到在某个级别匹配到selector
。
在上述示例中,templatename
和 selector
都可以是完全功能的表达式(甚至可以是条件表达式),例如:
<div th:insert="~{ footer :: (${user.isAdmin}? #{footer.admin} : #{footer.normaluser}) }"></div>
片段可以包含任何 th:*
属性。这些属性将在片段包含到目标模板(带有 th:insert
/th:replace
属性的模板)时进行评估,并且它们能够引用目标模板中定义的任何上下文变量。
# 不使用 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
的区别
th:insert
和 th:replace
的区别是什么?
th:insert
将指定的片段插入为其宿主标签的内容。th:replace
实际上是用指定的片段替换其宿主标签。
例如,以下 HTML 片段:
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
在宿主 <div>
标签中包含两次,如下所示:
<body>
...
<div th:insert="~{footer :: copy}"></div>
<div th:replace="~{footer :: copy}"></div>
</body>
结果将是:
<body>
...
<div>
<footer>© 2011 The Good Thymes Virtual Grocery</footer>
</div>
<footer>© 2011 The Good Thymes Virtual Grocery</footer>
</body>
# 8.2 参数化片段签名
为了创建更“函数式”的模板片段机制,使用 th:fragment
定义的片段可以指定一组参数:
<div th:fragment="frag (onevar,twovar)">
<p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>
这需要使用以下两种语法之一来调用片段:
<div th:replace="~{ ::frag (${value1},${value2}) }">...</div>
<div th:replace="~{ ::frag (onevar=${value1},twovar=${value2}) }">...</div>
注意,在最后一个选项中,顺序并不重要:
<div th:replace="~{ ::frag (twovar=${value2},onevar=${value1}) }">...</div>
# 无片段参数的片段局部变量
即使片段定义如下,没有参数:
<div th:fragment="frag">
...
</div>
我们也可以使用第二种语法来调用它们(只能使用第二种):
<div th:replace="~{::frag (onevar=${value1},twovar=${value2})}"></div>
这相当于 th:replace
和 th:with
的组合:
<div th:replace="~{::frag}" th:with="onevar=${value1},twovar=${value2}"></div>
注意,为片段指定局部变量——无论它是否有参数签名——都不会导致在执行片段之前清空上下文。片段仍然能够像当前一样访问调用模板中使用的所有上下文变量。
# th:assert
用于模板内断言
th:assert
属性可以指定一个逗号分隔的表达式列表,每个表达式都应评估为 true,否则会引发异常。
例如:
<div th:assert="${onevar},(${twovar} != 43)">...</div>
这在验证片段签名中的参数时非常有用:
<header th:fragment="contentheader(title)" th:assert="${!#strings.isEmpty(title)}">...</header>
# 8.3 灵活布局:超越简单的片段插入
由于片段表达式,我们可以为片段指定参数,这些参数不仅仅是文本、数字、Bean 对象,而是标记片段。这使我们能够以这样的方式创建片段,即它们可以通过调用模板中的标记进行丰富,从而形成非常灵活的模板布局机制。
例如,以下片段中的 title
和 links
变量:
<head th:fragment="common_header(title,links)">
<title th:replace="${title}">The awesome application</title>
<!-- 通用样式和脚本 -->
<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>
<!--/* 每页占位符用于额外链接 */-->
<th:block th:replace="${links}" />
</head>
我们可以这样调用这个片段:
<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>
结果将使用调用模板中的实际 <title>
和 <link>
标签作为 title
和 links
变量的值,从而在插入时自定义我们的片段。
# 使用空片段
一个特殊的片段表达式,即空片段(~{}
),可以用于指定无标记。使用前面的示例:
<head th:replace="~{ base :: common_header(~{::title},~{}) }">
<title>Awesome - Main</title>
</head>
注意,片段的第二个参数(links
)设置为空片段,因此不会为 <th:block th:replace="${links}" />
块写入任何内容。
# 使用无操作标记
无操作标记也可以作为片段的参数,如果我们只想让片段使用其当前标记作为默认值。再次使用 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
)设置为无操作(_
),这导致片段中的这部分完全不执行(title
= 无操作)。
# 高级条件插入片段
空片段和无操作标记的可用性使我们能够以非常简单和优雅的方式执行片段的条件插入。
例如,我们可以这样做,以仅在用户是管理员时插入 common :: adminhead
片段,否则插入空片段:
<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
标志——我们可以使用片段本身的存在作为默认操作的条件:
<!-- 如果 "common :: salutation" 片段不存在(或为空),则使用 <div> 的主体。-->
<div th:insert="~{common :: salutation} ?: _">
Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support.
</div>
# 8.4 移除模板片段
回到示例应用程序,让我们重新审视产品列表模板的最后一个版本:
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
<td>
<span th:text="${#lists.size(prod.comments)}">2</span> comment/s
<a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a>
</td>
</tr>
</table>
这段代码作为模板非常好,但作为静态页面(当直接由浏览器打开而不经过 Thymeleaf 处理时),它不会成为一个好的原型。
为什么?因为尽管浏览器可以完美显示,但表格中只有一行,且这一行是模拟数据。作为原型,它看起来不够真实……我们应该有多个产品,需要更多行。
因此,我们添加一些行:
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
<td>
<span th:text="${#lists.size(prod.comments)}">2</span> comment/s
<a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a>
</td>
</tr>
<tr class="odd">
<td>Blue Lettuce</td>
<td>9.55</td>
<td>no</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr>
<td>Mild Cinnamon</td>
<td>1.99</td>
<td>yes</td>
<td>
<span>3</span> comment/s
<a href="comments.html">view</a>
</td>
</tr>
</table>
现在我们有三个产品行,这对于原型来说肯定更好。但是,当我们用 Thymeleaf 处理它时会发生什么?
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<tr>
<td>Fresh Sweet Basil</td>
<td>4.99</td>
<td>yes</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr class="odd">
<td>Italian Tomato</td>
<td>1.25</td>
<td>no</td>
<td>
<span>2</span> comment/s
<a href="/gtvg/product/comments?prodId=2">view</a>
</td>
</tr>
<tr>
<td>Yellow Bell Pepper</td>
<td>2.50</td>
<td>yes</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr class="odd">
<td>Old Cheddar</td>
<td>18.75</td>
<td>yes</td>
<td>
<span>1</span> comment/s
<a href="/gtvg/product/comments?prodId=4">view</a>
</td>
</tr>
<tr class="odd">
<td>Blue Lettuce</td>
<td>9.55</td>
<td>no</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr>
<td>Mild Cinnamon</td>
<td>1.99</td>
<td>yes</td>
<td>
<span>3</span> comment/s
<a href="comments.html">view</a>
</td>
</tr>
</table>
最后两行是模拟行!当然,它们只是模拟行:迭代只应用于第一行,因此没有理由让 Thymeleaf 删除其他两行。
我们需要在模板处理期间删除这两行。让我们在第二和第三个 <tr>
标签上使用 th:remove
属性:
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
<td>
<span th:text="${#lists.size(prod.comments)}">2</span> comment/s
<a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a>
</td>
</tr>
<tr class="odd" th:remove="all">
<td>Blue Lettuce</td>
<td>9.55</td>
<td>no</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr th:remove="all">
<td>Mild Cinnamon</td>
<td>1.99</td>
<td>yes</td>
<td>
<span>3</span> comment/s
<a href="comments.html">view</a>
</td>
</tr>
</table>
处理后的结果将再次看起来像预期的那样:
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
<th>COMMENTS</th>
</tr>
<tr>
<td>Fresh Sweet Basil</td>
<td>4.99</td>
<td>yes</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr class="odd">
<td>Italian Tomato</td>
<td>1.25</td>
<td>no</td>
<td>
<span>2</span> comment/s
<a href="/gtvg/product/comments?prodId=2">view</a>
</td>
</tr>
<tr>
<td>Yellow Bell Pepper</td>
<td>2.50</td>
<td>yes</td>
<td>
<span>0</span> comment/s
</td>
</tr>
<tr class="odd">
<td>Old Cheddar</td>
<td>18.75</td>
<td>yes</td>
<td>
<span>1</span> comment/s
<a href="/gtvg/product/comments?prodId=4">view</a>
</td>
</tr>
</table>
all
值在属性中是什么意思?th:remove
可以根据其值以五种不同的方式行为:
all
:删除包含标签及其所有子元素。body
:不删除包含标签,但删除所有子元素。tag
:删除包含标签,但不删除其子元素。all-but-first
:删除包含标签的所有子元素,除了第一个。none
:不执行任何操作。此值对于动态评估非常有用。
all-but-first
值在原型设计时非常有用,因为它可以让我们节省一些 th:remove="all"
的使用。
# 8.5 布局继承
为了能够将单个文件作为布局,可以使用片段。以下是一个简单布局的示例,使用 th:fragment
和 th:replace
定义了 title
和 content
:
<!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>
在这个文件中,html
标签将被 layout
替换,但在布局中,title
和 content
将分别被 title
和 section
块替换。
如果需要,布局可以由多个片段组成,如 header
和 footer
。
# 9 局部变量
Thymeleaf 将局部变量定义为模板中某个特定片段所定义的变量,并且仅在该片段内可用。
我们已经看到的一个例子是产品列表页面中的 prod
迭代变量:
<tr th:each="prod : ${prods}">
...
</tr>
这个 prod
变量仅在 <tr>
标签的范围内可用。具体来说:
- 它可以在该标签内执行的任何其他
th:*
属性中使用,只要这些属性的优先级低于th:each
(这意味着它们将在th:each
之后执行)。 - 它可以在
<tr>
标签的任何子元素中使用,例如任何<td>
元素。
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>
标签范围内可用。
你可以使用通常的多重赋值语法同时定义多个变量:
<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>
让我们在我们的 Grocery 主页中使用它!还记得我们为输出格式化日期而编写的代码吗?
<p>
Today is:
<span th:text="${#calendars.format(today,'dd MMMM yyyy')}">13 february 2011</span>
</p>
那么,如果我们希望 "dd MMMM yyyy"
实际上依赖于语言环境呢?例如,我们可能希望在 home_en.properties
中添加以下消息:
date.format=MMMM dd',' yyyy
并在 home_es.properties
中添加等效的消息:
date.format=dd 'de' MMMM',' yyyy
现在,让我们使用 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:*
属性时会发生什么?例如:
<ul>
<li th:each="item : ${items}" th:text="${item.description}">Item description here...</li>
</ul>
我们希望 th:each
属性在 th:text
之前执行,以便得到我们想要的结果。但由于 HTML/XML 标准并未赋予标签中属性的书写顺序任何意义,因此必须在属性本身中建立一个优先级机制,以确保其按预期工作。
因此,所有 Thymeleaf 属性都定义了一个数字优先级,用于确定它们在标签中的执行顺序。这个顺序如下:
顺序 | 功能 | 属性 |
---|---|---|
1 | 片段包含 | th:insert th:replace |
2 | 片段迭代 | th:each |
3 | 条件评估 | th:if th:unless th:switch th:case |
4 | 局部变量定义 | th:object th:with |
5 | 通用属性修改 | th:attr th:attrprepend th:attrappend |
6 | 特定属性修改 | th:value th:href th:src ... |
7 | 文本(标签体修改) | th:text th:utext |
8 | 片段指定 | th:fragment |
9 | 片段移除 | th:remove |
这个优先级机制意味着,即使属性位置颠倒,上述迭代片段也会给出完全相同的结果(尽管可读性稍差):
<ul>
<li th:text="${item.description}" th:each="item : ${items}">Item description here...</li>
</ul>
# 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>
<!--*/-->
这对于原型设计时带有大量 <tr>
的表格非常有用:
<table>
<tr th:each="x : ${xs}">
...
</tr>
<!--/*-->
<tr>
...
</tr>
<tr>
...
</tr>
<!--*/-->
</table>
# 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
标签
Thymeleaf 标准方言中包含的唯一元素处理器(不是属性)是 th:block
。
th: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(无需在 <table>
内添加禁止的 <div>
块),并且在浏览器中静态打开作为原型时仍然可以正常工作!
尽管 th:block
本身不会在最终渲染的HTML输出中留下任何痕迹(即它不会生成任何额外的HTML标签),但它允许你在模板中执行各种Thymeleaf属性操作,如条件判断、迭代等。
你可以将 th:block
视为一个隐形的容器,用于包含需要被Thymeleaf处理的属性。例如:
<th:block th:if="${user.isAdmin()}">
<p>Welcome, Admin!</p>
</th:block>
在这个例子中,如果 user.isAdmin()
返回 true
,那么 <p>Welcome, Admin!</p>
就会被渲染到最终的HTML中;否则,整个块都会被忽略。
使用场景:
条件显示:如上面的例子所示,可以使用
th:block
结合th:if
或th:unless
来根据条件控制内容的显示或隐藏。迭代:也可以使用
th:block
来进行循环操作,而不需要额外的HTML标签:<th:block th:each="item : ${items}"> <p th:text="${item.name}">Item name here...</p> <p th:text="${item.description}">Item description here...</p> </th:block>
在这个例子中,对于
items
列表中的每个item
,都会生成相应的段落而不添加额外的HTML结构。局部变量声明:通过
th:with
属性来定义局部变量,可以在不影响HTML结构的情况下简化表达式或数据准备:<th:block th:with="total=${item.price * item.quantity}"> <p>Total price: <span th:text="${total}">Total here...</span></p> </th:block>
th:block
提供了一种灵活且干净的方式来应用Thymeleaf的逻辑处理能力,同时保持了输出HTML的简洁和语义准确性。这使得它成为优化模板代码结构的一个强大工具。
# 12 内联
# 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 = 'This is <b>great!</b>'
,给定以下片段:
<p>The message is "[(${msg})]"</p>
结果将不会转义 <b>
标签,因此:
<p>The message is "This is <b>great!</b>"</p>
而如果使用转义:
<p>The message is "[[${msg}]]"</p>
结果将被 HTML 转义:
<p>The message is "This is <b>great!</b>"</p>
需要注意的是,文本内联默认是启用的,作用在标记的标签体内,而不是标签本身,因此我们无需手动启用它。
# 内联与自然模板
如果你来自其他模板引擎,可能会问:为什么我们不从一开始就这样做呢?这比所有的 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>
这将导致:
<p>A double array looks like this: [[1, 2, 3], [4, 5]]!</p>
# 12.2 文本内联
文本内联与我们刚刚看到的表达式内联非常相似,但它实际上增加了更多的功能。它必须通过 th:inline="text"
显式启用。
文本内联不仅允许我们使用相同的内联表达式,而且实际上将标签体视为在 TEXT
模板模式下处理的模板,这允许我们执行基于文本的模板逻辑(不仅仅是输出表达式)。
我们将在下一章关于文本模板模式的部分中进一步讨论这一点。
# 12.3 JavaScript 内联
JavaScript 内联允许在 HTML
模板模式下更好地集成 JavaScript <script>
块。
与文本内联类似,这实际上等同于将脚本内容视为在 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}
表达式作为转义。如果我们使用非转义:
<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 自然模板的方式!
# 高级内联评估和 JavaScript 序列化
关于 JavaScript 内联的一个重要注意事项是,这种表达式评估是智能的,并且不仅限于字符串。Thymeleaf 将正确地将以下类型的对象写入 JavaScript 语法:
- 字符串
- 数字
- 布尔值
- 数组
- 集合
- 映射
- Bean(具有 getter 和 setter 方法的对象)
例如,如果我们有以下代码:
<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
接口来完成的,该接口可以在模板引擎使用的 StandardDialect
实例中进行配置。
默认的 JS 序列化机制会在类路径中查找 Jackson 库 (opens new window),如果存在,则使用它。如果不存在,它将应用一个内置的序列化机制,该机制涵盖了大多数场景的需求并产生类似的结果(但灵活性较低)。
# 12.4 CSS 内联
Thymeleaf 还允许在 CSS <style>
标签中使用内联,例如:
<style th:inline="css">
.[[${classname}]] {
text-align: [[${align}]];
}
</style>
例如,假设我们有两个变量设置为两个不同的 String
值:
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
。
# 高级功能:CSS 自然模板等
与之前对 JavaScript 的解释类似,CSS 内联还允许我们的 <style>
标签在静态和动态情况下都能工作,即通过将内联表达式包装在注释中来实现CSS 自然模板。例如:
<style th:inline="css">
.main\ elems {
text-align: /*[[${align}]]*/ left;
}
</style>
# 13 文本模板模式
# 13.1 文本语法
Thymeleaf 的三种模板模式被认为是文本模式:TEXT
、JAVASCRIPT
和 CSS
。这与标记模板模式(HTML
和 XML
)不同。
文本模板模式与标记模板模式的关键区别在于,在文本模板中没有标签可以插入逻辑(以属性的形式),因此我们必须依赖其他机制。
第一个也是最基本的机制是内联,我们已经在上一章中详细讨论过。内联语法是在文本模板模式下输出表达式结果的最简单方式,因此这是一个完全有效的文本邮件模板:
Dear [(${name})],
Please find attached the results of the report you requested
with name "[(${report.name})]".
Sincerely,
The Reporter.
即使没有标签,上面的例子也是一个完整且有效的 Thymeleaf 模板,可以在 TEXT
模板模式下执行。
但是,为了包含比简单的输出表达式更复杂的逻辑,我们需要一种新的非基于标签的语法:
[# th:each="item : ${items}"]
- [(${item})]
[/]
这实际上是以下更冗长形式的简写:
[#th:block th:each="item : ${items}"]
- [#th:block th:utext="${item}" /]
[/th:block]
请注意,这种新语法基于元素(即可处理的标签),这些元素声明为 [#element ...]
而不是 <element ...>
。元素像 [#element ...]
一样打开,像 [/element]
一样关闭,独立标签可以通过在打开元素中加上 /
来声明,几乎等同于 XML 标签:[#element ... /]
。
标准方言仅包含一个这些元素的处理器:我们已经知道的 th:block
,尽管我们可以在我们的方言中扩展它并以通常的方式创建新元素。此外,th:block
元素([#th:block ...] ... [/th:block]
)可以缩写为空字符串([# ...] ... [/]
),因此上面的块实际上等同于:
[# th:each="item : ${items}"]
- [# th:utext="${item}" /]
[/]
由于 [# th:utext="${item}" /]
等同于一个内联非转义表达式,我们可以直接使用它以减少代码量。因此,我们最终得到了上面看到的第一个代码片段:
[# th:each="item : ${items}"]
- [(${item})]
[/]
请注意,文本语法要求元素完全平衡(没有未关闭的标签)并且属性必须用引号括起来——它更类似于 XML 风格而不是 HTML 风格。
让我们看一个更完整的 TEXT
模板示例,一个纯文本邮件模板:
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
模板模式下,我们处理一个 greeter.js
文件作为文本模板,并从我们的 HTML 页面调用其结果。请注意,这不是 HTML 模板中的 <script>
块,而是一个独立的 .js
文件,作为模板进行处理:
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
模板中的 text
模式内联)发生交互,Thymeleaf 3.0 允许在其文本语法中的元素属性进行转义。因此:
TEXT
模板模式中的属性将被 HTML 反转义。JAVASCRIPT
模板模式中的属性将被 JavaScript 反转义。CSS
模板模式中的属性将被 CSS 反转义。
因此,这在 TEXT
模式模板中是完全可以的(注意 >
):
[# th:if="${120&amp;lt;user.age}"]
Congratulations!
[/]
当然,>
在真正的文本模板中没有意义,但如果我们在处理一个包含上述代码的 th:inline="text"
块的 HTML 模板时,我们希望确保我们的浏览器在静态打开文件作为原型时不会将 <user.age
视为开放标签的名称,那么这是一个好主意。
# 13.2 可扩展性
这种语法的一个优点是它与标记语法一样可扩展。开发者仍然可以定义自己的方言,使用自定义元素和属性,为它们应用前缀(可选),然后在文本模板模式中使用它们:
[#myorg:dosomething myorg:importantattr="211"]some text[/myorg:dosomething]
# 13.3 仅原型注释块:添加代码
JAVASCRIPT
和 CSS
模板模式(不适用于 TEXT
)允许在特殊注释语法 /*[+...+]*/
中包含代码,以便 Thymeleaf 在处理模板时自动取消注释这些代码:
var x = 23;
/*[+
var msg = "This is a working application";
+]*/
var f = function() {
...
将执行为:
var x = 23;
var msg = "This is a working application";
var f = function() {
...
你可以在这些注释中包含表达式,它们将被评估:
var x = 23;
/*[+
var msg = "Hello, " + [[${session.user.name}]];
+]*/
var f = function() {
...
# 13.4 解析器级注释块:删除代码
与仅原型注释块类似,所有三种文本模板模式(TEXT
、JAVASCRIPT
和 CSS
)都可以指示 Thymeleaf 删除特殊标记 /*[- */
和 /* -]*/
之间的代码,例如:
var x = 23;
/*[- */
var msg = "This is shown only when executed statically!";
/* -]*/
var f = function() {
...
或者在 TEXT
模式下:
...
/*[- Note the user is obtained from the session, which must exist -]*/
Welcome [(${session.user.name})]!
...
# 13.5 自然的 JavaScript 和 CSS 模板
正如上一章所见,JavaScript 和 CSS 内联提供了在 JavaScript/CSS 注释中包含内联表达式的可能性,例如:
...
var username = /*[[${session.user.name}]]*/ "Sebastian Lychee";
...
...这是有效的 JavaScript,执行后可能看起来像:
...
var username = "John Apricot";
...
事实上,这种将内联表达式包含在注释中的技巧可以用于整个文本模式语法:
/*[# th:if="${user.admin}"]*/
alert('Welcome admin');
/*[/]*/
上面的代码中的警告将在模板静态打开时显示——因为它是 100% 有效的 JavaScript——并且在用户是管理员时模板运行时也会显示。它等同于:
[# th:if="${user.admin}"]
alert('Welcome admin');
[/]
...这实际上是初始版本在模板解析期间转换成的代码。
然而,请注意,将元素包裹在注释中不会清理它们所在的代码行(直到找到 ;
为止),而内联输出表达式会这样做。这种行为仅保留给内联输出表达式。
因此,Thymeleaf 3.0 允许开发复杂的 JavaScript 脚本和 CSS 样式表作为自然模板,既可以作为原型,也可以作为工作模板。
# 14 为我们的杂货店添加更多页面
现在我们已经了解了很多关于使用 Thymeleaf 的知识,我们可以为我们的网站添加一些新页面来管理订单。
请注意,我们将专注于 HTML 代码,但如果你想查看相应的控制器,可以查看打包的源代码。
# 14.1 订单列表
让我们从创建一个订单列表页面开始,/WEB-INF/templates/order/list.html
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Good Thymes Virtual Grocery</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" media="all" href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
</head>
<body>
<h1>Order list</h1>
<table>
<tr>
<th>DATE</th>
<th>CUSTOMER</th>
<th>TOTAL</th>
<th></th>
</tr>
<tr th:each="o : ${orders}" th:class="${oStat.odd}? 'odd'">
<td th:text="${#calendars.format(o.date,'dd/MMM/yyyy')}">13 jan 2011</td>
<td th:text="${o.customer.name}">Frederic Tomato</td>
<td th:text="${#aggregates.sum(o.orderLines.{purchasePrice * amount})}">23.32</td>
<td>
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
</td>
</tr>
</table>
<p>
<a href="../home.html" th:href="@{/}">Return to home</a>
</p>
</body>
</html>
这里没有什么会让我们感到惊讶的,除了这一点 OGNL 魔法:
<td th:text="${#aggregates.sum(o.orderLines.{purchasePrice * amount})}">23.32</td>
它的作用是,对于订单中的每个订单行(OrderLine
对象),将其 purchasePrice
和 amount
属性相乘(通过调用相应的 getPurchasePrice()
和 getAmount()
方法),并将结果返回到一个数字列表中,然后由 #aggregates.sum(...)
函数聚合以获得订单总价。
你一定会爱上 OGNL 的强大功能。
# 14.2 订单详情
现在来看订单详情页面,在这个页面中我们将大量使用星号语法:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Good Thymes Virtual Grocery</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" media="all" href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
</head>
<body th:object="${order}">
<h1>Order details</h1>
<div>
<p><b>Code:</b> <span th:text="*{id}">99</span></p>
<p>
<b>Date:</b>
<span th:text="*{#calendars.format(date,'dd MMM yyyy')}">13 jan 2011</span>
</p>
</div>
<h2>Customer</h2>
<div th:object="*{customer}">
<p><b>Name:</b> <span th:text="*{name}">Frederic Tomato</span></p>
<p>
<b>Since:</b>
<span th:text="*{#calendars.format(customerSince,'dd MMM yyyy')}">1 jan 2011</span>
</p>
</div>
<h2>Products</h2>
<table>
<tr>
<th>PRODUCT</th>
<th>AMOUNT</th>
<th>PURCHASE PRICE</th>
</tr>
<tr th:each="ol,row : *{orderLines}" th:class="${row.odd}? 'odd'">
<td th:text="${ol.product.name}">Strawberries</td>
<td th:text="${ol.amount}" class="number">3</td>
<td th:text="${ol.purchasePrice}" class="number">23.32</td>
</tr>
</table>
<div>
<b>TOTAL:</b>
<span th:text="*{#aggregates.sum(orderLines.{purchasePrice * amount})}">35.23</span>
</div>
<p>
<a href="list.html" th:href="@{/order/list}">Return to order list</a>
</p>
</body>
</html>
这里并没有太多新东西,除了这个嵌套对象选择:
<body th:object="${order}">
...
<div th:object="*{customer}">
<p><b>Name:</b> <span th:text="*{name}">Frederic Tomato</span></p>
...
</div>
...
</body>
这使得 *{name}
等同于:
<p><b>Name:</b> <span th:text="${order.customer.name}">Frederic Tomato</span></p>
# 15 更多关于配置的内容
# 15.1 模板解析器
在我们的 Good Thymes Virtual Grocery 项目中,我们选择了一个名为 WebApplicationTemplateResolver
的 ITemplateResolver
实现,它允许我们从应用程序资源(在基于 Servlet 的 Web 应用程序中为 Servlet Context)中获取模板。
除了可以通过实现 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(甚至是非本地的 URL),例如:return (new URL(template)).openStream();
org.thymeleaf.templateresolver.StringTemplateResolver
,它直接将模板解析为指定的String
(或 模板名称,在这种情况下,它显然不仅仅是一个名称):return new StringReader(templateName);
所有预打包的 ITemplateResolver
实现都允许相同的配置参数,包括:
前缀和后缀(如之前所见):
templateResolver.setPrefix("/WEB-INF/templates/"); templateResolver.setSuffix(".html");
模板别名,允许使用不直接对应文件名的模板名称。如果同时存在后缀/前缀和别名,别名将在前缀/后缀之前应用:
templateResolver.addTemplateAlias("adminHome", "profiles/admin/home"); templateResolver.setTemplateAliases(aliasesMap);
读取模板时应用的编码:
templateResolver.setCharacterEncoding("UTF-8");
使用的模板模式:
// 默认是 HTML templateResolver.setTemplateMode("XML");
模板缓存的默认模式,以及定义特定模板是否可缓存的模式:
// 默认是 true templateResolver.setCacheable(false); templateResolver.getCacheablePatternSpec().addPattern("/users/*");
解析模板缓存项的 TTL(以毫秒为单位)。如果未设置,唯一从缓存中删除条目的方法是超过缓存的最大大小(最旧的条目将被删除):
// 默认没有 TTL(只有缓存大小超过时才会删除条目) templateResolver.setCacheTTLMs(60000L);
Thymeleaf + Spring 集成包提供了一个
SpringResourceTemplateResolver
实现,它使用 Spring 的所有基础设施来访问和读取应用程序中的资源,这是在启用 Spring 的应用程序中推荐的实现。
# 链式模板解析器
此外,模板引擎可以指定多个模板解析器,在这种情况下,可以为模板解析建立顺序,以便如果第一个解析器无法解析模板,则询问第二个解析器,依此类推:
ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
WebApplicationTemplateResolver webApplicationTemplateResolver =
new WebApplicationTemplateResolver(application);
webApplicationTemplateResolver.setOrder(Integer.valueOf(2));
templateEngine.addTemplateResolver(classLoaderTemplateResolver);
templateEngine.addTemplateResolver(webApplicationTemplateResolver);
当应用多个模板解析器时,建议为每个模板解析器指定模式,以便 Thymeleaf 可以快速丢弃那些不打算解析模板的解析器,从而提高性能。这样做不是必需的,但建议:
ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
// 这个类加载器不会为不匹配这些模式的模板被询问
classLoaderTemplateResolver.getResolvablePatternSpec().addPattern("/layout/*.html");
classLoaderTemplateResolver.getResolvablePatternSpec().addPattern("/menu/*.html");
WebApplicationTemplateResolver webApplicationTemplateResolver =
new WebApplicationTemplateResolver(application);
webApplicationTemplateResolver.setOrder(Integer.valueOf(2));
如果未指定这些 可解析模式,我们将依赖于我们使用的每个 ITemplateResolver
实现的特定功能。请注意,并非所有实现都能够在解析之前确定模板的存在,因此可能始终将模板视为 可解析 并中断解析链(不允许其他解析器检查相同的模板),然后无法读取实际资源。
所有与 Thymeleaf 核心一起提供的 ITemplateResolver
实现都包含一种机制,允许我们在考虑资源 可解析 之前 真正检查 资源是否存在。它是 checkExistence
标志,其工作方式如下:
ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
classLoaderTemplateResolver.setCheckExistence(true);
这个 checkExistence
标志强制解析器在解析阶段执行资源的 真正检查(如果存在检查返回 false,则允许调用链中的下一个解析器)。虽然这在每种情况下听起来都不错,但在大多数情况下,这意味着对资源本身进行双重访问(一次用于检查是否存在,另一次用于读取它),并且在某些情况下可能会成为性能问题,例如基于远程 URL 的模板资源——尽管使用模板缓存可以大大缓解这个潜在的性能问题(在这种情况下,模板仅在第一次访问时被 解析)。
# 15.2 消息解析器
我们没有为我们的 Grocery 应用程序显式指定消息解析器实现,正如之前解释的那样,这意味着使用的实现是 org.thymeleaf.messageresolver.StandardMessageResolver
对象。
StandardMessageResolver
是 IMessageResolver
接口的标准实现,但如果我们愿意,我们可以创建自己的实现,以适应我们应用程序的特定需求。
Thymeleaf + Spring 集成包默认提供了一个
IMessageResolver
实现,它使用 Spring 的标准方式检索外部化消息,通过在 Spring 应用上下文中声明的MessageSource
bean。
# 标准消息解析器
那么 StandardMessageResolver
是如何查找特定模板中请求的消息的呢?
如果模板名称是 home
,并且它位于 /WEB-INF/templates/home.html
,并且请求的区域设置是 gl_ES
,那么此解析器将按照以下顺序在这些文件中查找消息:
/WEB-INF/templates/home_gl_ES.properties
/WEB-INF/templates/home_gl.properties
/WEB-INF/templates/home.properties
有关完整的消息解析机制如何工作的更多详细信息,请参阅 StandardMessageResolver
类的 JavaDoc 文档。
# 配置消息解析器
如果我们想向模板引擎添加一个消息解析器(或多个)怎么办?很简单:
// 用于设置一个
templateEngine.setMessageResolver(messageResolver);
// 用于设置多个
templateEngine.addMessageResolver(messageResolver);
为什么我们希望有多个消息解析器?与模板解析器相同的原因:消息解析器是有序的,如果第一个解析器无法解析特定消息,则将询问第二个解析器,然后是第三个,依此类推。
# 15.3 转换服务
通过 双括号 语法(${{ ... }}
)执行数据转换和格式化操作的 转换服务 实际上是标准方言的功能,而不是 Thymeleaf 模板引擎本身的功能。
因此,配置它的方法是将我们自定义的 IStandardConversionService
接口实现直接设置到模板引擎中配置的 StandardDialect
实例中。例如:
IStandardConversionService customConversionService = ...
StandardDialect dialect = new StandardDialect();
dialect.setConversionService(customConversionService);
templateEngine.setDialect(dialect);
请注意,thymeleaf-spring5 和 thymeleaf-spring6 包包含
SpringStandardDialect
,并且此方言已经预先配置了一个IStandardConversionService
实现,该实现将 Spring 自己的 转换服务 基础设施集成到 Thymeleaf 中。
# 15.4 日志记录
Thymeleaf 非常重视日志记录,并始终尝试通过其日志记录接口提供最大量的有用信息。
使用的日志记录库是 slf4j
,它实际上充当了我们可能希望在应用程序中使用的任何日志记录实现(例如 log4j
)的桥梁。
Thymeleaf 类将记录 TRACE
、DEBUG
和 INFO
级别的信息,具体取决于我们所需的详细程度,除了常规日志记录外,它还将使用与 TemplateEngine
类关联的三个特殊日志记录器,我们可以为不同的目的单独配置它们:
org.thymeleaf.TemplateEngine.CONFIG
将在初始化期间输出库的详细配置。org.thymeleaf.TemplateEngine.TIMER
将输出处理每个模板所花费的时间信息(用于基准测试!)org.thymeleaf.TemplateEngine.cache
是一组日志记录器的前缀,它们输出有关缓存的特定信息。尽管缓存日志记录器的名称可由用户配置,因此可能会更改,但默认情况下它们是: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 的工作原理依赖于一组解析器(用于标记和文本),这些解析器将模板解析为事件序列(如打开标签、文本、关闭标签、注释等),以及一系列处理器(每个处理器对应一种需要应用的行为),这些处理器通过将原始模板与数据结合来修改解析后的事件序列,从而生成我们期望的结果。
默认情况下,Thymeleaf 还包括一个缓存,用于存储解析后的模板;即在处理模板文件之前,从读取和解析模板文件中生成的事件序列。这在 Web 应用程序中尤其有用,并基于以下概念:
- 输入/输出几乎总是任何应用程序中最慢的部分。相比之下,内存处理速度极快。
- 克隆现有的内存事件序列总是比读取模板文件、解析它并为其创建新的事件序列要快得多。
- Web 应用程序通常只有几十个模板。
- 模板文件大小适中,并且在应用程序运行时不会被修改。
所有这些都表明,在 Web 应用程序中缓存最常用的模板是可行的,而不会浪费大量内存,同时还可以节省大量用于输入/输出操作的时间,这些操作针对的是一小部分实际上从未更改的文件。
那么,我们如何控制这个缓存呢?首先,我们之前已经了解到,我们可以在模板解析器上启用或禁用缓存,甚至可以仅针对特定模板进行操作:
// 默认值为 true
templateResolver.setCacheable(false);
templateResolver.getCacheablePatternSpec().addPattern("/users/*");
此外,我们还可以通过建立自己的 Cache Manager 对象来修改其配置,该对象可以是默认的 StandardCacheManager
实现的实例:
// 默认值为 200
StandardCacheManager cacheManager = new StandardCacheManager();
cacheManager.setTemplateCacheMaxSize(100);
...
templateEngine.setCacheManager(cacheManager);
有关配置缓存的更多信息,请参阅 org.thymeleaf.cache.StandardCacheManager
的 Javadoc API。
可以从模板缓存中手动删除条目:
// 完全清除缓存
templateEngine.clearTemplateCache();
// 从缓存中清除特定模板
templateEngine.clearTemplateCacheFor("/users/userList");
# 17 解耦模板逻辑
# 17.1 解耦逻辑:概念
到目前为止,我们已经为我们的杂货店项目使用了以 常规方式 完成的模板,逻辑以属性的形式插入到模板中。
但 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
块中有很多 <attr>
标签。这些 <attr>
标签通过它们的 sel
属性对原始模板的节点执行 属性注入,这些属性包含 Thymeleaf 标记选择器(实际上是 AttoParser 标记选择器)。
还要注意,<attr>
标签可以嵌套,以便它们的选择器 追加。例如,上面的 sel="/tr[0]"
将被处理为 sel="#usersTable/tr[0]"
。而用户名的 <td>
选择器将被处理为 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,从而从设计的角度来看具有更好的可维护性。
当然,设计人员或开发人员之间仍然需要一些 约定 —— 例如,用户 <table>
将需要一个 id="usersTable"
—— 但在许多情况下,纯 HTML 模板将是设计和开发团队之间更好的沟通工具。
# 17.2 配置解耦模板
# 启用解耦模板
默认情况下,不会为每个模板期望解耦逻辑。相反,配置的模板解析器(ITemplateResolver
的实现)需要明确标记它们解析的模板为 使用解耦逻辑。
除了 StringTemplateResolver
(不允许解耦逻辑)之外,所有其他开箱即用的 ITemplateResolver
实现都提供了一个名为 useDecoupledLogic
的标志,该标志将标记该解析器解析的所有模板可能具有部分或全部逻辑存在于单独的资源中:
final WebApplicationTemplateResolver templateResolver =
new WebApplicationTemplateResolver(application);
...
templateResolver.setUseDecoupledLogic(true);
# 混合耦合和解耦逻辑
解耦模板逻辑在启用时并不是必需的。启用时,它意味着引擎将 查找 包含解耦逻辑的资源,如果存在,则解析并将其与原始模板合并。如果解耦逻辑资源不存在,则不会抛出错误。
此外,在同一个模板中,我们可以混合使用 耦合 和 解耦 逻辑,例如在原始模板文件中添加一些 Thymeleaf 属性,但将其他属性留给单独的解耦逻辑文件。最常见的情况是使用新的(在 v3.0 中)th:ref
属性。
# 17.3 th:ref
属性
th:ref
只是一个标记属性。从处理的角度来看,它不做任何事情,并且在模板处理时简单地消失,但它的用处在于它充当 标记引用,即它可以像 标签名 或 片段(th:fragment
)一样通过名称从 标记选择器 解析。
所以如果我们有这样的选择器:
<attr sel="whatever" .../>
这将匹配:
- 任何
<whatever>
标签。 - 任何带有
th:fragment="whatever"
属性的标签。 - 任何带有
th:ref="whatever"
属性的标签。
th:ref
相对于例如使用纯 HTML id
属性的优势是什么?仅仅是因为我们可能不想在我们的标签中添加那么多 id
和 class
属性作为 逻辑锚点,这可能会 污染 我们的输出。
同样,th:ref
的缺点是什么?显然,我们将在模板中添加一些 Thymeleaf 逻辑(“逻辑”)。
请注意,th:ref
属性的适用性 不仅限于解耦逻辑模板文件:它在其他类型的场景中同样适用,例如在片段表达式(~{...}
)中。
# 17.4 解耦模板的性能影响
影响非常小。当一个解析的模板被标记为使用解耦逻辑并且它没有被缓存时,模板逻辑资源将首先被解析、解析并处理为内存中的指令序列:基本上是要注入到每个标记选择器的属性列表。
但这是唯一需要的 额外步骤,因为在此之后,真正的模板将被解析,并且在解析时,这些属性将被解析器本身 即时 注入,这要归功于 AttoParser 中先进的节点选择能力。因此,解析后的节点将从解析器中出来,就好像它们已经在原始模板文件中写入了注入的属性一样。
最大的优势是什么?当一个模板被配置为缓存时,它将被缓存为已经包含注入的属性。因此,对于可缓存的模板,一旦它们被缓存,使用 解耦模板 的开销将绝对为 零。
# 17.5 解耦逻辑的解析
Thymeleaf 解析与每个模板对应的解耦逻辑资源的方式可由用户配置。它由一个扩展点 org.thymeleaf.templateparser.markup.decoupled.IDecoupledTemplateLogicResolver
确定,该扩展点提供了一个 默认实现:StandardDecoupledTemplateLogicResolver
。
这个标准实现做了什么?
- 首先,它将
prefix
和suffix
应用于模板资源的 基本名称(通过其ITemplateResource#getBaseName()
方法获得)。前缀和后缀都可以配置,默认情况下,前缀为空,后缀为.th.xml
。 - 其次,它要求模板资源通过其
ITemplateResource#relative(String relativeLocation)
方法解析具有计算名称的 相对资源。
可以在 TemplateEngine
中轻松配置要使用的 IDecoupledTemplateLogicResolver
的特定实现:
final StandardDecoupledTemplateLogicResolver decoupledresolver =
new StandardDecoupledTemplateLogicResolver();
decoupledResolver.setPrefix("../viewlogic/");
...
templateEngine.setDecoupledTemplateLogicResolver(decoupledResolver);
# 18 附录 A:表达式基础对象
一些对象和变量映射始终可以被调用。让我们来看看它们:
# 基础对象
#ctx : 上下文对象。根据我们的环境(独立或 Web),它是
org.thymeleaf.context.IContext
或org.thymeleaf.context.IWebContext
的实现。注意
#vars
和#root
是同一对象的同义词,但建议使用#ctx
。
/*
* ======================================================================
* 参见 org.thymeleaf.context.IContext 类的 Javadoc API
* ======================================================================
*/
${#ctx.locale}
${#ctx.variableNames}
/*
* ======================================================================
* 参见 org.thymeleaf.context.IWebContext 类的 Javadoc API
* ======================================================================
*/
${#ctx.request}
${#ctx.response}
${#ctx.session}
${#ctx.servletContext}
- #locale : 直接访问与当前请求关联的
java.util.Locale
。
${#locale}
# Web 上下文命名空间(用于请求/会话属性等)
在 Web 环境中使用 Thymeleaf 时,我们可以使用一系列快捷方式来访问请求参数、会话属性和应用程序属性:
注意这些不是 上下文对象,而是作为变量添加到上下文中的映射,因此我们无需使用
#
来访问它们。在某种程度上,它们充当 命名空间。
- param : 用于检索请求参数。
${param.foo}
是一个包含foo
请求参数值的String[]
,因此通常使用${param.foo[0]}
来获取第一个值。
/*
* ============================================================================
* 参见 org.thymeleaf.context.WebRequestParamsVariablesMap 类的 Javadoc API
* ============================================================================
*/
${param.foo} // 检索请求参数 'foo' 的 String[] 值
${param.size()}
${param.isEmpty()}
${param.containsKey('foo')}
...
- session : 用于检索会话属性。
/*
* ======================================================================
* 参见 org.thymeleaf.context.WebSessionVariablesMap 类的 Javadoc API
* ======================================================================
*/
${session.foo} // 检索会话属性 'foo'
${session.size()}
${session.isEmpty()}
${session.containsKey('foo')}
...
- application : 用于检索应用程序/ Servlet 上下文属性。
/*
* =============================================================================
* 参见 org.thymeleaf.context.WebServletContextVariablesMap 类的 Javadoc API
* =============================================================================
*/
${application.foo} // 检索 ServletContext 属性 'foo'
${application.size()}
${application.isEmpty()}
${application.containsKey('foo')}
...
注意,访问请求属性时不需要指定命名空间(与 请求参数 不同),因为所有请求属性都自动作为变量添加到上下文根中:
${myRequestAttribute}
# 19 附录 B: 表达式工具对象
# 执行信息
- #execInfo : 提供有关在 Thymeleaf 标准表达式中处理的模板的有用信息的表达式对象。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.ExecutionInfo 类的 Javadoc API
* ======================================================================
*/
/*
* 返回 'leaf' 模板的名称和模式。这意味着返回的是从中解析出正在处理的事件的模板。
* 因此,如果这段代码不在根模板 "A" 中,而是在从另一个名为 "B" 的模板插入到 "A" 中的片段中,
* 这将返回 "B" 作为名称,以及 B 的模板模式。
*/
${#execInfo.templateName}
${#execInfo.templateMode}
/*
* 返回 'root' 模板的名称和模式。这意味着返回的是模板引擎最初要求处理的模板。
* 因此,如果这段代码不在根模板 "A" 中,而是在从另一个名为 "B" 的模板插入到 "A" 中的片段中,
* 这仍将返回 "A" 和 A 的模板模式。
*/
${#execInfo.processedTemplateName}
${#execInfo.processedTemplateMode}
/*
* 返回正在处理的模板的堆栈(实际上是 List<String> 或 List<TemplateMode>)。
* 第一个元素将是 'processedTemplate'(根模板),最后一个元素将是 'leaf' 模板,
* 中间将显示所有以嵌套方式插入到根模板中的片段,以到达 'leaf' 模板。
*/
${#execInfo.templateNames}
${#execInfo.templateModes}
/*
* 返回正在处理的模板的堆栈,类似于 'templateNames' 和 'templateModes',
* 但返回的是包含完整模板元数据的 List<TemplateData>。
*/
${#execInfo.templateStack}
# 消息
- #messages : 用于在变量表达式中获取外部化消息的实用方法,与使用
#{...}
语法获取消息的方式相同。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Messages 类的 Javadoc API
* ======================================================================
*/
/*
* 获取外部化消息。可以接收单个键、键加参数,或键的数组/列表/集合(在这种情况下,它将返回外部化消息的数组/列表/集合)。
* 如果找不到消息,则返回默认消息(如 '??msgKey??')。
*/
${#messages.msg('msgKey')}
${#messages.msg('msgKey', param1)}
${#messages.msg('msgKey', param1, param2)}
${#messages.msg('msgKey', param1, param2, param3)}
${#messages.msgWithParams('msgKey', new Object[] {param1, param2, param3, param4})}
${#messages.arrayMsg(messageKeyArray)}
${#messages.listMsg(messageKeyList)}
${#messages.setMsg(messageKeySet)}
/*
* 获取外部化消息或 null。如果找不到指定键的消息,则返回 null 而不是默认消息。
*/
${#messages.msgOrNull('msgKey')}
${#messages.msgOrNull('msgKey', param1)}
${#messages.msgOrNull('msgKey', param1, param2)}
${#messages.msgOrNull('msgKey', param1, param2, param3)}
${#messages.msgOrNullWithParams('msgKey', new Object[] {param1, param2, param3, param4})}
${#messages.arrayMsgOrNull(messageKeyArray)}
${#messages.listMsgOrNull(messageKeyList)}
${#messages.setMsgOrNull(messageKeySet)}
# URIs/URLs
- #uris : 用于在 Thymeleaf 标准表达式中执行 URI/URL 操作(尤其是转义/取消转义)的实用对象。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Uris 类的 Javadoc API
* ======================================================================
*/
/*
* 作为 URI/URL 路径转义/取消转义
*/
${#uris.escapePath(uri)}
${#uris.escapePath(uri, encoding)}
${#uris.unescapePath(uri)}
${#uris.unescapePath(uri, encoding)}
/*
* 作为 URI/URL 路径段(在 '/' 符号之间)转义/取消转义
*/
${#uris.escapePathSegment(uri)}
${#uris.escapePathSegment(uri, encoding)}
${#uris.unescapePathSegment(uri)}
${#uris.unescapePathSegment(uri, encoding)}
/*
* 作为片段标识符(#frag)转义/取消转义
*/
${#uris.escapeFragmentId(uri)}
${#uris.escapeFragmentId(uri, encoding)}
${#uris.unescapeFragmentId(uri)}
${#uris.unescapeFragmentId(uri, encoding)}
/*
* 作为查询参数(?var=value)转义/取消转义
*/
${#uris.escapeQueryParam(uri)}
${#uris.escapeQueryParam(uri, encoding)}
${#uris.unescapeQueryParam(uri)}
${#uris.unescapeQueryParam(uri, encoding)}
# 转换
- #conversions : 允许在模板的任何位置执行 转换服务 的实用对象。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Conversions 类的 Javadoc API
* ======================================================================
*/
/*
* 将 'object' 值转换为指定的类。
*/
${#conversions.convert(object, 'java.util.TimeZone')}
${#conversions.convert(object, targetClass)}
# 日期
- #dates : 用于
java.util.Date
对象的实用方法。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Dates 类的 Javadoc API
* ======================================================================
*/
/*
* 使用标准区域设置格式格式化日期。也适用于数组、列表或集合。
*/
${#dates.format(date)}
${#dates.arrayFormat(datesArray)}
${#dates.listFormat(datesList)}
${#dates.setFormat(datesSet)}
/*
* 使用 ISO8601 格式格式化日期。也适用于数组、列表或集合。
*/
${#dates.formatISO(date)}
${#dates.arrayFormatISO(datesArray)}
${#dates.listFormatISO(datesList)}
${#dates.setFormatISO(datesSet)}
/*
* 使用指定模式格式化日期。也适用于数组、列表或集合。
*/
${#dates.format(date, 'dd/MMM/yyyy HH:mm')}
${#dates.arrayFormat(datesArray, 'dd/MMM/yyyy HH:mm')}
${#dates.listFormat(datesList, 'dd/MMM/yyyy HH:mm')}
${#dates.setFormat(datesSet, 'dd/MMM/yyyy HH:mm')}
/*
* 获取日期属性。也适用于数组、列表或集合。
*/
${#dates.day(date)} // 还有 arrayDay(...), listDay(...) 等。
${#dates.month(date)} // 还有 arrayMonth(...), listMonth(...) 等。
${#dates.monthName(date)} // 还有 arrayMonthName(...), listMonthName(...) 等。
${#dates.monthNameShort(date)} // 还有 arrayMonthNameShort(...), listMonthNameShort(...) 等。
${#dates.year(date)} // 还有 arrayYear(...), listYear(...) 等。
${#dates.dayOfWeek(date)} // 还有 arrayDayOfWeek(...), listDayOfWeek(...) 等。
${#dates.dayOfWeekName(date)} // 还有 arrayDayOfWeekName(...), listDayOfWeekName(...) 等。
${#dates.dayOfWeekNameShort(date)} // 还有 arrayDayOfWeekNameShort(...), listDayOfWeekNameShort(...) 等。
${#dates.hour(date)} // 还有 arrayHour(...), listHour(...) 等。
${#dates.minute(date)} // 还有 arrayMinute(...), listMinute(...) 等。
${#dates.second(date)} // 还有 arraySecond(...), listSecond(...) 等。
${#dates.millisecond(date)} // 还有 arrayMillisecond(...), listMillisecond(...) 等。
/*
* 从其组件创建日期(java.util.Date)对象。
*/
${#dates.create(year,month,day)}
${#dates.create(year,month,day,hour,minute)}
${#dates.create(year,month,day,hour,minute,second)}
${#dates.create(year,month,day,hour,minute,second,millisecond)}
/*
* 创建当前日期和时间的日期(java.util.Date)对象。
*/
${#dates.createNow()}
${#dates.createNowForTimeZone()}
/*
* 创建当前日期的日期对象(时间设置为 00:00)。
*/
${#dates.createToday()}
${#dates.createTodayForTimeZone()}
# 日历
- #calendars : 类似于
#dates
,但用于java.util.Calendar
对象。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Calendars 类的 Javadoc API
* ======================================================================
*/
/*
* 使用标准区域设置格式格式化日历。也适用于数组、列表或集合。
*/
${#calendars.format(cal)}
${#calendars.arrayFormat(calArray)}
${#calendars.listFormat(calList)}
${#calendars.setFormat(calSet)}
/*
* 使用 ISO8601 格式格式化日历。也适用于数组、列表或集合。
*/
${#calendars.formatISO(cal)}
${#calendars.arrayFormatISO(calArray)}
${#calendars.listFormatISO(calList)}
${#calendars.setFormatISO(calSet)}
/*
* 使用指定模式格式化日历。也适用于数组、列表或集合。
*/
${#calendars.format(cal, 'dd/MMM/yyyy HH:mm')}
${#calendars.arrayFormat(calArray, 'dd/MMM/yyyy HH:mm')}
${#calendars.listFormat(calList, 'dd/MMM/yyyy HH:mm')}
${#calendars.setFormat(calSet, 'dd/MMM/yyyy HH:mm')}
/*
* 获取日历属性。也适用于数组、列表或集合。
*/
${#calendars.day(date)} // 还有 arrayDay(...), listDay(...) 等。
${#calendars.month(date)} // 还有 arrayMonth(...), listMonth(...) 等。
${#calendars.monthName(date)} // 还有 arrayMonthName(...), listMonthName(...) 等。
${#calendars.monthNameShort(date)} // 还有 arrayMonthNameShort(...), listMonthNameShort(...) 等。
${#calendars.year(date)} // 还有 arrayYear(...), listYear(...) 等。
${#calendars.dayOfWeek(date)} // 还有 arrayDayOfWeek(...), listDayOfWeek(...) 等。
${#calendars.dayOfWeekName(date)} // 还有 arrayDayOfWeekName(...), listDayOfWeekName(...) 等。
${#calendars.dayOfWeekNameShort(date)} // 还有 arrayDayOfWeekNameShort(...), listDayOfWeekNameShort(...) 等。
${#calendars.hour(date)} // 还有 arrayHour(...), listHour(...) 等。
${#calendars.minute(date)} // 还有 arrayMinute(...), listMinute(...) 等。
${#calendars.second(date)} // 还有 arraySecond(...), listSecond(...) 等。
${#calendars.millisecond(date)} // 还有 arrayMillisecond(...), listMillisecond(...) 等。
/*
* 从其组件创建日历(java.util.Calendar)对象。
*/
${#calendars.create(year,month,day)}
${#calendars.create(year,month,day,hour,minute)}
${#calendars.create(year,month,day,hour,minute,second)}
${#calendars.create(year,month,day,hour,minute,second,millisecond)}
${#calendars.createForTimeZone(year,month,day,timeZone)}
${#calendars.createForTimeZone(year,month,day,hour,minute,timeZone)}
${#calendars.createForTimeZone(year,month,day,hour,minute,second,timeZone)}
${#calendars.createForTimeZone(year,month,day,hour,minute,second,millisecond,timeZone)}
/*
* 创建当前日期和时间的日历(java.util.Calendar)对象。
*/
${#calendars.createNow()}
${#calendars.createNowForTimeZone()}
/*
* 创建当前日期的日历对象(时间设置为 00:00)。
*/
${#calendars.createToday()}
${#calendars.createTodayForTimeZone()}
# 时间(java.time)
- #temporals : 处理 JDK8+ 中
java.time
API 的日期/时间对象。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Temporals 类的 Javadoc API
* ======================================================================
*/
/*
* 使用标准区域设置格式格式化日期。也适用于数组、列表或集合。
*/
${#temporals.format(temporal)}
${#temporals.arrayFormat(temporalsArray)}
${#temporals.listFormat(temporalsList)}
${#temporals.setFormat(temporalsSet)}
/*
* 使用指定模式格式化日期。也适用于数组、列表或集合。
*/
${#temporals.format(temporal, 'dd/MMM/yyyy HH:mm')}
${#temporals.arrayFormat(temporalsArray, 'dd/MMM/yyyy HH:mm')}
${#temporals.listFormat(temporalsList, 'dd/MMM/yyyy HH:mm')}
${#temporals.setFormat(temporalsSet, 'dd/MMM/yyyy HH:mm')}
/*
* 使用 ISO-8601 格式格式化日期。也适用于数组、列表或集合。
*/
${#temporals.formatISO(temporal)}
${#temporals.arrayFormatISO(temporalsArray)}
${#temporals.listFormatISO(temporalsList)}
${#temporals.setFormatISO(temporalsSet)}
/*
* 获取日期属性。也适用于数组、列表或集合。
*/
${#temporals.day(temporal)} // 还有 arrayDay(...), listDay(...) 等。
${#temporals.month(temporal)} // 还有 arrayMonth(...), listMonth(...) 等。
${#temporals.monthName(temporal)} // 还有 arrayMonthName(...), listMonthName(...) 等。
${#temporals.monthNameShort(temporal)} // 还有 arrayMonthNameShort(...), listMonthNameShort(...) 等。
${#temporals.year(temporal)} // 还有 arrayYear(...), listYear(...) 等。
${#temporals.dayOfWeek(temporal)} // 还有 arrayDayOfWeek(...), listDayOfWeek(...) 等。
${#temporals.dayOfWeekName(temporal)} // 还有 arrayDayOfWeekName(...), listDayOfWeekName(...) 等。
${#temporals.dayOfWeekNameShort(temporal)} // 还有 arrayDayOfWeekNameShort(...), listDayOfWeekNameShort(...) 等。
${#temporals.hour(temporal)} // 还有 arrayHour(...), listHour(...) 等。
${#temporals.minute(temporal)} // 还有 arrayMinute(...), listMinute(...) 等。
${#temporals.second(temporal)} // 还有 arraySecond(...), listSecond(...) 等。
${#temporals.nanosecond(temporal)} // 还有 arrayNanosecond(...), listNanosecond(...) 等。
/*
* 从其组件创建时间(java.time.Temporal)对象。
*/
${#temporals.create(year,month,day)} // 返回 java.time.LocalDate 实例
${#temporals.create(year,month,day,hour,minute)} // 返回 java.time.LocalDateTime 实例
${#temporals.create(year,month,day,hour,minute,second)} // 返回 java.time.LocalDateTime 实例
${#temporals.create(year,month,day,hour,minute,second,nanosecond)} // 返回 java.time.LocalDateTime 实例
/*
* 创建当前日期和时间的时间(java.time.Temporal)对象。
*/
${#temporals.createNow()} // 返回 java.time.LocalDateTime 实例
${#temporals.createNowForTimeZone(zoneId)} // 返回 java.time.ZonedDateTime 实例
${#temporals.createToday()} // 返回 java.time.LocalDate 实例
${#temporals.createTodayForTimeZone(zoneId)} // 返回 java.time.LocalDate 实例
/*
* 创建指定日期的时间对象。
*/
${#temporals.createDate(isoDate)} // 返回 java.time.LocalDate 实例
${#temporals.createDateTime(isoDate)} // 返回 java.time.LocalDateTime 实例
${#temporals.createDate(isoDate, pattern)} // 返回 java.time.LocalDate 实例
${#temporals.createDateTime(isoDate, pattern)} // 返回 java.time.LocalDateTime 实例
# 数字
- #numbers : 用于数字对象的实用方法。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Numbers 类的 Javadoc API
* ======================================================================
*/
/*
* 设置最小整数位数。也适用于数组、列表或集合。
*/
${#numbers.formatInteger(num,3)}
${#numbers.arrayFormatInteger(numArray,3)}
${#numbers.listFormatInteger(numList,3)}
${#numbers.setFormatInteger(numSet,3)}
/*
* 设置最小整数位数和千位分隔符:
* 'POINT', 'COMMA', 'WHITESPACE', 'NONE' 或 'DEFAULT'(根据区域设置)。
* 也适用于数组、列表或集合。
*/
${#numbers.formatInteger(num,3,'POINT')}
${#numbers.arrayFormatInteger(numArray,3,'POINT')}
${#numbers.listFormatInteger(numList,3,'POINT')}
${#numbers.setFormatInteger(numSet,3,'POINT')}
/*
* 设置最小整数位数和精确小数位数。也适用于数组、列表或集合。
*/
${#numbers.formatDecimal(num,3,2)}
${#numbers.arrayFormatDecimal(numArray,3,2)}
${#numbers.listFormatDecimal(numList,3,2)}
${#numbers.setFormatDecimal(numSet,3,2)}
/*
* 设置最小整数位数和精确小数位数,以及千位和小数分隔符。
* 也适用于数组、列表或集合。
*/
${#numbers.formatDecimal(num,3,'POINT',2,'COMMA')}
${#numbers.arrayFormatDecimal(numArray,3,'POINT',2,'COMMA')}
${#numbers.listFormatDecimal(numList,3,'POINT',2,'COMMA')}
${#numbers.setFormatDecimal(numSet,3,'POINT',2,'COMMA')}
/*
* 格式化货币。也适用于数组、列表或集合。
*/
${#numbers.formatCurrency(num)}
${#numbers.arrayFormatCurrency(numArray)}
${#numbers.listFormatCurrency(numList)}
${#numbers.setFormatCurrency(numSet)}
/*
* 格式化百分比。也适用于数组、列表或集合。
*/
${#numbers.formatPercent(num)}
${#numbers.arrayFormatPercent(numArray)}
${#numbers.listFormatPercent(numList)}
${#numbers.setFormatPercent(numSet)}
/*
* 设置最小整数位数和精确小数位数。
*/
${#numbers.formatPercent(num, 3, 2)}
${#numbers.arrayFormatPercent(numArray, 3, 2)}
${#numbers.listFormatPercent(numList, 3, 2)}
${#numbers.setFormatPercent(numSet, 3, 2)}
/*
* 创建从 x 到 y 的整数序列(数组)。
*/
${#numbers.sequence(from,to)}
${#numbers.sequence(from,to,step)}
# 字符串
- #strings : 用于
String
对象的实用方法。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Strings 类的 Javadoc API
* ======================================================================
*/
/*
* Null-safe 的 toString()。
*/
${#strings.toString(obj)} // 还有 array*, list* 和 set*
/*
* 检查字符串是否为空(或 null)。在检查之前执行 trim() 操作。
* 也适用于数组、列表或集合。
*/
${#strings.isEmpty(name)}
${#strings.arrayIsEmpty(nameArr)}
${#strings.listIsEmpty(nameList)}
${#strings.setIsEmpty(nameSet)}
/*
* 在字符串上执行 'isEmpty()' 检查,如果为 false 则返回它,否则返回指定的默认字符串。
* 也适用于数组、列表或集合。
*/
${#strings.defaultString(text,default)}
${#strings.arrayDefaultString(textArr,default)}
${#strings.listDefaultString(textList,default)}
${#strings.setDefaultString(textSet,default)}
/*
* 检查字符串中是否包含片段。
* 也适用于数组、列表或集合。
*/
${#strings.contains(name,'ez')} // 还有 array*, list* 和 set*
${#strings.containsIgnoreCase(name,'ez')} // 还有 array*, list* 和 set*
/*
* 检查字符串是否以片段开头或结尾。
* 也适用于数组、列表或集合。
*/
${#strings.startsWith(name,'Don')} // 还有 array*, list* 和 set*
${#strings.endsWith(name,endingFragment)} // 还有 array*, list* 和 set*
/*
* 子字符串相关操作。
* 也适用于数组、列表或集合。
*/
${#strings.indexOf(name,frag)} // 还有 array*, list* 和 set*
${#strings.substring(name,3,5)} // 还有 array*, list* 和 set*
${#strings.substringAfter(name,prefix)} // 还有 array*, list* 和 set*
${#strings.substringBefore(name,suffix)} // 还有 array*, list* 和 set*
${#strings.replace(name,'las','ler')} // 还有 array*, list* 和 set*
/*
* 添加前缀和后缀。
* 也适用于数组、列表或集合。
*/
${#strings.prepend(str,prefix)} // 还有 array*, list* 和 set*
${#strings.append(str,suffix)} // 还有 array*, list* 和 set*
/*
* 转换大小写。
* 也适用于数组、列表或集合。
*/
${#strings.toUpperCase(name)} // 还有 array*, list* 和 set*
${#strings.toLowerCase(name)} // 还有 array*, list* 和 set*
/*
* 分割和连接。
*/
${#strings.arrayJoin(namesArray,',')}
${#strings.listJoin(namesList,',')}
${#strings.setJoin(namesSet,',')}
${#strings.arraySplit(namesStr,',')} // 返回 String[]
${#strings.listSplit(namesStr,',')} // 返回 List<String>
${#strings.setSplit(namesStr,',')} // 返回 Set<String>
/*
* 修剪字符串。
* 也适用于数组、列表或集合。
*/
${#strings.trim(str)} // 还有 array*, list* 和 set*
/*
* 计算字符串长度。
* 也适用于数组、列表或集合。
*/
${#strings.length(str)} // 还有 array*, list* 和 set*
/*
* 缩写文本,使其最大长度为 n。如果文本更大,则会被截断并以 "..." 结尾。
* 也适用于数组、列表或集合。
*/
${#strings.abbreviate(str,10)} // 还有 array*, list* 和 set*
/*
* 将第一个字符转换为大写(反之亦然)。
*/
${#strings.capitalize(str)} // 还有 array*, list* 和 set*
${#strings.unCapitalize(str)} // 还有 array*, list* 和 set*
/*
* 将每个单词的第一个字符转换为大写。
*/
${#strings.capitalizeWords(str)} // 还有 array*, list* 和 set*
${#strings.capitalizeWords(str,delimiters)} // 还有 array*, list* 和 set*
/*
* 转义字符串。
*/
${#strings.escapeXml(str)} // 还有 array*, list* 和 set*
${#strings.escapeJava(str)} // 还有 array*, list* 和 set*
${#strings.escapeJavaScript(str)} // 还有 array*, list* 和 set*
${#strings.unescapeJava(str)} // 还有 array*, list* 和 set*
${#strings.unescapeJavaScript(str)} // 还有 array*, list* 和 set*
/*
* Null-safe 比较和连接。
*/
${#strings.equals(first, second)}
${#strings.equalsIgnoreCase(first, second)}
${#strings.concat(values...)}
${#strings.concatReplaceNulls(nullValue, values...)}
/*
* 生成随机字母数字字符串。
*/
${#strings.randomAlphanumeric(count)}
# 对象
- #objects : 用于一般对象的实用方法。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Objects 类的 Javadoc API
* ======================================================================
*/
/*
* 如果对象不为 null,则返回它,否则返回默认值。
* 也适用于数组、列表或集合。
*/
${#objects.nullSafe(obj,default)}
${#objects.arrayNullSafe(objArray,default)}
${#objects.listNullSafe(objList,default)}
${#objects.setNullSafe(objSet,default)}
# 布尔值
- #bools : 用于布尔值评估的实用方法。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Bools 类的 Javadoc API
* ======================================================================
*/
/*
* 评估条件,与 th:if 标签中的评估方式相同。
* 也适用于数组、列表或集合。
*/
${#bools.isTrue(obj)}
${#bools.arrayIsTrue(objArray)}
${#bools.listIsTrue(objList)}
${#bools.setIsTrue(objSet)}
/*
* 带否定的评估。
* 也适用于数组、列表或集合。
*/
${#bools.isFalse(cond)}
${#bools.arrayIsFalse(condArray)}
${#bools.listIsFalse(condList)}
${#bools.setIsFalse(condSet)}
/*
* 评估并应用 AND 运算符。
* 接收数组、列表或集合作为参数。
*/
${#bools.arrayAnd(condArray)}
${#bools.listAnd(condList)}
${#bools.setAnd(condSet)}
/*
* 评估并应用 OR 运算符。
* 接收数组、列表或集合作为参数。
*/
${#bools.arrayOr(condArray)}
${#bools.listOr(condList)}
${#bools.setOr(condSet)}
# 数组
- #arrays : 用于数组的工具方法
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Arrays 类的 Javadoc API
* ======================================================================
*/
/*
* 转换为数组,尝试推断数组组件类。
* 注意,如果生成的数组为空,或者目标对象的元素
* 不全是同一类,则此方法将返回 Object[]。
*/
${#arrays.toArray(object)}
/*
* 转换为指定组件类的数组。
*/
${#arrays.toStringArray(object)}
${#arrays.toIntegerArray(object)}
${#arrays.toLongArray(object)}
${#arrays.toDoubleArray(object)}
${#arrays.toFloatArray(object)}
${#arrays.toBooleanArray(object)}
/*
* 计算长度
*/
${#arrays.length(array)}
/*
* 检查数组是否为空
*/
${#arrays.isEmpty(array)}
/*
* 检查元素是否包含在数组中
*/
${#arrays.contains(array, element)}
${#arrays.containsAll(array, elements)}
# 列表
- #lists : 用于列表的工具方法
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Lists 类的 Javadoc API
* ======================================================================
*/
/*
* 转换为列表
*/
${#lists.toList(object)}
/*
* 计算大小
*/
${#lists.size(list)}
/*
* 检查列表是否为空
*/
${#lists.isEmpty(list)}
/*
* 检查元素是否包含在列表中
*/
${#lists.contains(list, element)}
${#lists.containsAll(list, elements)}
/*
* 对给定列表的副本进行排序。列表的成员必须实现
* Comparable 接口,或者您必须定义一个比较器。
*/
${#lists.sort(list)}
${#lists.sort(list, comparator)}
# 集合
- #sets : 用于集合的工具方法
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Sets 类的 Javadoc API
* ======================================================================
*/
/*
* 转换为集合
*/
${#sets.toSet(object)}
/*
* 计算大小
*/
${#sets.size(set)}
/*
* 检查集合是否为空
*/
${#sets.isEmpty(set)}
/*
* 检查元素是否包含在集合中
*/
${#sets.contains(set, element)}
${#sets.containsAll(set, elements)}
# 映射
- #maps : 用于映射的工具方法
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Maps 类的 Javadoc API
* ======================================================================
*/
/*
* 计算大小
*/
${#maps.size(map)}
/*
* 检查映射是否为空
*/
${#maps.isEmpty(map)}
/*
* 检查键或值是否包含在映射中
*/
${#maps.containsKey(map, key)}
${#maps.containsAllKeys(map, keys)}
${#maps.containsValue(map, value)}
${#maps.containsAllValues(map, value)}
# 聚合
- #aggregates : 用于在数组或集合上创建聚合的工具方法
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Aggregates 类的 Javadoc API
* ======================================================================
*/
/*
* 计算总和。如果数组或集合为空,则返回 null
*/
${#aggregates.sum(array)}
${#aggregates.sum(collection)}
/*
* 计算平均值。如果数组或集合为空,则返回 null
*/
${#aggregates.avg(array)}
${#aggregates.avg(collection)}
# ID
- #ids : 用于处理可能重复的
id
属性的工具方法(例如,由于迭代导致的结果)。
/*
* ======================================================================
* 参见 org.thymeleaf.expression.Ids 类的 Javadoc API
* ======================================================================
*/
/*
* 通常用于 th:id 属性,用于将计数器附加到 id 属性值,
* 以便即使涉及迭代过程,它也能保持唯一。
*/
${#ids.seq('someId')}
/*
* 通常用于 <label> 标签中的 th:for 属性,以便这些标签可以引用
* 通过 #ids.seq(...) 函数生成的 ID。
*
* 根据 <label> 是在具有 #ids.seq(...) 函数的元素之前还是之后,
* 应调用 "next"(标签在 "seq" 之前)或 "prev" 函数(标签在 "seq" 之后)。
*/
${#ids.next('someId')}
${#ids.prev('someId')}
# 20 附录 C:标记选择器语法
Thymeleaf 的标记选择器直接借用了 Thymeleaf 的解析库:AttoParser (opens new window)。
这种选择器的语法与 XPath、CSS 和 jQuery 中的选择器非常相似,使得大多数用户能够轻松使用。你可以在 AttoParser 文档 (opens new window) 中查看完整的语法参考。
例如,以下选择器将选择标记中所有具有 content
类的 <div>
元素(注意这并不是最简洁的写法,继续阅读以了解原因):
<div th:insert="~{mytemplate :: //div[@class='content']}">...</div>
# 基本语法
/x
表示当前节点的直接子节点,且名称为x
。//x
表示当前节点的子节点,且名称为x
,可以在任何深度。x[@z="v"]
表示名称为x
且具有名为z
的属性且值为"v"
的元素。x[@z1="v1" and @z2="v2"]
表示名称为x
且具有属性z1
和z2
,值分别为"v1"
和"v2"
的元素。x[i]
表示在兄弟节点中位置为i
的名称为x
的元素。x[@z="v"][i]
表示名称为x
,具有属性z
且值为"v"
,并且在满足此条件的兄弟节点中位置为i
的元素。
# 更简洁的语法
x
完全等价于//x
(在任何深度搜索名称为x
的元素或引用,引用是指th:ref
或th:fragment
属性)。- 选择器也可以没有元素名称/引用,只要包含参数规范即可。因此
[@class='oneclass']
是一个有效的选择器,它将查找任何具有class
属性且值为"oneclass"
的元素(标签)。
# 高级属性选择功能
- 除了
=
(等于),其他比较运算符也有效:!=
(不等于)、^=
(以...开头)和$=
(以...结尾)。例如:x[@class^='section']
表示名称为x
且class
属性值以section
开头的元素。 - 属性可以以
@
开头(XPath 风格)或不以@
开头(jQuery 风格)。因此x[z='v']
等价于x[@z='v']
。 - 多属性修饰符可以用
and
连接(XPath 风格),也可以通过链式多个修饰符(jQuery 风格)。因此x[@z1='v1' and @z2='v2']
实际上等价于x[@z1='v1'][@z2='v2']
(也等价于x[z1='v1'][z2='v2']
)。
# 直接的 jQuery 风格选择器
x.oneclass
等价于x[class='oneclass']
。.oneclass
等价于[class='oneclass']
。x#oneid
等价于x[id='oneid']
。#oneid
等价于[id='oneid']
。x%oneref
表示具有th:ref="oneref"
或th:fragment="oneref"
属性的<x>
标签。%oneref
表示任何具有th:ref="oneref"
或th:fragment="oneref"
属性的标签。注意,这实际上等价于简单的oneref
,因为引用可以代替元素名称。- 直接选择器和属性选择器可以混合使用:
a.external[@href^='https']
。
因此,上面的标记选择器表达式:
<div th:insert="~{mytemplate :: //div[@class='content']}">...</div>
可以写成:
<div th:insert="~{mytemplate :: div.content}">...</div>
再看一个不同的例子,以下代码:
<div th:replace="~{mytemplate :: myfrag}">...</div>
将查找具有 th:fragment="myfrag"
片段签名(或 th:ref
引用)的元素。但也会查找名称为 myfrag
的标签(如果它们存在,在 HTML 中不存在)。注意与以下代码的区别:
<div th:replace="~{mytemplate :: .myfrag}">...</div>
这将查找任何具有 class="myfrag"
的元素,而不关心 th:fragment
签名(或 th:ref
引用)。
# 多值类匹配
标记选择器将 class
属性视为多值的,因此即使元素具有多个类值,也可以在此属性上应用选择器。
例如,div.two
将匹配 <div class="one two three" />
。