Thymeleaf官方文档(中文版)

Thymeleaf官方文档(中文版)

本文基于官方文档翻译,如有错误欢迎指正。

Project version: 3.1.3.RELEASE

Project web site: https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html (opens new window)

# 一、介绍 Thymeleaf # 1、Thymeleaf 是什么? Thymeleaf 是一个现代的服务器端 Java 模板引擎,既可以用于 Web 环境,也能在独立环境中运行。它支持处理 HTML、XML、JavaScript、CSS 甚至纯文本文件。

Thymeleaf 的核心目标是提供一种优雅、易维护的模板创建方式。它采用"自然模板"的设计理念,能够将业务逻辑巧妙地融入模板文件中,而不会影响模板作为设计原型的功能。这样一来,设计师和开发者之间的协作更加顺畅,有效缩小了两个团队之间的鸿沟。

Thymeleaf 从设计之初就严格遵循 Web 标准,特别是 HTML5 规范,让你可以创建完全符合标准的模板。

# 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 Schema 的验证。

TEXT 模板模式支持使用特殊语法创建非标记类型的模板,比如文本邮件或文档模板。值得注意的是,HTML 或 XML 模板也可以按 TEXT 模式处理,这时它们不会被当作标记解析,所有标签、DOCTYPE、注释等都会被当作普通文本处理。

JAVASCRIPT 模板模式支持在 Thymeleaf 应用中处理 JavaScript 文件。你可以像在 HTML 文件中一样在 JavaScript 文件中使用模型数据,同时还能享受 JavaScript 专有的功能,比如专门的转义处理或"自然脚本"。JAVASCRIPT 模板模式属于"文本"模式,因此使用与 TEXT 模板模式相同的特殊语法。

CSS 模板模式支持处理 Thymeleaf 应用中的 CSS 文件。和 JAVASCRIPT 模式一样,CSS 模板模式也属于"文本"模式,使用 TEXT 模板模式的特殊语法。

RAW 模板模式完全不处理模板内容。它主要用于将原始资源(如文件、URL 响应等)直接插入到正在处理的模板中。比如,你可以安全地在应用模板中包含外部的 HTML 资源,即使这些资源包含 Thymeleaf 代码,也不会被执行。

# 3、方言:标准方言 Thymeleaf 是一个高度可扩展的模板引擎(可以说是"模板引擎框架"),允许你精细地定义和自定义模板的处理方式。

负责处理标记元素(如标签、文本、注释等,或非标记模板中的占位符)的对象被称为"处理器"。一组处理器加上一些额外组件就构成了一个"方言"。Thymeleaf 核心库开箱即用地提供了"标准方言",对大多数用户来说已经足够了。

本教程主要讲解标准方言。接下来你将学到的每个属性和语法特性都是由标准方言定义的,即使没有明确说明。

当然,如果你想要定义自己的处理逻辑,同时充分利用库的高级功能,完全可以创建自定义方言(甚至扩展标准方言)。Thymeleaf 也支持同时使用多种方言。

官方的 thymeleaf-spring5 和 thymeleaf-spring6 集成包都定义了一种称为"SpringStandard Dialect"的方言,它与标准方言大部分相同,但进行了一些小的调整,以便更好地利用 Spring 框架中的某些功能(例如,通过使用 Spring Expression Language 而不是 OGNL)。因此,如果您是 Spring MVC 用户,您不会浪费时间,因为您在这里学到的几乎所有内容都将在您的 Spring 应用程序中有用。

标准方言的大部分处理器都是"属性处理器"。这样设计的好处是浏览器可以正确显示 HTML 模板文件,因为浏览器会自动忽略不认识的属性。比如,使用标签库的 JSP 可能包含浏览器无法直接显示的代码:

...Thymeleaf 标准方言将允许我们使用以下方式实现相同的功能:

不仅浏览器会正确显示此内容,而且这还允许我们(可选)在其中指定一个值属性(在这种情况下为“James Carrot”),当在浏览器中静态打开原型时将显示该值,并且在处理模板期间将被 ${user.name} 的评估结果替换。

这有助于您的设计师和开发人员处理相同的模板文件,并减少将静态原型转换为工作模板文件所需的工作量。能够做到这一点的功能称为“自然模板”。

# 二、好时光虚拟杂货店 本指南此章节及后续章节所展示示例的源代码,可以在 好时光虚拟杂货店(GTVG) 示例应用中找到,该应用有两个(等效的)版本:

javax.* 基础版:https://github.com/thymeleaf/thymeleaf/tree/3.1-master/examples/core/thymeleaf-examples-gtvg-javax jakarta.* 基础版:https://github.com/thymeleaf/thymeleaf/tree/3.1-master/examples/core/thymeleaf-examples-gtvg-jakarta # 1、一家杂货店的网站 为了更好地解释用 Thymeleaf 处理模板所涉及的概念,本教程将使用一个演示应用程序,您可以从项目网站下载。

此应用程序是一个虚构的虚拟杂货店的网站,将为我们提供许多场景来展示 Thymeleaf 的众多功能。

首先,我们的应用程序需要一组简单的模型实体:通过创建 Orders 出售给 Customers 的 Products。我们还将管理关于这些 Products 的 Comments:

我们的应用程序还将有一个非常简单的服务层,由包含如下方法的 Service 对象组成:

public class ProductService {

//...

public List findAll() {

return ProductRepository.getInstance().findAll();

}

public Product findById(Integer id) {

return ProductRepository.getInstance().findById(id);

}

}

在 Web 层,我们的应用程序将有一个过滤器,根据请求 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、创建和配置模板引擎 我们过滤器中的 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 templateResolutionAttributes);

}

这些对象负责确定我们如何访问模板。在这个 GTVG 应用程序中,使用 org.thymeleaf.templateresolver.WebApplicationTemplateResolver 意味着我们将从 IWebApplication 对象中检索模板文件作为资源:这是 Thymeleaf 的抽象,在基于 Servlet 的应用程序中,基本上围绕 Servlet API 的 javax.servlet.ServletContext 或 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 文件:

Good Thymes Virtual Grocery

href="../../css/gtvg.css" th:href="@{/css/gtvg.css}" />

Welcome to our grocery store!

你首先会注意到,这个文件是 HTML5 格式的,可以在任何浏览器中正确显示,因为它不包含任何非 HTML 标签(浏览器会忽略所有不认识的属性,比如 th:text)。

但你可能还会注意到,这个模板并不是一个真正有效的 HTML5 文档,因为我们使用的这些非标准属性(以 th:* 形式)在 HTML5 规范中是不被允许的。事实上,我们甚至在 标签中添加了一个 xmlns:th 属性,这完全不符合 HTML5 规范:

这在模板处理中没有任何影响,但它可以防止 IDE 报告关于这些 th:* 属性缺少命名空间定义的警告。

那么,如果我们想让这个模板成为有效的 HTML5 文档怎么办?很简单:切换到 Thymeleaf 的数据属性语法,使用 data- 前缀作为属性名,并使用连字符(-)而不是冒号(:)作为分隔符:

Good Thymes Virtual Grocery

href="../../css/gtvg.css" data-th-href="@{/css/gtvg.css}" />

Welcome to our grocery store!

HTML5 规范允许自定义的 data- 前缀属性,因此使用上面的代码,我们的模板就是一个有效的 HTML5 文档。

两种表示法完全等价且可以互换,但为了代码示例的简洁性和紧凑性,本教程将使用命名空间表示法(th:*)。此外,th:* 表示法更通用,允许在所有 Thymeleaf 模板模式(XML、TEXT 等)中使用,而 data- 表示法仅在 HTML 模式下允许。

# 使用 th:text 和外部化文本 外部化文本是将模板代码片段提取到模板文件之外,让它们可以保存在单独的文件中(通常是 .properties 文件),并且可以轻松地用其他语言编写的等效文本替换(这个过程称为国际化,简称 i18n)。外部化的文本片段通常称为"消息"。

每个消息都有一个标识键,Thymeleaf 允许你使用 #{...} 语法指定文本应该对应于特定消息:

Welcome to our grocery store!

我们在这里看到的实际上是 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 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);

让我们看看使用西班牙语环境的结果:

Good Thymes Virtual Grocery

¡Bienvenido a nuestra tienda de comestibles!

# 3.2 更多关于文本和变量的内容 # 未转义的文本 主页的最简单版本似乎已经准备好了,但我们还没有考虑到一些事情……如果我们有这样一条消息怎么办?

home.welcome=Welcome to our fantastic grocery store!

如果我们像以前一样执行这个模板,我们将得到:

Welcome to our <b>fantastic</b> grocery store!

这并不是我们期望的,因为 标签被转义了,会在浏览器中显示。

这是 th:text 属性的默认行为。如果我们希望 Thymeleaf 保留 HTML 标签而不转义它们,需要使用不同的属性:th:utext(用于"未转义的文本"):

Welcome to our grocery store!

这将输出我们想要的消息:

Welcome to our fantastic grocery store!

# 使用和显示变量 现在让我们为主页添加更多内容。例如,我们可能希望在欢迎信息下方显示日期:

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 变量,现在可以在模板中显示它:

Welcome to our grocery store!

Today is: 13 February 2011

正如你所看到的,我们仍然使用 th:text 属性来完成这项工作(这是正确的,因为我们想要替换标签的主体),但这次语法有点不同,我们使用的是 ${...} 表达式值,而不是 #{...} 表达式值。这是一个变量表达式,它包含一种称为 OGNL(对象图导航语言)的表达式,该表达式会在上下文变量映射上执行。

${today} 表达式简单地表示"获取名为 today 的变量",但这些表达式可能更复杂(例如 ${user.name} 表示"获取名为 user 的变量,并调用其 getName() 方法")。

属性值中有很多可能性:消息、变量表达式……还有很多。下一章将展示所有这些可能性。

# 4 标准表达式语法 我们暂时中断虚拟杂货店的开发,来学习 Thymeleaf 标准方言中最重要的部分之一:Thymeleaf 标准表达式语法。

我们已经看到了两种用这种语法表示的有效属性值:消息表达式和变量表达式:

Welcome to our grocery store!

Today is: 13 february 2011

但还有更多类型的表达式,以及已知表达式的更多有趣细节。

首先,让我们快速总结一下标准表达式的特性:

简单表达式:

变量表达式:${...} 选择变量表达式:*{...} 消息表达式:#{...} 链接 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) 特殊标记:

无操作:_ 所有这些特性都可以组合和嵌套:

'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))

# 4.1 消息 正如我们已经知道的,#{...} 消息表达式允许我们将以下内容关联起来:

Welcome to our grocery store!

……到以下内容:

home.welcome=¡Bienvenido a nuestra tienda de comestibles!

但我们还没有考虑到一个方面:如果消息文本不是完全静态的怎么办?例如,如果应用程序知道当前访问网站的用户是谁,并且我们想通过名字来问候他们?

¡Bienvenido a nuestra tienda de comestibles, John Apricot!

这意味着我们需要为消息添加一个参数。就像这样:

home.welcome=¡Bienvenido a nuestra tienda de comestibles, {0}!

参数根据 java.text.MessageFormat 标准语法指定,这意味着你可以按照 java.text.* 包中的 API 文档格式化数字和日期。

为了给参数指定一个值,并且给定一个名为 user 的 HTTP 会话属性,我们可以这样做:

Welcome to our grocery store, Sebastian Pepper!

注意:这里使用 th:utext 意味着格式化后的消息不会被转义。此示例假设 user.name 已经被转义。

可以指定多个参数,用逗号分隔。

消息键本身也可以来自变量:

Welcome to our grocery store, Sebastian Pepper!

# 4.2 变量 我们已经提到,${...} 表达式实际上是 OGNL(对象图导航语言)表达式,在上下文包含的变量映射上执行。

有关 OGNL 语法和功能的详细信息,你应该阅读 OGNL 语言指南 (opens new window)。

在启用 Spring MVC 的应用程序中,OGNL 将被 SpringEL 替换,但其语法与 OGNL 非常相似(实际上,在大多数常见情况下完全相同)。

从 OGNL 的语法中,我们知道以下表达式:

Today is: 13 february 2011.

……实际上等同于:

ctx.getVariable("today");

但 OGNL 允许我们创建更强大的表达式,这就是以下内容:

Welcome to our grocery store, Sebastian Pepper!

……通过执行以下操作获取用户名:

((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: US.

你可以在 附录 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);

……然后在视图层执行日期格式化:

Today is: 13 May 2011

# 4.3 选择表达式(星号语法) 变量表达式不仅可以写成 ${...},还可以写成 *{...}。

但有一个重要的区别:星号语法在选定对象上计算表达式,而不是在整个上下文上。也就是说,只要没有选定对象,美元和星号语法完全相同。

那么什么是选定对象?使用 th:object 属性的表达式的结果。让我们在用户配置文件(userprofile.html)页面中使用:

Name: Sebastian.

Surname: Pepper.

Nationality: Saturn.

这完全等同于:

Name: Sebastian.

Surname: Pepper.

Nationality: Saturn.

当然,美元和星号语法可以混合使用:

Name: Sebastian.

Surname: Pepper.

Nationality: Saturn.

当选定对象存在时,选定对象也会作为 #object 表达式变量提供给美元表达式:

Name: Sebastian.

Surname: Pepper.

Nationality: Saturn.

如前所述,如果没有执行对象选择,美元和星号语法是等价的。

Name: Sebastian.

Surname: Pepper.

Nationality: Saturn.

# 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 属性:

th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view

view

view

这里有一些需要注意的事项:

th:href 是一个修饰属性:一旦处理,它会计算要使用的链接 URL 并将该值设置为 标签的 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 基础也可以是计算另一个表达式的结果:

view

view

# 主页的菜单 现在我们知道如何创建链接 URL,那么在主页中添加一个小菜单来链接到网站中的其他页面呢?

Please select an option

  1. Product List
  2. Order List
  3. Subscribe to our Newsletter
  4. See User Profile

# 服务器根相对 URL 可以使用额外的语法来创建服务器根相对(而不是上下文根相对)URL,以便链接到同一服务器中的不同上下文。这些 URL 指定为 @{~/path/to/something}。

# 4.5 片段 片段表达式是一种表示标记片段并在模板间移动它们的简单方法。这允许我们复制它们,将它们作为参数传递给其他模板等。

最常见的用途是使用 th:insert 或 th:replace 进行片段插入(稍后会在有关模板布局的部分中详细介绍):

...

但它们可以像任何其他变量一样在任何地方使用:

在本教程的后面部分,有一个专门介绍模板布局的部分,包括对片段表达式的更深入解释。

# 4.6 字面量 # 文本字面量 文本字面量只是用单引号括起来的字符串。它们可以包含任何字符,但你应该使用 \' 转义其中的任何单引号。

Now you are looking at a template file.

# 数字字面量 数字字面量就是数字。

The year is 1492.

In two years, it will be 1494.

# 布尔字面量 布尔字面量是 true 和 false。例如:

...

在这个例子中,== false 写在括号外,因此由 Thymeleaf 处理。如果写在括号内,则由 OGNL/SpringEL 引擎处理:

...

# Null 字面量 null 字面量也可以使用:

...

# 字面量标记 数字、布尔值和空值字面量实际上是字面量标记的一种特殊情况。

这些标记在标准表达式中允许进行一些简化。它们的工作方式与文本字面量('...')完全相同,但它们只允许字母(A-Z 和 a-z)、数字(0-9)、括号([ 和 ])、点(.)、连字符(-)和下划线(_)。所以不允许有空格、逗号等。

好的部分是什么?标记不需要任何引号包围。所以我们可以这样做:

...

而不是:

...

# 4.7 文本拼接 文本,无论是字面量还是计算变量或消息表达式的结果,都可以使用 + 操作符轻松拼接:

# 4.8 字面量替换 字面量替换允许格式化包含变量值的字符串,而无需使用 '...' + '...' 进行拼接。

这些替换必须用竖线(|)括起来,例如:

这等同于:

字面量替换可以与其他类型的表达式结合使用:

注意:只有变量/消息表达式(${...}、*{...}、#{...})允许在 |...| 字面量替换中使用。不允许其他字面量('...')、布尔/数字标记、条件表达式等。

# 4.9 算术操作 一些算术操作也是可用的:+、-、*、/ 和 %。

请注意,这些操作符也可以在 OGNL 变量表达式中使用(在这种情况下将由 OGNL 而不是 Thymeleaf 标准表达式引擎执行):

注意,这些操作符有一些文本别名:div(/)、mod(%)。

# 4.10 比较器和相等 表达式中的值可以使用 >、<、>= 和 <= 符号进行比较,== 和 != 运算符可用于检查相等性(或不等性)。请注意,XML 规定 < 和 > 符号不应在属性值中使用,因此应替换为 < 和 >。

更简单的替代方案是使用这些操作符的文本别名:gt(>)、lt(<)、ge(>=)、le(<=)、not(!)。还有 eq(==)、neq/ne(!=)。

# 4.11 条件表达式 条件表达式用于根据条件(本身是另一个表达式)的计算结果来计算两个表达式中的一个。

让我们看一个示例片段(引入另一个属性修饰符,th:class):

...

条件表达式的所有三个部分(条件、then 和 else)本身都是表达式,这意味着它们可以是变量(${...}、*{...})、消息(#{...})、URL(@{...})或字面量('...')。

条件表达式也可以使用括号嵌套:

...

else 表达式也可以省略,在这种情况下,如果条件为假,则返回空值:

...

# 4.12 默认表达式(Elvis 操作符) 默认表达式是一种没有 then 部分的特殊条件值。它等同于 Groovy 等语言中的 Elvis 操作符,允许你指定两个表达式:如果第一个表达式计算为非空值,则使用它;否则使用第二个表达式。

让我们在用户资料页面中看看它的作用:

...

Age: 27.

如你所见,操作符是 ?:,我们在这里用它来指定年龄的默认值(在此例中是一个字面量值),仅当 *{age} 的计算结果为 null 时。因此,这等同于:

Age: 27.

与条件值一样,它们可以包含括号中的嵌套表达式:

Name:

Sebastian

# 4.13 无操作标记 无操作标记由下划线符号(_)表示。

这个标记的含义是指定表达式的结果应为 无操作,即表现得好像可处理属性(例如 th:text)根本不存在一样。

其中一个应用场景是,这允许开发人员使用原型文本作为默认值。例如,而不是:

...

……我们可以直接使用 "no user authenticated" 作为原型文本,从而使代码更简洁且在设计上更通用:

no user authenticated

# 4.14 数据转换/格式化 Thymeleaf 为变量(${...})和选择(*{...})表达式定义了一种 双括号 语法,允许我们通过配置的 转换服务 应用 数据转换。

它基本上是这样的:

...

注意到双括号了吗?${​{ ... }}。这指示 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 执行它:

Some text here...

请注意,法语语言环境的预处理步骤会创建以下等效内容:

Some text here...

预处理字符串 __ 可以在属性中使用 \_\_ 进行转义。

# 5 设置属性值 本章将解释如何在标记中设置(或修改)属性的值。

# 5.1 设置任何属性的值 假设我们的网站发布了一则新闻通讯,我们希望用户能够订阅它,因此我们创建了一个 /WEB-INF/templates/subscribe.html 模板,并添加了一个表单:

与 Thymeleaf 一样,这个模板最初更像是一个静态原型,而不是一个 Web 应用程序的模板。首先,表单中的 action 属性静态链接到模板文件本身,因此没有地方进行有用的 URL 重写。其次,提交按钮中的 value 属性使其显示英文文本,但我们希望它是国际化的。

因此,我们引入了 th:attr 属性,它能够更改其所在标签的属性值:

概念非常简单:th:attr 只需接受一个表达式,该表达式会将值分配给属性。创建相应的控制器和消息文件后,处理此文件的结果是:

除了新的属性值之外,你还可以看到应用程序上下文名称已自动前缀到 URL 基础 /gtvg/subscribe,如前一章所述。

但如果我们想一次设置多个属性怎么办?XML 规则不允许你在标签中两次设置一个属性,因此 th:attr 会接受一个逗号分隔的赋值列表,例如:

th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

给定所需的消息文件后,这会输出:

Logo de Good Thymes

# 5.2 设置特定属性的值 到目前为止,你可能会想到下面这样的代码:

这段代码在标记中看起来相当丑陋。在属性值中指定赋值可能非常实用,但如果你必须一直这样做,这并不是创建模板的最优雅方式。

Thymeleaf 同意你的观点,这就是为什么在模板中很少使用 th:attr。通常情况下,你会使用其他 th:* 属性,它们的任务是设置特定的标签属性(而不像 th:attr 那样只设置任何属性)。

例如,要设置 value 属性,请使用 th:value:

这看起来好多了!让我们尝试对 form 标签中的 action 属性做同样的操作:

还记得我们之前在 home.html 中放置的那些 th:href 吗?它们正是这种类型的属性:

  • Product List
  • 有许多类似的属性,每个属性都针对特定的 HTML5 属性。

    # 5.3 一次设置多个值 有两个相当特殊的属性叫做 th:alt-title 和 th:lang-xmllang,它们可以同时将两个属性设置为相同的值。具体来说:

    th:alt-title 将设置 alt 和 title。 th:lang-xmllang 将设置 lang 和 xml:lang。 对于我们的 GTVG 主页,这将允许我们替换以下代码:

    th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

    或者这个等效的代码:

    th:src="@{/images/gtvglogo.png}" th:title="#{logo}" th:alt="#{logo}" />

    替换为以下代码:

    th:src="@{/images/gtvglogo.png}" th:alt-title="#{logo}" />

    # 5.4 追加和前置 Thymeleaf 还提供了 th:attrappend 和 th:attrprepend 属性,它们会将计算结果附加(后缀)或前置(前缀)到现有属性值。

    例如,你可能希望将 CSS 类的名称存储在一个上下文变量中,以便添加到按钮中,因为要使用的特定 CSS 类取决于用户之前执行的操作:

    如果你使用 cssStyle 变量设置为 "warning" 处理此模板,你会得到:

    标准方言中还有两个特定的"追加属性":th:classappend 和 th:styleappend 属性,它们用于向元素添加 CSS 类或样式片段,而不覆盖现有的类或样式:

    # 5.5 固定值布尔属性 HTML 有布尔属性的概念,这些属性没有值,它们的存在意味着值为 "true"。在 XHTML 中,这些属性只接受一个值,即它们本身。

    例如,checked:

    标准方言包括允许你通过计算条件来设置这些属性的属性,因此,如果计算为 true,则会将该属性设置为其固定值,如果计算为 false,则不设置该属性:

    标准方言中存在的固定值布尔属性如下:

    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:* 处理器。

    例如:

    ...

    会产生:

    ...

    # 5.7 支持 HTML5 友好的属性和元素名称 还可以使用完全不同的语法以更符合 HTML5 的方式将处理器应用于模板。

    ... ...

    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 页面中显示产品,我们使用一个表格。每个产品会显示在一行(一个 元素)中,因此对于模板,我们需要创建一个模板行——一个展示如何显示每个产品的示例——然后指示 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 allProducts = productService.findAll();

    final WebContext ctx = new WebContext(webExchange, webExchange.getLocale());

    ctx.setVariable("prods", allProducts);

    templateEngine.process("product/list", ctx, writer);

    }

    然后,我们在模板中使用 th:each 来迭代产品列表:

    Good Thymes Virtual Grocery

    href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />

    Product list

    NAME PRICE IN STOCK
    Onions 2.41 yes

    Return to home

    你上面看到的 prod : ${prods} 属性值的意思是“对于 ${prods} 的结果中的每个元素,重复此模板片段,并使用当前元素作为名为 prod 的变量”。让我们为每个我们看到的元素命名:

    我们将 ${prods} 称为迭代表达式或迭代对象。 我们将 prod 称为迭代变量。 请注意,prod 迭代变量的范围是 元素,这意味着它可用于内部标签,如 。

    # 可迭代的值 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 布尔属性。 让我们看看如何在前面的示例中使用它:

    NAME PRICE IN STOCK
    Onions 2.41 yes

    状态变量(在本例中为 iterStat)通过在迭代变量本身后写入其名称来在 th:each 属性中定义,两者之间用逗号分隔。与迭代变量一样,状态变量的范围也是由包含 th:each 属性的标签定义的代码片段。

    让我们看看处理模板的结果:

    Good Thymes Virtual Grocery

    Product list

    NAME PRICE IN STOCK
    Fresh Sweet Basil 4.99 yes
    Italian Tomato 1.25 no
    Yellow Bell Pepper 2.50 yes
    Old Cheddar 18.75 yes

    Return to home

    请注意,我们的迭代状态变量工作得非常完美,仅为奇数行设置了 odd CSS 类。

    如果你没有显式设置状态变量,Thymeleaf 将始终通过将 Stat 后缀添加到迭代变量的名称来为你创建一个:

    NAME PRICE IN STOCK
    Onions 2.41 yes

    # 6.3 通过延迟检索数据优化 有时我们可能希望优化数据集合(例如从数据库中检索)的检索,以便仅在真正使用这些集合时才检索它们。

    实际上,这可以应用于任何数据,但考虑到内存中集合可能的大小,检索用于迭代的集合是这种场景中最常见的情况。

    为了支持这一点,Thymeleaf 提供了一种延迟加载上下文变量的机制。实现 ILazyContextVariable 接口的上下文变量——最可能通过扩展其默认实现 LazyContextVariable——将在执行时解析。例如:

    context.setVariable(

    "users",

    new LazyContextVariable>() {

    @Override

    protected List loadValue() {

    return databaseRepository.findAllUsers();

    }

    });

    这个变量可以在不知道其延迟性的情况下使用,例如在以下代码中:

    • user name

    但同时,如果 condition 在以下代码中评估为 false,则该变量永远不会初始化(其 loadValue() 方法永远不会被调用):

    • user name

    # 7 条件评估 # 7.1 简单条件判断:if 和 unless 有时你需要模板中的某段内容仅在满足特定条件时才显示。

    例如,假设我们希望在产品表中显示一列,展示每个产品的评论数量,并且如果有评论,则提供一个链接到该产品的评论详情页面。

    为了实现这一点,我们可以使用 th:if 属性:

    NAME PRICE IN STOCK COMMENTS
    Onions 2.41 yes

    2 comment/s

    th:href="@{/product/comments(prodId=${prod.id})}"

    th:if="${not #lists.isEmpty(prod.comments)}">view

    这里有很多内容要看,所以我们重点关注重要的一行:

    th:href="@{/product/comments(prodId=${prod.id})}"

    th:if="${not #lists.isEmpty(prod.comments)}">view

    这将创建一个指向评论页面的链接(URL 为 /product/comments),并将 prodId 参数设置为产品的 id,但仅当产品有评论时才会显示。

    让我们看看生成的标记:

    NAME PRICE IN STOCK COMMENTS
    Fresh Sweet Basil 4.99 yes

    0 comment/s

    Italian Tomato 1.25 no

    2 comment/s

    view

    Yellow Bell Pepper 2.50 yes

    0 comment/s

    Old Cheddar 18.75 yes

    1 comment/s

    view

    完美!这正是我们想要的。

    需要注意的是,th:if 属性不仅会评估布尔条件。它的功能还稍微超出这个范围,它将按照以下规则评估指定的表达式为 true:

    如果值不为空:

    如果值是布尔值且为 true。 如果值是数字且非零。 如果值是字符且非零。 如果值是字符串且不是 false、off 或 no。 如果值不是布尔值、数字、字符或字符串。 (如果值为空,th:if 将评估为 false)。 此外,th:if 还有一个反向属性 th:unless,我们可以在前面的例子中使用它,而不是在 OGNL 表达式中使用 not:

    th:href="@{/comments(prodId=${prod.id})}"

    th:unless="${#lists.isEmpty(prod.comments)}">view

    # 7.2 Switch 语句 还有一种方式可以使用 th:switch / th:case 属性集来显示条件内容,类似于 Java 中的 switch 结构:

    User is an administrator

    User is a manager

    请注意,一旦一个 th:case 属性被评估为 true,同一 switch 上下文中的其他 th:case 属性将被评估为 false。

    默认选项指定为 th:case="*":

    User is an administrator

    User is a manager

    User is some other thing

    # 8 模板布局 # 8.1 包含模板片段 # 定义和引用片段 在我们的模板中,我们经常希望包含其他模板的部分,如页脚、页眉、菜单等。为了实现这一点,Thymeleaf 需要定义这些部分,即“片段”,可以使用 th:fragment 属性来定义。

    假设我们想为所有的杂货页面添加一个标准的版权页脚,我们可以创建一个 /WEB-INF/templates/footer.html 文件,包含以下代码:

    © 2011 The Good Thymes Virtual Grocery

    上面的代码定义了一个名为 copy 的片段,我们可以很容易地使用 th:insert 或 th:replace 属性将其包含在我们的主页中:

    ...

    注意,th:insert 需要一个片段表达式(~{...}),即一个结果为片段的表达式。

    # 片段指定语法 片段表达式的语法非常简单,有三种不同的格式:

    "~{templatename::selector}":包含在指定模板上应用标记选择器所得到的片段。注意,selector 可以是一个片段名,因此你可以指定像 ~{footer :: copy} 这样的简单内容。

    "~{templatename}":包含指定名称的完整模板。

    "~{::selector}" 或 "~{this::selector}":从同一模板中插入一个片段,匹配 selector。如果在当前模板中找不到,将向最初处理的模板(根模板)遍历模板调用堆栈,直到在某个级别匹配到 selector。

    在上述示例中,templatename 和 selector 都可以是完全功能的表达式(甚至可以是条件表达式),例如:

    片段可以包含任何 th:* 属性。这些属性将在片段包含到目标模板(带有 th:insert/th:replace 属性的模板)时进行评估,并且它们能够引用目标模板中定义的任何上下文变量。

    # 不使用 th:fragment 引用片段 由于标记选择器的强大功能,我们可以包含不使用任何 th:fragment 属性的片段。它甚至可以来自一个完全不了解 Thymeleaf 的应用程序的标记代码:

    ...

    © 2011 The Good Thymes Virtual Grocery

    ...

    我们可以简单地通过 id 属性引用上面的片段,类似于 CSS 选择器:

    ...

    # th:insert 和 th:replace 的区别 th:insert 和 th:replace 的区别是什么?

    th:insert 将指定的片段插入为其宿主标签的内容。 th:replace 实际上是用指定的片段替换其宿主标签。 例如,以下 HTML 片段:

    © 2011 The Good Thymes Virtual Grocery

    在宿主

    标签中包含两次,如下所示:

    ...

    结果将是:

    ...

    © 2011 The Good Thymes Virtual Grocery

    © 2011 The Good Thymes Virtual Grocery

    # 8.2 参数化片段签名 为了创建更“函数式”的模板片段机制,使用 th:fragment 定义的片段可以指定一组参数:

    ...

    这需要使用以下两种语法之一来调用片段:

    ...

    ...

    注意,在最后一个选项中,顺序并不重要:

    ...

    # 无片段参数的片段局部变量 即使片段定义如下,没有参数:

    ...

    我们也可以使用第二种语法来调用它们(只能使用第二种):

    这相当于 th:replace 和 th:with 的组合:

    注意,为片段指定局部变量——无论它是否有参数签名——都不会导致在执行片段之前清空上下文。片段仍然能够像当前一样访问调用模板中使用的所有上下文变量。

    # th:assert 用于模板内断言 th:assert 属性可以指定一个逗号分隔的表达式列表,每个表达式都应评估为 true,否则会引发异常。

    例如:

    ...

    这在验证片段签名中的参数时非常有用:

    ...

    # 8.3 灵活布局:超越简单的片段插入 由于片段表达式,我们可以为片段指定参数,这些参数不仅仅是文本、数字、Bean 对象,而是标记片段。这使我们能够以这样的方式创建片段,即它们可以通过调用模板中的标记进行丰富,从而形成非常灵活的模板布局机制。

    例如,以下片段中的 title 和 links 变量:

    The awesome application

    我们可以这样调用这个片段:

    Awesome - Main

    结果将使用调用模板中的实际 和 <link> 标签作为 title 和 links 变量的值,从而在插入时自定义我们的片段。</p> <p># 使用空片段 一个特殊的片段表达式,即空片段(~{}),可以用于指定无标记。使用前面的示例:</p> <p><head th:replace="~{ base :: common_header(~{::title},~{}) }"></p> <p><title>Awesome - Main

    注意,片段的第二个参数(links)设置为空片段,因此不会为 块写入任何内容。

    # 使用无操作标记 无操作标记也可以作为片段的参数,如果我们只想让片段使用其当前标记作为默认值。再次使用 common_header 示例:

    Awesome - Main

    注意,片段的第一个参数(title)设置为无操作(_),这导致片段中的这部分完全不执行(title = 无操作)。

    # 高级条件插入片段 空片段和无操作标记的可用性使我们能够以非常简单和优雅的方式执行片段的条件插入。

    例如,我们可以这样做,以仅在用户是管理员时插入 common :: adminhead 片段,否则插入空片段:

    ...

    此外,我们可以使用无操作标记来仅在满足指定条件时插入片段,但如果条件不满足,则保留标记而不进行修改:

    Welcome [[${user.name}]], click here for help-desk support.

    此外,如果我们配置了模板解析器以检查模板资源的存在性——通过它们的 checkExistence 标志——我们可以使用片段本身的存在作为默认操作的条件:

    Welcome [[${user.name}]], click here for help-desk support.

    # 8.4 移除模板片段 回到示例应用程序,让我们重新审视产品列表模板的最后一个版本:

    NAME PRICE IN STOCK COMMENTS
    Onions 2.41 yes

    2 comment/s

    view

    这段代码作为模板非常好,但作为静态页面(当直接由浏览器打开而不经过 Thymeleaf 处理时),它不会成为一个好的原型。

    为什么?因为尽管浏览器可以完美显示,但表格中只有一行,且这一行是模拟数据。作为原型,它看起来不够真实……我们应该有多个产品,需要更多行。

    因此,我们添加一些行:

    NAME PRICE IN STOCK COMMENTS
    Onions 2.41 yes

    2 comment/s

    view

    Blue Lettuce 9.55 no

    0 comment/s

    Mild Cinnamon 1.99 yes

    3 comment/s

    view

    现在我们有三个产品行,这对于原型来说肯定更好。但是,当我们用 Thymeleaf 处理它时会发生什么?

    NAME PRICE IN STOCK COMMENTS
    Fresh Sweet Basil 4.99 yes

    0 comment/s

    Italian Tomato 1.25 no

    2 comment/s

    view

    Yellow Bell Pepper 2.50 yes

    0 comment/s

    Old Cheddar 18.75 yes

    1 comment/s

    view

    Blue Lettuce 9.55 no

    0 comment/s

    Mild Cinnamon 1.99 yes

    3 comment/s

    view

    最后两行是模拟行!当然,它们只是模拟行:迭代只应用于第一行,因此没有理由让 Thymeleaf 删除其他两行。

    我们需要在模板处理期间删除这两行。让我们在第二和第三个 标签上使用 th:remove 属性:

    NAME PRICE IN STOCK COMMENTS
    Onions 2.41 yes

    2 comment/s

    view

    Blue Lettuce 9.55 no

    0 comment/s

    Mild Cinnamon 1.99 yes

    3 comment/s

    view

    处理后的结果将再次看起来像预期的那样:

    NAME PRICE IN STOCK COMMENTS
    Fresh Sweet Basil 4.99 yes

    0 comment/s

    Italian Tomato 1.25 no

    2 comment/s

    view

    Yellow Bell Pepper 2.50 yes

    0 comment/s

    Old Cheddar 18.75 yes

    1 comment/s

    view

    all 值在属性中是什么意思?th:remove 可以根据其值以五种不同的方式行为:

    all:删除包含标签及其所有子元素。 body:不删除包含标签,但删除所有子元素。 tag:删除包含标签,但不删除其子元素。 all-but-first:删除包含标签的所有子元素,除了第一个。 none:不执行任何操作。此值对于动态评估非常有用。 all-but-first 值在原型设计时非常有用,因为它可以让我们节省一些 th:remove="all" 的使用。

    # 8.5 布局继承 为了能够将单个文件作为布局,可以使用片段。以下是一个简单布局的示例,使用 th:fragment 和 th:replace 定义了 title 和 content:

    Layout Title

    Layout H1

    Layout content

    Layout footer

    在这个文件中,html 标签将被 layout 替换,但在布局中,title 和 content 将分别被 title 和 section 块替换。

    如果需要,布局可以由多个片段组成,如 header 和 footer。

    # 9 局部变量 Thymeleaf 将局部变量定义为模板中某个特定片段所定义的变量,并且仅在该片段内可用。

    我们已经看到的一个例子是产品列表页面中的 prod 迭代变量:

    ...

    这个 prod 变量仅在 标签的范围内可用。具体来说:

    它可以在该标签内执行的任何其他 th:* 属性中使用,只要这些属性的优先级低于 th:each(这意味着它们将在 th:each 之后执行)。 它可以在 标签的任何子元素中使用,例如任何 元素。 Thymeleaf 提供了一种无需迭代即可声明局部变量的方法,使用 th:with 属性,其语法类似于属性值赋值:

    The name of the first person is Julius Caesar.

    当 th:with 被处理时,firstPer 变量将作为局部变量创建,并添加到上下文中的变量映射中,因此它可以与上下文中声明的任何其他变量一起进行评估,但仅在包含的

    标签范围内可用。

    你可以使用通常的多重赋值语法同时定义多个变量:

    The name of the first person is Julius Caesar.

    But the name of the second person is

    Marcus Antonius.

    th:with 属性允许重用同一属性中定义的变量:

    ...

    让我们在我们的 Grocery 主页中使用它!还记得我们为输出格式化日期而编写的代码吗?

    Today is:

    13 february 2011

    那么,如果我们希望 "dd MMMM yyyy" 实际上依赖于语言环境呢?例如,我们可能希望在 home_en.properties 中添加以下消息:

    date.format=MMMM dd',' yyyy

    并在 home_es.properties 中添加等效的消息:

    date.format=dd 'de' MMMM',' yyyy

    现在,让我们使用 th:with 将本地化的日期格式放入变量中,然后在 th:text 表达式中使用它:

    Today is: 13 February 2011

    这既简洁又容易。事实上,考虑到 th:with 的优先级高于 th:text,我们可以在 标签中解决所有问题:

    Today is:

    13 February 2011

    你可能会想:优先级?我们还没有讨论过这个!别担心,因为下一章将专门讨论这个问题。

    # 10 属性优先级 当你在同一个标签中写入多个 th:* 属性时会发生什么?例如:

    • Item description here...

    我们希望 th:each 属性在 th:text 之前执行,以便得到我们想要的结果。但由于 HTML/XML 标准并未赋予标签中属性的书写顺序任何意义,因此必须在属性本身中建立一个优先级机制,以确保其按预期工作。

    因此,所有 Thymeleaf 属性都定义了一个数字优先级,用于确定它们在标签中的执行顺序。这个顺序如下:

    顺序 功能 属性 1 片段包含 th:insertth:replace 2 片段迭代 th:each 3 条件评估 th:ifth:unlessth:switchth:case 4 局部变量定义 th:objectth:with 5 通用属性修改 th:attrth:attrprependth:attrappend 6 特定属性修改 th:valueth:hrefth:src... 7 文本(标签体修改) th:textth:utext 8 片段指定 th:fragment 9 片段移除 th:remove 这个优先级机制意味着,即使属性位置颠倒,上述迭代片段也会给出完全相同的结果(尽管可读性稍差):

    • Item description here...

    # 11 注释和块 # 11.1 标准 HTML/XML 注释 标准的 HTML/XML 注释 可以在 Thymeleaf 模板中的任何地方使用。这些注释内的任何内容都不会被 Thymeleaf 处理,而是原封不动地复制到结果中:

    ...

    # 11.2 Thymeleaf 解析器级注释块 解析器级注释块是在 Thymeleaf 解析模板时会被简单删除的代码。它们看起来像这样:

    Thymeleaf 会删除 之间的所有内容,因此这些注释块也可以用于在模板静态打开时显示代码,知道它会在 Thymeleaf 处理时被删除:

    you can see me only before Thymeleaf processes me!

    这对于原型设计时带有大量 的表格非常有用:

    ...

    ...

    ...

    # 11.3 Thymeleaf 仅原型注释块 Thymeleaf 允许定义特殊的注释块,当模板静态打开时(即作为原型)被视为注释,但在执行模板时被视为正常标记。

    hello!

    goodbye!

    Thymeleaf 的解析系统会简单地删除 标记,但不会删除其内容,因此内容将保持未注释状态。因此,在执行模板时,Thymeleaf 实际上会看到:

    hello!

    ...

    goodbye!

    与解析器级注释块一样,此功能与方言无关。

    # 11.4 合成 th:block 标签 Thymeleaf 标准方言中包含的唯一元素处理器(不是属性)是 th:block。

    th:block 是一个简单的属性容器,允许模板开发者指定他们想要的任何属性。Thymeleaf 将执行这些属性,然后简单地使块本身(而不是其内容)消失。

    因此,例如,在创建每个元素需要多个 的迭代表格时,它可能非常有用:

    ... ...
    ...

    特别是与仅原型注释块结合使用时非常有用:

    ... ...
    ...

    注意,此解决方案允许模板成为有效的 HTML(无需在

    内添加禁止的
    块),并且在浏览器中静态打开作为原型时仍然可以正常工作!

    尽管 th:block 本身不会在最终渲染的HTML输出中留下任何痕迹(即它不会生成任何额外的HTML标签),但它允许你在模板中执行各种Thymeleaf属性操作,如条件判断、迭代等。

    你可以将 th:block 视为一个隐形的容器,用于包含需要被Thymeleaf处理的属性。例如:

    Welcome, Admin!

    在这个例子中,如果 user.isAdmin() 返回 true,那么

    Welcome, Admin!

    就会被渲染到最终的HTML中;否则,整个块都会被忽略。

    使用场景:

    条件显示:如上面的例子所示,可以使用 th:block 结合 th:if 或 th:unless 来根据条件控制内容的显示或隐藏。

    迭代:也可以使用 th:block 来进行循环操作,而不需要额外的HTML标签:

    Item name here...

    Item description here...

    在这个例子中,对于 items 列表中的每个 item,都会生成相应的段落而不添加额外的HTML结构。

    局部变量声明:通过 th:with 属性来定义局部变量,可以在不影响HTML结构的情况下简化表达式或数据准备:

    Total price: Total here...

    th:block 提供了一种灵活且干净的方式来应用Thymeleaf的逻辑处理能力,同时保持了输出HTML的简洁和语义准确性。这使得它成为优化模板代码结构的一个强大工具。

    # 12 内联 # 12.1 表达式内联 尽管标准方言允许我们使用标签属性完成几乎所有操作,但在某些情况下,我们可能更倾向于直接将表达式写入 HTML 文本中。例如,我们可能更倾向于这样写:

    Hello, [[${session.user.name}]]!

    而不是这样:

    Hello, Sebastian!

    在 Thymeleaf 中,[[...]] 或 [(...)] 之间的表达式被视为内联表达式,我们可以在其中使用任何在 th:text 或 th:utext 属性中有效的表达式。

    需要注意的是,[[...]] 对应于 th:text(即结果会被 HTML 转义),而 [(...)] 对应于 th:utext,不会执行任何 HTML 转义。因此,对于变量 msg = 'This is great!',给定以下片段:

    The message is "[(${msg})]"

    结果将不会转义 标签,因此:

    The message is "This is great!"

    而如果使用转义:

    The message is "[[${msg}]]"

    结果将被 HTML 转义:

    The message is "This is <b>great!</b>"

    需要注意的是,文本内联默认是启用的,作用在标记的标签体内,而不是标签本身,因此我们无需手动启用它。

    # 内联与自然模板 如果你来自其他模板引擎,可能会问:为什么我们不从一开始就这样做呢?这比所有的 th:text 属性代码少多了!

    嗯,要小心,因为虽然你可能会发现内联非常有趣,但你应该始终记住,内联表达式在静态打开 HTML 文件时会原样显示,因此你可能无法再将它们用作设计原型了!

    不使用内联的情况下,浏览器静态显示我们的代码片段:

    Hello, Sebastian!

    而使用内联的情况下:

    Hello, [[${session.user.name}]]!

    在设计实用性上的区别非常明显。

    # 禁用内联 可以通过 th:inline="none" 禁用内联机制,因为有时我们可能希望输出 [[...]] 或 [(...)] 序列而不将其内容作为表达式处理:

    A double array looks like this: [[1, 2, 3], [4, 5]]!

    这将导致:

    A double array looks like this: [[1, 2, 3], [4, 5]]!

    # 12.2 文本内联 文本内联与我们刚刚看到的表达式内联非常相似,但它实际上增加了更多的功能。它必须通过 th:inline="text" 显式启用。

    文本内联不仅允许我们使用相同的内联表达式,而且实际上将标签体视为在 TEXT 模板模式下处理的模板,这允许我们执行基于文本的模板逻辑(不仅仅是输出表达式)。

    我们将在下一章关于文本模板模式的部分中进一步讨论这一点。

    # 12.3 JavaScript 内联 JavaScript 内联允许在 HTML 模板模式下更好地集成 JavaScript

    这将导致:

    需要注意两件重要的事情:

    首先,JavaScript 内联不仅会输出所需的文本,还会用引号括起来并对内容进行 JavaScript 转义,因此表达式结果将作为格式良好的 JavaScript 字面量输出。

    其次,这是因为我们使用双括号表达式 [[${session.user.name}]] 输出 ${session.user.name} 表达式作为转义。如果我们使用非转义:

    结果将如下:

    这是格式错误的 JavaScript 代码。但如果我们通过追加内联表达式构建脚本的某些部分,输出非转义内容可能是我们所需要的,因此这是一个很好的工具。

    # JavaScript 自然模板 JavaScript 内联机制的智能性远不止于应用 JavaScript 特定的转义并将表达式结果输出为有效的字面量。

    例如,我们可以将(转义的)内联表达式包装在 JavaScript 注释中:

    Thymeleaf 将忽略我们在注释后和分号前写入的所有内容(在本例中为 'Gertrud Kiwifruit'),因此执行此操作的结果将与我们不使用包装注释时完全相同:

    但请仔细查看原始模板代码:

    请注意,这是有效的 JavaScript 代码。并且当你以静态方式打开模板文件时(不在服务器上执行),它将完美执行。

    因此,我们在这里拥有了一种实现JavaScript 自然模板的方式!

    # 高级内联评估和 JavaScript 序列化 关于 JavaScript 内联的一个重要注意事项是,这种表达式评估是智能的,并且不仅限于字符串。Thymeleaf 将正确地将以下类型的对象写入 JavaScript 语法:

    字符串 数字 布尔值 数组 集合 映射 Bean(具有 getter 和 setter 方法的对象) 例如,如果我们有以下代码:

    ${session.user} 表达式将评估为一个 User 对象,Thymeleaf 将正确将其转换为 JavaScript 语法:

    这种 JavaScript 序列化的方式是通过实现 org.thymeleaf.standard.serializer.IStandardJavaScriptSerializer 接口来完成的,该接口可以在模板引擎使用的 StandardDialect 实例中进行配置。

    默认的 JS 序列化机制会在类路径中查找 Jackson 库 (opens new window),如果存在,则使用它。如果不存在,它将应用一个内置的序列化机制,该机制涵盖了大多数场景的需求并产生类似的结果(但灵活性较低)。

    # 12.4 CSS 内联 Thymeleaf 还允许在 CSS

    例如,假设我们有两个变量设置为两个不同的 String 值:

    classname = 'main elems'

    align = 'center'

    我们可以像这样使用它们:

    结果将是:

    请注意,CSS 内联也具有一定的智能性,就像 JavaScript 一样。具体来说,通过转义表达式(如 [[${classname}]])输出的表达式将被转义为CSS 标识符。这就是为什么我们的 classname = 'main elems' 在上面的代码片段中变成了 main\ elems。

    # 高级功能:CSS 自然模板等 与之前对 JavaScript 的解释类似,CSS 内联还允许我们的

    # 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] 一样关闭,独立标签可以通过在打开元素中加上 / 来声明,几乎等同于 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 模板中的