Thymeleaf

教程:使用 Thymeleaf

1 介绍 Thymeleaf

1.1 什么是 Thymeleaf?

Thymeleaf 是一个现代的服务器端 Java 模板引擎,适用于 Web 和独立环境,能够处理 HTML、XML、JavaScript、CSS 甚至纯文本。

Thymeleaf 的主要目标是提供一种优雅且易于维护的方式来创建模板。为实现这一目标,它基于以下概念构建:自然模板将其逻辑注入到模板文件中,这种方式不会影响模板作为设计原型的使用。这改善了设计人员和开发团队之间的沟通,并弥合了两者的差距。

从一开始,Thymeleaf 的设计就考虑到了网页标准——尤其是HTML5——如果你有这方面的需求,它允许你创建完全符合验证要求的模板。

1.2 Thymeleaf 可以处理哪些类型的模板?

开箱即用,Thymeleaf 允许你处理六种类型的模板,每种类型被称为一种模板模式(Template Mode):

  • HTML
  • XML
  • TEXT
  • JAVASCRIPT
  • CSS
  • RAW

存在两种标记模板模式 (HTMLXML),三种文本模板模式 (TEXT, JAVASCRIPTCSS),以及一种无操作模板模式 (RAW)。

这个HTML该模板模式可以接受任何种类的 HTML 输入,包括 HTML5、HTML4 和 XHTML。不会进行验证或格式良好性检查,在输出中将尽可能尊重模板代码/结构。

这个XML该模板模式可以接受 XML 输入。在这种情况下,代码必须是格式良好的——不允许未闭合的标签、未加引号的属性等——如果发现格式不良的情况,解析器将抛出异常。请注意不执行验证,仅检查格式良好性。验证(根据 DTD 或 XML Schema)将会被执行。

这个TEXT模板模式将允许使用一种特殊的语法来处理非标记性质的模板。此类模板的示例可能包括文本邮件或模板化文档。请注意,HTML 或 XML 模板也可以作为TEXT来进行处理,在这种情况下它们不会被解析为标记,每一个标签、文档类型声明(DOCTYPE)、注释等都将被视为纯文本。

这个JAVASCRIPT模板模式将允许在 Thymeleaf 应用中处理 JavaScript 文件。这意味着可以像在 HTML 文件中一样在 JavaScript 文件内部使用模型数据,但还带有特定于 JavaScript 的集成特性,比如专门的转义功能或自然脚本。第一个JAVASCRIPT模板模式被认为是一种文本模式,因此使用与TEXT template mode.

这个CSS模板模式将允许在 Thymeleaf 应用中处理 CSS 文件。类似于JAVASCRIPT模式,CSS模板模式也是一种文本模式,并使用来自TEXT template mode.

这个RAW模板模式将完全不处理模板。它旨在用于将未改动的资源(文件、URL 响应等)插入到正在处理的模板中。例如,可以将外部不受控的 HTML 格式资源安全地包含到应用程序模板中,因为我们清楚这些资源可能包含的任何 Thymeleaf 代码都不会被执行。

1.3 方言:标准方言

Thymeleaf 是一个极具可扩展性的模板引擎(实际上它可以被称为一个模板引擎框架,它允许您定义并自定义模板的处理方式,达到非常细致的程度。

对标记元素(一个标签、一些文本、一条注释,或者如果模板不是标记的话只是一个占位符)应用某些逻辑的对象称为一个处理器处理器,而一组这样的处理器——可能再加上一些额外的组件——就构成了一个方言(dialect)方言。Thymeleaf 的核心库提供了一种名为标准方言标准方言的内置方言,这应该足以满足大多数用户的需求。

请注意,方言实际上可以没有处理器并且仅由其他类型的组件组成,但处理器绝对是更常见的使用场景。

本教程介绍了标准方言。接下来几页中介绍的每个属性和语法功能都是由此方言定义的,即使没有明确提及也是如此。

当然,如果用户希望定义自己的处理逻辑同时又利用该库的高级功能,则可以创建自己的方言(甚至扩展标准方言)。Thymeleaf 还可以配置为同时使用多个方言。

官方的 thymeleaf-spring3 和 thymeleaf-spring4 集成包都定义了一个名为“SpringStandard Dialect”(Spring 标准方言)的方言,它基本与标准方言一致,但有少量调整以更好地利用 Spring 框架中的某些特性(例如,使用 Spring 表达式语言(SpringEL)代替 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 好时光虚拟杂货店(Good Thymes Virtual Grocery)

本指南以及后续章节中所示示例的源代码可以在Good Thymes Virtual Grocery (GTVG)示例应用程序中找到,该应用有两个(等效)版本:

2.1 一家杂货店网站

为了更好地解释使用 Thymeleaf 处理模板所涉及的概念,本教程将使用一个演示应用程序,您可以从该项目的官方网站下载它。

该应用程序是一个虚构的虚拟杂货店网站,将为我们提供许多展示 Thymeleaf 各种特性的场景。

开始之前,我们需要为我们的应用程序准备一套简单的模型实体:Products所售的商品Customers通过创建Orders订单来卖给顾客。我们还将管理关于这些Comments的信息。Products:

Example application model
示例应用程序模型

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

public class ProductService {

    ...

    public List<Product> findAll() {
        return ProductRepository.getInstance().findAll();
    }

    public Product findById(Integer id) {
        return ProductRepository.getInstance().findById(id);
    }
    
}

在 Web 层,我们的应用程序将拥有一个过滤器,它将根据请求 URL 把执行任务委托给启用了 Thymeleaf 的命令:


/*
 * The application object needs to be declared first (implements IWebApplication)
 * In this case, the Jakarta-based version will be used.
 */
public void init(final FilterConfig filterConfig) throws ServletException {
    this.application =
            JakartaServletWebApplication.buildApplication(
                filterConfig.getServletContext());
    // We will see later how the TemplateEngine object is built and configured
    this.templateEngine = buildTemplateEngine(this.application);
}

/*
 * Each request will be processed by creating an exchange object (modeling
 * the request, its response and all the data needed for this process) and
 * then calling the corresponding controller.
 */
private boolean process(HttpServletRequest request, HttpServletResponse response)
        throws ServletException {
    
    try {

        final IWebExchange webExchange = 
            this.application.buildExchange(request, response);
        final IWebRequest webRequest = webExchange.getRequest();

        // This prevents triggering engine executions for resource URLs
        if (request.getRequestURI().startsWith("/css") ||
                request.getRequestURI().startsWith("/images") ||
                request.getRequestURI().startsWith("/favicon")) {
            return false;
        }
        
        /*
         * Query controller/URL mapping and obtain the controller
         * that will process the request. If no controller is available,
         * return false and let other filters/servlets process the request.
         */
        final IGTVGController controller = 
            ControllerMappings.resolveControllerForRequest(webRequest);
        if (controller == null) {
            return false;
        }

        /*
         * Write the response headers
         */
        response.setContentType("text/html;charset=UTF-8");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);

        /*
         * Obtain the response writer
         */
        final Writer writer = response.getWriter();

        /*
         * Execute the controller and process view template,
         * writing the results to the response writer. 
         */
        controller.process(webExchange, this.templateEngine, writer);
        
        return true;
        
    } catch (Exception e) {
        try {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final IOException ignored) {
            // Just ignore this
        }
        throw new ServletException(e);
    }
    
}

这是我们的IGTVGController接口:

public interface IGTVGController {

    public void process(
            final IWebExchange webExchange, 
            final ITemplateEngine templateEngine,
            final Writer writer)
            throws Exception;
    
}

现在我们要做的就是创建IGTVGController接口的实现,从服务中检索数据,并使用ITemplateEngine对象。

最终效果将如下所示:

Example application home page
示例应用程序首页

但首先让我们看看模板引擎是如何初始化的。

2.2 创建并配置模板引擎

这个init(...)方法中的过滤器包含以下这一行代码:

this.templateEngine = buildTemplateEngine(this.application);

现在我们来看看org.thymeleaf.TemplateEngine对象是如何初始化的:

private static ITemplateEngine buildTemplateEngine(final IWebApplication application) {

    // Templates will be resolved as application (ServletContext) resources
    final WebApplicationTemplateResolver templateResolver = 
            new WebApplicationTemplateResolver(application);

    // HTML is the default mode, but we will set it anyway for better understanding of code
    templateResolver.setTemplateMode(TemplateMode.HTML);
    // This will convert "home" to "/WEB-INF/templates/home.html"
    templateResolver.setPrefix("/WEB-INF/templates/");
    templateResolver.setSuffix(".html");
    // Set template cache TTL to 1 hour. If not set, entries would live in cache until expelled by LRU
    templateResolver.setCacheTTLMs(Long.valueOf(3600000L));

    // Cache is set to true by default. Set to false if you want templates to
    // be automatically updated when modified.
    templateResolver.setCacheable(true);

    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 {

    ...
  
    /*
     * Templates are resolved by their name (or content) and also (optionally) their 
     * owner template in case we are trying to resolve a fragment for another template.
     * Will return null if template cannot be handled by this template resolver.
     */
    public TemplateResolution resolveTemplate(
            final IEngineConfiguration configuration,
            final String ownerTemplate, final String template,
            final Map<String, Object> templateResolutionAttributes);
}

接口的对象。这些对象负责确定我们将如何访问模板,在 GTVG 应用程序中,使用org.thymeleaf.templateresolver.WebApplicationTemplateResolver表示我们将通过IWebApplication对象以资源形式获取模板文件:一个 Thymeleaf 的抽象概念,在基于 Servlet 的应用程序中,它基本上是对 Servlet API[javax|jakarta].servlet.ServletContext对象的封装,并且它会从 Web 应用程序的根目录解析资源。

但这还不是关于模板解析器要说的全部内容,因为我们可以设置一些配置参数。首先,模板模式:

templateResolver.setTemplateMode(TemplateMode.HTML);

HTML 是WebApplicationTemplateResolver, but it is good practice to establish it anyway so that our code documents clearly what is going on.

templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");

这个前缀后缀将修改我们将要传递给引擎的模板名称,从而得到实际使用的资源名称。

使用此配置时,模板名称“product/list”将对应于:

servletContext.getResourceAsStream("/WEB-INF/templates/product/list.html")

可选地,可以通过模板解析器中的cacheTTLMs属性来配置已解析模板在缓存中保留的时间长度:

templateResolver.setCacheTTLMs(3600000L);

如果达到最大缓存大小且该模板是最旧的缓存条目,则模板仍可能在 TTL 到期之前被移出缓存。

用户可以通过实现ICacheManager接口,或者通过修改StandardCacheManager对象以管理默认缓存,来自定义缓存行为和大小。

关于模板解析器还有很多需要学习的内容,但现在我们先来看看模板引擎对象的创建。

模板引擎

模板引擎对象是org.thymeleaf.ITemplateEngine接口的实现。Thymeleaf 核心提供了其中一个实现:org.thymeleaf.TemplateEngine,我们在这里创建它的实例:

templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(templateResolver);

相当简单,不是吗?我们只需要创建一个实例并为其设置模板解析器即可。

模板解析器是唯一必需的参数,TemplateEngine需要它,尽管还有许多其他参数将在后面介绍(消息解析器、缓存大小等)。就目前而言,这已经足够了。

我们的模板引擎现在已经准备好了,我们可以开始使用 Thymeleaf 创建页面。

3 使用文本

3.1 多语言欢迎信息

我们的第一个任务是为我们这个杂货网站创建一个首页。

这个页面的第一个版本将非常简单:只有一个标题和一条欢迎消息。这是我们的/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 规范。事实上,我们甚至还在xmlns:th属性添加到了<html>标签上,这完全是不符合 HTML5 规范的做法:

<html xmlns:th="http://www.thymeleaf.org">

……这对模板处理没有任何影响,但它充当了一个咒语来防止 IDE 抱怨缺少命名空间定义的问题,对于那些带有前缀的th:*属性中填充值。

所以如果我们想让这个模板符合 HTML5要怎么做呢?很简单:切换到 Thymeleaf 的 data 属性语法,使用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- prefixed attributes are allowed by the HTML5 specification, so, with this code above, our template would be a 合法的 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 控制器。

上下文(Contexts)

为了处理我们的模板,我们将创建一个HomeController实现了之前看到的IGTVGController接口的类:

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接口的对象。上下文中应该通过变量 map 包含模板引擎执行所需的所有数据,并引用用于外部化消息的语言环境。

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}将返回一个变量x存储在 Thymeleaf 上下文中或作为交换属性(在 Servlet 术语中称为“请求属性”)。
  • ${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 &lt;b&gt;fantastic&lt;/b&gt; 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);

}

我们向上下文中添加了一个名为Stringtoday变量,现在我们可以在模板中显示它:

<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(对象图导航语言)的语言编写的表达式,该表达式将在我们之前讨论过的上下文变量 map 上执行。

这个${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
    • 字面令牌:one, sometext, main,…
  • 文本操作:
    • 字符串连接:+
    • 文字替换:|The name is ${name}|
  • 算术运算:
    • 二元运算符:+, -, *, /, %
    • 减号(一元运算符):-
  • 布尔运算:
    • 二元运算符:and, or
    • 布尔取反(一元运算符):!, not
  • 比较和相等性:
    • 比较运算符:>, <, >=, <= (gt, lt, ge, le)
    • 相等性运算符:==, != (eq, ne)
  • 条件运算符:
    • 如果-那么:(if) ? (then)
    • 如果-那么-否则:(if) ? (then) : (else)
    • 默认值:(value) ?: (defaultvalue)
  • 特殊标记:
    • 无操作:_

所有这些特性都可以组合和嵌套使用:

'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标准语法指定的,这意味着你可以根据API文档中指定的方式对数字和日期进行格式化。java.text.*包中。

为了指定我们参数的值,并给定一个名为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 变量

我们已经提到过,${...}表达式实际上是执行上下文变量 map 中对象的 OGNL(对象图导航语言)表达式。

有关OGNL语法和特性的详细信息,请阅读OGNL语言指南

在启用了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 的功能之一。让我们看一些更多的例子:

/*
 * Access to properties using the point (.). Equivalent to calling property getters.
 */
${person.father.name}

/*
 * Access to properties can also be made by using brackets ([]) and writing 
 * the name of the property as a variable or between single quotes.
 */
${person['father']['name']}

/*
 * If the object is a map, both dot and bracket syntax will be equivalent to 
 * executing a call on its get(...) method.
 */
${countriesByCode.ES}
${personsByName['Stephen Zucchini'].age}

/*
 * Indexed access to arrays or collections is also performed with brackets, 
 * writing the index without quotes.
 */
${personsArray[0].name}

/*
 * Methods can be called, even with arguments.
 */
${person.createCompleteName()}
${person.createCompleteNameWithSeparator('-')}

表达式基本对象

当在上下文变量上评估 OGNL 表达式时,为了提高灵活性,一些对象会被提供给表达式使用。这些对象将按照 OGNL 标准以#符号开头进行引用:

  • #ctx: 上下文对象。
  • #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#datesjava.util.Calendar对象的形式,在完整的元素及其内容上执行。
  • #temporals的方法。java.timeJDK8+中的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.5 片段

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

最常见的用途是使用th:insertth: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>

布尔字面量

布尔字面量是truefalse中的标签一样。例如:

<div th:if="${user.isAdmin()} == false"> ...

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

<div th:if="${user.isAdmin() == false}"> ...

null 字面量

这个null还可以使用

<div th:if="${variable.something} == null"> ...

字面量标记

数字、布尔和 null 字面量实际上是字面量标记.

这些标记在标准表达式中可以稍微简化一些操作。它们的工作方式与文本字面量完全相同('...'),但只允许字母(A-Za-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}">

4.8 字面量替换

字面量替换允许轻松格式化包含变量值的字符串,而无需使用'...' + '...'.

进行字面量拼接。|),例如:

<span th:text="|Welcome to our application, ${user.name}!|">

相当于:

<span th:text="'Welcome to our application, ' + ${user.name} + '!'">

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

<span th:text="${onevar} + ' ' + |${twovar}, ${threevar}|">

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

4.9 算术运算

还可以使用一些算术运算:+, -, *, /%.

<div th:with="isEven=(${prodStat.count} % 2 == 0)">

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

<div th:with="isEven=${prodStat.count % 2 == 0}">

注意某些运算符存在对应的文本别名:div (/),mod (%)。

4.10 比较器和相等性判断

表达式中的值可以通过>, <, >=<=符号以及==!=运算符来进行比较以检查相等性(或不相等)。注意 XML 规定不应在属性值中使用<>符号,因此应将其替换为&lt;&gt;.

<div th:if="${prodStat.count} &gt; 1">
<span th:text="'Execution mode is ' + ( (${execMode} == 'dev')? 'Development' : 'Production')">

更简单的替代方式可能是使用部分运算符对应的文本别名:gt (>),lt (<),ge (>=),le (<=),not (!)以及eq (==),neq/ne (!=)。

4.11 条件表达式

条件表达式用于根据一个条件表达式的求值结果选择两个表达式中的一个进行求值(该条件本身就是另一个表达式)。

我们来看一个示例片段(引入了另一个属性修改器, th:class):

<tr th:class="${row.even}? 'even' : 'odd'">
  ...
</tr>

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

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

<tr th:class="${row.even}? (${row.first}? 'first' : 'even') : 'odd'">
  ...
</tr>

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

<tr th:class="${row.even}? 'alt'">
  ...
</tr>

4.12 默认表达式(Elvis 运算符)

A 默认表达式是一种没有then部分的条件值。它等价于一些语言(比如 Groovy)中存在的Elvis 运算符,它允许你指定两个表达式:如果第一个表达式不为空,则使用第一个;如果为空,则使用第二个。

让我们在用户资料页面上看看它的实际效果:

<div th:object="${session.user}">
  ...
  <p>Age: <span th:text="*{age}?: '(no age specified)'">27</span>.</p>
</div>

,我们在这里用它为名称指定一个默认值(在这个例子中是一个字面值),只有当表达式?:?:*{age}的求值结果为空时才使用该默认值。因此这等价于:

<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>

作为原型文本,这样得到的代码从设计角度来看既更加简洁又更加多用途:'未认证用户' as a prototyping text, which results in code that is both more concise and versatile from a design standpoint:

<span th:text="${user.name} ?: _">no user authenticated</span>

4.14 数据转换/格式化

Thymeleaf 定义了一种双括号语法,用于变量(${...})和选择(*{...})表达式,这种语法允许我们通过配置好的数据转换来实现转换服务.

其基本方式如下:

<td th:text="${{user.lastAccessDate}}">...</td>

。它指示 Thymeleaf 将${{...}}user.lastAccessDate,并要求它在写入结果之前执行一次转换服务 and asks it to perform a 格式化操作(转换为String)。

假设user.lastAccessDate的类型是java.util.Calendar,如果已经注册了转换服务(一个IStandardConversionService接口的实现)并且其中包含了针对Calendar -> String的有效转换规则,那么该转换就会被执行。

来进行IStandardConversionService实现(StandardConversionService类)只是对转换为.toString()的任意对象执行String方法。关于如何注册自定义转换服务实现的更多信息,请参阅配置进阶章节。

官方 thymeleaf-spring3 和 thymeleaf-spring4 集成包透明地将 Thymeleaf 的转换服务机制与 Spring 自身的转换服务基础设施集成在一起,使得在 Spring 配置中声明的转换服务和格式化器会自动对${{...}}*{{...}}表达式。

提供支持。

除了这些表达式处理功能之外,Thymeleaf 还有一个预处理是在正常处理之前执行的一次表达式处理,它允许修改最终会被执行的表达式。表达式。

功能。

经过预处理的表达式和普通的表达式完全相同,但它们被双下划线符号包围(例如__${expression}__)。

假设我们有一个国际化Messages_fr.properties条目,其中包含一个调用特定语言静态方法的 OGNL 表达式,比如:

article.text=@myapp.translator.Translator@translateToFrench({0})

…还有一个Messages_es.properties equivalent:

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>

除了新的属性值外,您还可以看到应用上下文名称已按照前一章所述自动添加到了/gtvg/subscribe中的 URL 基路径前面。

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

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

在提供了所需的 messages 文件的情况下,这将输出:

<img src="/gtgv/images/gtvglogo.png" title="Logo de Good Thymes" alt="Logo de Good Thymes" />

5.2 设置特定属性的值

到目前为止,你可能觉得类似这样的写法:

<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>

…看起来相当丑陋。在属性值中指定赋值虽然非常实用,但如果总是这样做,就不是创建模板最优雅的方式了。

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

那样)。value这样看起来好多了!让我们尝试对th:value:

<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>

也做同样的事情。action<select>form标签中指定命令对象:

<form action="subscribe.html" th:action="@{/subscribe}">

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

<li><a href="product/list.html" th:href="@{/product/list}">Product List</a></li>

像这样的属性有很多,每个都针对特定的 HTML5 属性:

th:abbrth:acceptth:accept-charset
th:accesskeyth:actionth:align
th:altth:archiveth:audio
th:autocompleteth:axisth:background
th:bgcolorth:borderth:cellpadding
th:cellspacingth:challengeth:charset
th:citeth:classth:classid
th:codebaseth:codetypeth:cols
th:colspanth:compactth:content
th:contenteditableth:contextmenuth:data
th:datetimeth:dirth:draggable
th:dropzoneth:enctypeth:for
th:formth:formactionth:formenctype
th:formmethodth:formtargetth:fragment
th:frameth:frameborderth:headers
th:heightth:highth:href
th:hreflangth:hspaceth:http-equiv
th:iconth:idth:inline
th:keytypeth:kindth:label
th:langth:listth:longdesc
th:lowth:manifestth:marginheight
th:marginwidthth:maxth:maxlength
th:mediath:methodth:min
th:nameth:onabortth:onafterprint
th:onbeforeprintth:onbeforeunloadth:onblur
th:oncanplayth:oncanplaythroughth:onchange
th:onclickth:oncontextmenuth:ondblclick
th:ondragth:ondragendth:ondragenter
th:ondragleaveth:ondragoverth:ondragstart
th:ondropth:ondurationchangeth:onemptied
th:onendedth:onerrorth:onfocus
th:onformchangeth:onforminputth:onhashchange
th:oninputth:oninvalidth:onkeydown
th:onkeypressth:onkeyupth:onload
th:onloadeddatath:onloadedmetadatath:onloadstart
th:onmessageth:onmousedownth:onmousemove
th:onmouseoutth:onmouseoverth:onmouseup
th:onmousewheelth:onofflineth:ononline
th:onpauseth:onplayth:onplaying
th:onpopstateth:onprogressth:onratechange
th:onreadystatechangeth:onredoth:onreset
th:onresizeth:onscrollth:onseeked
th:onseekingth:onselectth:onshow
th:onstalledth:onstorageth:onsubmit
th:onsuspendth:ontimeupdateth:onundo
th:onunloadth:onvolumechangeth:onwaiting
th:optimumth:patternth:placeholder
th:posterth:preloadth:radiogroup
th:relth:revth:rows
th:rowspanth:rulesth:sandbox
th:schemeth:scopeth:scrolling
th:sizeth:sizesth:span
th:spellcheckth:srcth:srclang
th:standbyth:startth:step
th:styleth:summaryth:tabindex
th:targetth:titleth:type
th:usemapth:valueth:valuetype
th:vspaceth:widthth:wrap
th:xmlbaseth:xmllangth:xmlspace

5.3 一次设置多个值

有两个非常特殊的属性叫做th:alt-titleth:lang-xmllang它们可用于同时将两个属性设置为相同的值。具体来说:

  • th:alt-title将会设置alttitle.
  • th:lang-xmllang将会设置langxml: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:attrappendth: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" />

在标准方言中还有两个专门的附加属性:用于在不覆盖现有类的情况下添加 CSS 类或th:classappendth:styleappend属性,style的代码片段:

<tr th:each="prod : ${prods}" class="row" th:classappend="${prodStat.odd}? 'odd'">

(不用担心那个th:each属性。它是一个迭代属性,我们稍后再讨论。)

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:asyncth:autofocusth:autoplay
th:checkedth:controlsth:declare
th:defaultth:deferth:disabled
th:formnovalidateth:hiddenth:ismap
th:loopth:multipleth:novalidate
th:nowrapth:openth:pubdate
th:readonlyth:requiredth:reversed
th:scopedth:seamlessth: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:* one, it does not replace it. There is no intention at all to deprecate the namespaced syntax in the future.

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} the 被迭代的表达式被迭代变量.
  • 我们将prod the 迭代变量或简称为iter 变量.

请注意,proditer 变量的作用域限定在<tr>元素内,这意味着它对像<td>.

可迭代值

这个java.util.List类并不是 Thymeleaf 中唯一可用作迭代的数据类型。实际上,有一组相当全面的对象被认为是可被迭代的:可迭代对象可迭代对象:th:each属性指定:

  • 任何实现了java.util.Iterable
  • 任何实现了java.util.Enumeration.
  • 任何实现了java.util.Iterator接口的对象,其值会按照迭代器返回的顺序直接使用,不需要缓存所有值到内存中。
  • 任何实现了java.util.Map接口的 Map 对象。当迭代 Map 时,迭代变量类型为java.util.Map.Entry.
  • 任何实现了java.util.stream.Stream.
  • 接口的 Enumeration 对象。
  • 任何数组。

6.2 保持迭代状态

使用th:each时,Thymeleaf 提供了一种机制来跟踪你的迭代状态:即状态变量.

状态变量是在th:each属性中定义的,包含了如下数据:

  • 当前迭代索引,从 0 开始。这是index属性。
  • 当前迭代索引,从 1 开始。这是count属性。
  • 被迭代变量中的元素总数。这是size属性。
  • 这个iter 变量每次迭代。这是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属性中定义的,通过在其自身之后写入状态变量名并用逗号分隔。与 iter 变量一样,状态变量的作用域也限定于包含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>

注意到我们的迭代状态变量完美地工作了,只为奇数行设置了oddCSS 类。

如果你没有显式地设置一个状态变量,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>

但如果loadValue()方法不会被调用)condition的值在如下代码中false的话,则永远不会初始化该变量:

<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按照如下规则进行计算:

  • 如果值不是 null:
    • 如果值是布尔值并且为true.
    • 如果值是一个数字并且非零
    • 如果值是一个字符并且非零
    • 如果值是一个字符串并且不是 "false"、"off" 或 "no"
    • 如果值不是一个布尔值、数值、字符或字符串。
  • (如果值为 null,则 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 语句

也可以使用类似于 Java 中的switch结构的方式有条件地显示内容:这就是th:switch / th:case属性集。

<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#{roles.manager}">User is a manager</p>
</div>

注意,一旦有某个th:case属性被计算为true,那么在同一 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">
      &copy; 2011 The Good Thymes Virtual Grocery
    </div>
  
  </body>
  
</html>

上述代码定义了一个名为copy的片段,我们可以轻松地在主页中使用其中一个th:insertth:replace属性:

<body>

  ...

  <div th:insert="~{footer :: copy}"></div>
  
</body>

注意th:insert需要一个片段表达式 (~{...}),这是一个能产生一个片段的表达式。片段表达式的语法.

片段指定语法

的语法非常简单。共有三种不同的格式:片段表达式 is quite straightforward. There are three different formats:

  • "~{templatename::selector}"包含对名为templatename。请注意selector的写法。~{templatename::fragmentname}如同上面例子中的那样,~{footer :: copy}上面所述的那种方式。

    Markup Selector 的语法由底层的 AttoParser 解析库定义,类似于 XPath 表达式或 CSS 选择器。更多信息请参见附录 C

  • "~{templatename}"包含名为templatename.

    注意,在th:insert/th:replace标签中使用的模板名称必须能够被当前模板引擎所使用的模板解析器解析。

  • ~{::selector}""~{this::selector}"插入当前模板中匹配selector的片段。如果在当前模板中未找到匹配的片段,则会在模板调用栈(插入操作)中逐级向上查找,直到最初处理的模板(即根模板)为止,寻找在某一级是否能找到匹配项。selector上述示例中的

两者templatenameselector可以是功能完整的表达式(甚至可以是条件表达式),例如:

<div th:insert="~{ footer :: (${user.isAdmin}? #{footer.admin} : #{footer.normaluser}) }"></div>

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

采用这种片段方式的一个很大优势是,你可以将片段写在浏览器可完美显示的页面中,这些页面具有完整甚至有效标记结构,同时仍然保留Thymeleaf将其包含到其他模板中的能力。

不使用th:fragment

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

...
<div id="copy-section">
  &copy; 2011 The Good Thymes Virtual Grocery
</div>
...

我们可以简单地通过其id属性来引用该片段,类似于CSS选择器的方式:

<body>

  ...

  <div th:insert="~{footer :: #copy-section}"></div>
  
</body>

th:insertth:replace

之间的区别是什么?th:insertth:replace?

  • th:insert将把指定的片段插入为主机标签的内容体中。

  • th:replace实际上会替换其主机标签为指定的片段。

所以像这样的HTML片段:

<footer th:fragment="copy">
  &copy; 2011 The Good Thymes Virtual Grocery
</footer>

…在主机标签中包含两次…<div>标签来定义要通过 AJAX 渲染的片段,例如:

<body>

  ...

  <div th:insert="~{footer :: copy}"></div>

  <div th:replace="~{footer :: copy}"></div>
  
</body>

…结果将是:

<body>

  ...

  <div>
    <footer>
      &copy; 2011 The Good Thymes Virtual Grocery
    </footer>
  </div>

  <footer>
    &copy; 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>

这需要使用以下两种语法之一从th:insertth:replace:

<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})}">

这相当于结合了th:replaceth:with:

<div th:replace="~{::frag}" th:with="onevar=${value1},twovar=${value2}">

注意对于一个片段的局部变量的声明——无论它是否有参数签名——都不会在其执行前清空上下文。片段仍然能够访问调用模板中正在使用的每个上下文变量,就像现在一样。

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对象的参数……而是标记片段本身。

这让我们能以一种方式创建片段,使其可以通过来自调用模板的标记进行增强,从而实现非常灵活的模板布局机制.

注意在下面的片段中使用了titlelinks变量:

<head th:fragment="common_header(title,links)">

  <title th:replace="${title}">The awesome application</title>

  <!-- Common styles and scripts -->
  <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
  <link rel="shortcut icon" th:href="@{/images/favicon.ico}">
  <script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>

  <!--/* Per-page placeholder for additional links */-->
  <th:block th:replace="${links}" />

</head>

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

...
<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>标签作为titlelinks变量的值,这使得我们的片段在插入过程中被定制化:

...
<head>

  <title>Awesome - Main</title>

  <!-- Common styles and scripts -->
  <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
  <link rel="shortcut icon" href="/awe/images/favicon.ico">
  <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>

  <link rel="stylesheet" href="/awe/css/bootstrap.min.css">
  <link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css">

</head>
...

使用空片段

一个特殊的片段表达式,空片段 (~{}),可用于指定不输出任何标记。继续之前的例子:

<head th:replace="~{ base :: common_header(~{::title},~{}) }">

  <title>Awesome - Main</title>

</head>
...

注意片段的第二个参数(links)被设置为空片段,因此<th:block th:replace="${links}" />区块部分不会输出任何内容:

...
<head>

  <title>Awesome - Main</title>

  <!-- Common styles and scripts -->
  <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
  <link rel="shortcut icon" href="/awe/images/favicon.ico">
  <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>

</head>
...

使用无操作标记

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

...
<head th:replace="~{base :: common_header(_,~{::link})}">

  <title>Awesome - Main</title>

  <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
  <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">

</head>
...

注意到title参数(common_header片段的第一个参数)被设为无操作 (_),结果是该片段部分根本不会被执行(title = 无操作):

  <title th:replace="${title}">The awesome application</title>

因此结果是:

...
<head>

  <title>The awesome application</title>

  <!-- Common styles and scripts -->
  <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
  <link rel="shortcut icon" href="/awe/images/favicon.ico">
  <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>

  <link rel="stylesheet" href="/awe/css/bootstrap.min.css">
  <link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css">

</head>
...

高级条件插入片段

由于同时存在空片段无操作标记,我们可以以非常简便优雅的方式执行片段的条件插入。

例如,如果我们想在用户是管理员时插入我们的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标志——我们可以将片段本身的是否存在作为默认操作中的条件:

...
<!-- The body of the <div> will be used if the "common :: salutation" fragment  -->
<!-- does not exist (or is empty).                                              -->
<div th:insert="~{common :: salutation} ?: _">
    Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support.
</div>
...

8.4 删除模板片段

返回示例应用,让我们重新看一下我们产品列表模板的最新版本:

<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没有理由删除另外两行。

我们需要一种方法在模板处理过程中删除这两行。让我们在第二和第三行使用th:remove属性来实现这一点<tr>标签:

<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"时间:

<table>
  <thead>
    <tr>
      <th>NAME</th>
      <th>PRICE</th>
      <th>IN STOCK</th>
      <th>COMMENTS</th>
    </tr>
  </thead>
  <tbody th:remove="all-but-first">
    <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>
  </tbody>
</table>

这个th:remove属性可以接受任意Thymeleaf标准表达式类型的值,只要它返回一个允许的字符串值(all, tag, body, all-but-firstnone)。

这意味着移除操作可以是条件性的,例如:

<a href="/something" th:remove="${condition}? tag : none">Link text not to be removed</a>

请注意,th:remove会将null视为none的同义词,因此以下写法与上面示例的作用相同:

<a href="/something" th:remove="${condition}? tag">Link text not to be removed</a>

在这种情况下,如果${condition}为 false,null将被返回,因此不会执行任何移除操作。

8.5 布局继承

为了能够使用单个文件作为布局,可以使用片段。一个简单布局的示例包括titlecontent使用th:fragmentth:replace:

<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:replace="${title}">Layout Title</title>
</head>
<body>
    <h1>Layout H1</h1>
    <div th:replace="${content}">
        <p>Layout content</p>
    </div>
    <footer>
        Layout footer
    </footer>
</body>
</html>

此示例声明了一个名为layout的片段,titlecontent作为参数。两者都将在继承它的页面中通过提供片段表达式进行替换,如下例所示。

<!DOCTYPE html>
<html th:replace="~{layoutFile :: layout(~{::title}, ~{::section})}">
<head>
    <title>Page Title</title>
</head>
<body>
<section>
    <p>Page content</p>
    <div>Included on page</div>
</section>
</body>
</html>

在这个文件中,html标签将被layout替换,但在布局中,titlecontent将分别被titlesection模块替换。

如果需要,布局可以由多个片段组成,例如headerfooter.

9 局部变量

Thymeleaf 将局部变量称为那些为模板特定片段定义的,并且只能在该片段内部使用的变量。

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

<tr th:each="prod : ${prods}">
    ...
</tr>

那么这个prod该变量只能在<tr>标签的范围内使用。具体来说:

  • 它对同一个标签内执行顺序优先级较低的th:*属性也可用(这意味着它们会在之后执行),优先级优先级th:each (which means they will execute after 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> tag.

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

<div th:with="firstPer=${persons[0]},secondPer=${persons[1]}">
  <p>
    The name of the first person is <span th:text="${firstPer.name}">Julius Caesar</span>.
  </p>
  <p>
    But the name of the second person is 
    <span th:text="${secondPer.name}">Marcus Antonius</span>.
  </p>
</div>

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

<div th:with="company=${user.company + ' Co.'},account=${accounts[company]}">...</div>

让我们在杂货店主页上使用这个功能吧!还记得我们写的输出格式化日期的代码吗?

<p>
  Today is: 
  <span th:text="${#calendars.format(today,'dd MMMM yyyy')}">13 february 2011</span>
</p>

那么,如果我们希望这个"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具有更高的precedence优先级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 允许定义特殊的注释区块,在静态打开模板时(即作为原型)标记为注释,但在执行模板时由 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(无需添加禁止的<div>块内部<table>),并且在浏览器中静态打开作为原型时仍然可以正常工作!

12 内联

12.1 表达式内联

虽然标准方言允许我们几乎完全通过标签属性实现所有功能,但在某些情况下,我们可能更倾向于直接在 HTML 文本中编写表达式。例如,我们可能更愿意编写以下内容:

<p>Hello, [[${session.user.name}]]!</p>

…而不是这个:

<p>Hello, <span th:text="${session.user.name}">Sebastian</span>!</p>

[[...]][(...)]被认为是内联表达式在 Thymeleaf 中,其中我们可以使用任何在th:textth: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 &lt;b&gt;great!&lt;/b&gt;"</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 内联可以更好地集成 JavaScript<script>在以HTML template mode.

文本内联类似的是,这实际上等同于将脚本内容当作以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语法正确写入以下类型的对象:

  • 字符串
  • 数字
  • 布尔
  • 数组
  • 集合
  • 映射
  • JavaBean(具有gettersetter

例如,如果我们有如下代码:

<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库如果存在,则使用它;否则将应用内置的序列化机制,该机制覆盖大多数场景的需求并产生类似结果(但灵活性较低)。

12.4 CSS内联

Thymeleaf也允许在CSS<style>标签中使用内联,例如:

<style th:inline="css">
  ...
</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, JAVASCRIPTCSS。这将它们与标记模板模式区分开来:HTMLXML.

的关键区别在于文本模板模式与标记模板不同之处在于,在文本模板中没有可以通过属性插入逻辑的标签,所以我们必须依赖其他机制。

这些机制中最基础的是内联内联语法

  Dear [(${name})],

  Please find attached the results of the report you requested
  with name "[(${report.name})]".

  Sincerely,
    The Reporter.

Even without tags, the example above is a complete and valid Thymeleaf template that can be executed in the TEXT template mode.

But in order to include more complex logic than mere output expressions, we need a new non-tag-based syntax:

[# th:each="item : ${items}"]
  - [(${item})]
[/]

Which is actually the condensed更冗长形式的缩写版:

[#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页面调用。请注意,这里并不是不是 a <script>HTML模板中的一个.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);

};

转义元素属性

为了避免与其他可能以其他模式处理的模板部分交互(例如在一个text-模式模板内的HTML模式内联),Thymeleaf 3.0允许在文本语法中的元素属性上进行转义。所以:

  • 模式下的属性将是TEXT template mode will be HTML非转义.
  • 模式下的属性将是JAVASCRIPT template mode will be JavaScript非转义.
  • 模式下的属性将是CSS template mode will be CSS非转义.

所以这在TEXT模式模板中是完全正常的(注意这里的&gt;):

  [# th:if="${120&lt;user.age}"]
     Congratulations!
  [/]

当然在真正的&lt; would make no sense in a 文本模板中毫无意义,但如果我们在一个包含以上代码的HTML模板中处理th:inline="text"块,并希望确保浏览器在静态打开文件作为原型时不会将那个<user.age解释为一个开放标签的名称,那么这样做是一个好主意。

13.2 可扩展性

这种语法的一个优势是它与标记语法一样具有可扩展性。开发者仍然可以定义带有自定义元素和属性的自己的方言,为其应用前缀(可选),然后在文本模板模式中使用它们:

  [#myorg:dosomething myorg:importantattr="211"]some text[/myorg:dosomething]

13.3 文本原型专用注释块:添加代码

这个JAVASCRIPTCSS模式(不适用于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, JAVASCRIPTCSS)都可以指示 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');
  /*[/]*/

当模板静态打开时,上面代码中的那个 alert 将会显示出来——因为它完全是合法的 JavaScript——当模板运行且用户是管理员时也会显示。它等价于:

  [# th:if="${user.admin}"]
     alert('Welcome admin');
  [/]

……而这就是初始版本在模板解析过程中转换成的代码。

但请注意,将元素包裹在注释中不会像内联输出表达式那样清理它们所在的行(向右直到找到;为止)。该行为是专为内联输出表达式保留的。

因此,Thymeleaf 3.0 允许开发人员以自然模板的形式开发复杂的 JavaScript 脚本和 CSS 样式表,同时作为原型(prototype)可工作的模板.

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对象),将其purchasePriceamount属性(通过调用对应的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 示例中,我们选择了一个名为ITemplateResolver的实现类,WebApplicationTemplateResolver它允许我们从应用程序资源中获取模板(在基于 Servlet 的 Web 应用程序中就是指Servlet 上下文)。

除了可以通过实现ITemplateResolver,接口来创建我们自己的模板解析器之外,

  • 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模板名称的字符串template的属性(或者如果我们更喜欢使用解析为模板,在这种情况下,“模板名称”显然不仅仅是名字这么简单):

    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");
  • 使用的模板模式:

    // Default is HTML
    templateResolver.setTemplateMode("XML");
  • 模板缓存的默认模式,以及定义特定模板是否可缓存的模式:

    // Default is true
    templateResolver.setCacheable(false);
    templateResolver.getCacheablePatternSpec().addPattern("/users/*");
  • 由此模板解析器生成的已解析模板缓存条目的生存时间(毫秒)。如果没有设置,则只能通过超出缓存最大容量(剔除最老的条目)来删除缓存条目。

    // Default is no TTL (only cache size exceeded would remove entries)
    templateResolver.setCacheTTLMs(60000L);

Thymeleaf + Spring 集成包提供了一个SpringResourceTemplateResolver实现,

链式模板解析器

此外,模板引擎可以指定多个模板解析器,在这种情况下可以在它们之间建立顺序以进行模板解析,这样,如果第一个解析器无法解析模板,则会询问第二个,依此类推:

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));
// This classloader will not be even asked for any templates not matching these patterns 
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));
classLoaderTempalteResolver.setCheckExistence(true);

这个checkExistence标志位强制解析器在解析阶段执行一次实际检查以确认资源是否存在(如果存在性检查返回 false,则调用链中的下一个解析器)。虽然这在大多数情况下听起来是件好事,但在多数场景下它意味着对资源本身进行两次访问(一次用于检查是否存在,另一次用于读取),这可能会成为某些场景下的性能问题,例如基于远程 URL 的模板资源——这可能造成潜在的性能问题,但通过使用模板缓存可以大大缓解这一问题(在这种情况下,模板只会在它们第一次被访问时被解析

15.2 消息解析器

我们没有为我们的 Grocery 应用显式指定一个消息解析器实现,并且如前所述,这意味着我们使用的是一个org.thymeleaf.messageresolver.StandardMessageResolver对象。

StandardMessageResolverIMessageResolver接口的标准实现,但如果需要,我们可以根据应用程序的具体需求创建自己的实现。

Thymeleaf + Spring 集成包默认提供了一个IMessageResolver实现,它使用标准的 Spring 方式检索外部化的消息,即通过使用MessageSourceSpring 应用上下文中声明的 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 文档。

配置消息解析器

如果我们想向模板引擎添加一个或多个消息解析器该怎么办?很简单:

// For setting only one
templateEngine.setMessageResolver(messageResolver);

// For setting more than one
templateEngine.addMessageResolver(messageResolver);

为什么我们要有多个消息解析器呢?原因与模板解析器相同:消息解析器是有顺序的,如果第一个无法解析某个特定消息,则会询问第二个,然后是第三个,依此类推。

15.3 转换服务

这个转换服务它使我们能够通过双括号语法(${{...}})来进行数据转换和格式化操作。

因此,配置它的方法是将我们自定义的IStandardConversionService接口实现直接设置到StandardDialect的实例中,该实例被配置进模板引擎。像这样:

IStandardConversionService customConversionService = ...

StandardDialect dialect = new StandardDialect();
dialect.setConversionService(customConversionService);

templateEngine.setDialect(dialect);

注意 thymeleaf-spring3 和 thymeleaf-spring4 包包含SpringStandardDialect,并且此方言已经预先配置了IStandardConversionService的实现,将 Spring 自身的转换服务基础设施集成到了 Thymeleaf 中。

15.4 日志记录

Thymeleaf 对日志记录非常重视,并始终努力通过其日志接口提供尽可能多的有用信息。

使用的日志库是slf4j,,实际上它作为桥梁连接着我们应用程序中可能使用的任何日志实现(例如,log4j)。

Thymeleaf 类会以TRACE, DEBUGINFO-级别的信息进行日志记录,具体取决于我们期望的详细程度;除了常规日志外,它还会使用三个与 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 能够工作依赖于一套解析器——用于标记和文本的解析器——这些解析器将模板解析为一系列事件(开始标签、文本、结束标签、注释等),以及一系列处理器——每种类型行为都需要一个处理器——它们修改解析后的模板事件序列,从而结合原始模板与我们的数据来生成我们预期的结果。

默认情况下,它还包含一个缓存,用来存储已解析的模板;即读取和解析模板文件后生成的事件序列。这在开发 Web 应用程序时特别有用,并且建立在以下概念之上:

  • 输入/输出几乎总是任何应用程序中最慢的部分。相比之下,内存中的处理速度非常快。
  • 克隆一个现有的内存内事件序列,总比重新读取一个模板文件、解析它并为其创建一个新的事件序列要快得多。
  • Web 应用程序通常只有几十个模板。
  • 模板文件大小通常是中等或较小,并且在应用程序运行期间不会被修改。

所有这一切表明,在 Web 应用程序中缓存最常用的模板是可行的,而不会浪费大量内存,并且还可以节省很多时间,否则这些时间会被花在对一小部分实际上从不变更的文件的输入/输出操作上。

我们如何控制这个缓存?首先,我们之前已经学过可以在模板解析器中启用或禁用它,甚至可以只对特定的模板起作用:

// Default is true
templateResolver.setCacheable(false);
templateResolver.getCacheablePatternSpec().addPattern("/users/*");

此外,我们可以通过建立自己的缓存管理器对象来修改其配置,StandardCacheManager该对象可以是默认实现的实例:

// Default is 200
StandardCacheManager cacheManager = new StandardCacheManager();
cacheManager.setTemplateCacheMaxSize(100);
...
templateEngine.setCacheManager(cacheManager);

更多关于配置缓存的信息,请参考org.thymeleaf.cache.StandardCacheManager的javadoc API文档。

可以手动从模板缓存中移除条目:

// Clear the cache completely
templateEngine.clearTemplateCache();

// Clear a specific template from the cache
templateEngine.clearTemplateCacheFor("/users/userList");

17 分离式模板逻辑

17.1 分离式逻辑:概念

到目前为止,我们为杂货店开发的模板都是采用通常的常规方式,通过在模板中插入属性形式的逻辑代码来完成。

但是Thymeleaf也允许我们完全分离模板标记与它的逻辑,从而创建出完全无逻辑的标记模板HTMLXML模板模式下。

主要思想是模板逻辑将定义在一个独立的逻辑文件(更准确地说是一个逻辑资源,因为不需要实际存在为一个文件)。默认情况下,该逻辑资源是一个与模板文件位于相同路径(如文件夹)下的额外文件,具有相同的名称但带有.th.xml扩展名:

/templates
+->/home.html
+->/home.th.xml

因此,home.html文件可以完全不包含Thymeleaf逻辑代码。它可能看起来像这样:

<!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.html文件将这个模板转换成Thymeleaf模板,例如像这样:home.th.xml文件:

<?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>

这里我们可以看到大量<attr>标签出现在一个thlogic块中。这些<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属性有什么优势呢?仅仅是因为我们可能不希望往标签中添加太多idclass属性作为逻辑锚点,这可能会导致输出结果变得混乱

同样地,那th:ref的缺点又是什么呢?很明显,我们在模板中引入了一些Thymeleaf逻辑代码(也就是这里的“逻辑”代码)。

注意,th:ref属性的这一用途不仅适用于分离式逻辑模板文件;在其他类型的场景中也能同样发挥作用,比如片段表达式中(~{...})。

17.4 分离式模板的性能影响

影响非常小。当一个被解析的模板被标记为使用分离式逻辑且未被缓存时,首先会解析模板逻辑资源,将其解析并处理成内存中的一系列指令:基本上就是一组将要注入到各个标记选择器的属性列表。

但这只是唯一的额外步骤,因为在这之后,真正的模板才会被解析,而在解析过程中,这些属性会被动态注入到对应的选择器中:on-the-fly由解析器本身完成,这得益于AttoParser中节点选择的高级功能。因此,解析后的节点会像它们在原始模板文件中拥有被注入的属性一样从解析器中输出。

这样做最大的优势是什么?当一个模板被配置为缓存时,它会被缓存时已经包含被注入的属性。因此在使用解耦的模板对于可缓存的模板来说,一旦被缓存,将绝对地.

17.5 解耦逻辑的解析

Thymeleaf 解析与每个模板对应的解耦逻辑资源的方式是用户可以配置的。它由一个扩展点决定,org.thymeleaf.templateparser.markup.decoupled.IDecoupledTemplateLogicResolver,对于这个扩展点提供了一个默认实现StandardDecoupledTemplateLogicResolver.

这个标准实现做了什么?

  • 首先,它会对模板资源的prefix前缀suffixbase name的基础名称(通过其ITemplateResource#getBaseName()方法获取)。前缀和后缀都可以配置,默认情况下,前缀为空,后缀为.th.xml.
  • 第二,它要求模板资源通过其相对资源方法以计算出的名称解析一个ITemplateResource#relative(String relativeLocation)

所使用的IDecoupledTemplateLogicResolver的具体实现可以在TemplateEngine中轻松配置:

final StandardDecoupledTemplateLogicResolver decoupledresolver = 
        new StandardDecoupledTemplateLogicResolver();
decoupledResolver.setPrefix("../viewlogic/");
...
templateEngine.setDecoupledTemplateLogicResolver(decoupledResolver);

18 附录A:表达式基础对象

一些对象和变量映射始终可用并且可以调用。让我们看一下这些对象:

基础对象

  • #ctx:上下文对象。根据我们所处的环境(独立或Web)不同,其实现也不同。org.thymeleaf.context.IContextorg.thymeleaf.context.IWebContext实现类可能不同,但最终指向同一个对象。

    注意#vars#root是该对象的同义词,不过推荐使用#ctx来访问它们。某种意义上,它们充当了

/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.context.IContext
 * ======================================================================
 */

${#ctx.locale}
${#ctx.variableNames}

/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.context.IWebContext
 * ======================================================================
 */

${#ctx.request}
${#ctx.response}
${#ctx.session}
${#ctx.servletContext}
  • #locale:直接访问与当前请求关联的java.util.Localelocale
${#locale}

访问请求/会话属性等的Web上下文命名空间

在Web环境中使用Thymeleaf时,我们可以使用一系列快捷方式来访问请求参数、会话属性和应用程序属性:

注意这些不是上下文对象,而是添加到上下文中的变量映射,所以我们无需使用#. In some way, they act as 命名空间.

  • param:用于检索请求参数。${param.foo}是一个String[]Mapfoo请求参数的值,所以通常会使用${param.foo[0]}来获取第一个值。
/*
 * ============================================================================
 * See javadoc API for class org.thymeleaf.context.WebRequestParamsVariablesMap
 * ============================================================================
 */

${param.foo}              // Retrieves a String[] with the values of request parameter 'foo'
${param.size()}
${param.isEmpty()}
${param.containsKey('foo')}
...
  • session:用于检索会话属性。
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.context.WebSessionVariablesMap
 * ======================================================================
 */

${session.foo}                 // Retrieves the session atttribute 'foo'
${session.size()}
${session.isEmpty()}
${session.containsKey('foo')}
...
  • 应用程序:用于检索应用程序/Servlet上下文属性。
/*
 * =============================================================================
 * See javadoc API for class org.thymeleaf.context.WebServletContextVariablesMap
 * =============================================================================
 */

${application.foo}              // Retrieves the ServletContext atttribute 'foo'
${application.size()}
${application.isEmpty()}
${application.containsKey('foo')}
...

注意访问请求属性不需要指定命名空间(而不是请求参数),因为所有请求属性都会自动作为变量添加到上下文根部:

${myRequestAttribute}

19 附录B:表达式工具对象

执行信息

  • #execInfo:表达式对象,在Thymeleaf标准表达式中提供有关正在处理的模板的有用信息。
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.ExecutionInfo
 * ======================================================================
 */

/*
 * Return the name and mode of the 'leaf' template. This means the template
 * from where the events being processed were parsed. So if this piece of
 * code is not in the root template "A" but on a fragment being inserted
 * into "A" from another template called "B", this will return "B" as a
 * name, and B's mode as template mode.
 */
${#execInfo.templateName}
${#execInfo.templateMode}

/*
 * Return the name and mode of the 'root' template. This means the template
 * that the template engine was originally asked to process. So if this
 * piece of code is not in the root template "A" but on a fragment being
 * inserted into "A" from another template called "B", this will still 
 * return "A" and A's template mode.
 */
${#execInfo.processedTemplateName}
${#execInfo.processedTemplateMode}

/*
 * Return the stacks (actually, List<String> or List<TemplateMode>) of
 * templates being processed. The first element will be the 
 * 'processedTemplate' (the root one), the last one will be the 'leaf'
 * template, and in the middle all the fragments inserted in nested
 * manner to reach the leaf from the root will appear.
 */
${#execInfo.templateNames}
${#execInfo.templateModes}

/*
 * Return the stack of templates being processed similarly (and in the
 * same order) to 'templateNames' and 'templateModes', but returning
 * a List<TemplateData> with the full template metadata.
 */
${#execInfo.templateStack}

消息

  • #messages:在变量表达式中获取外部化消息的工具方法,就像使用#{...}语法时一样。
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Messages
 * ======================================================================
 */

/*
 * Obtain externalized messages. Can receive a single key, a key plus arguments,
 * or an array/list/set of keys (in which case it will return an array/list/set of 
 * externalized messages).
 * If a message is not found, a default message (like '??msgKey??') is returned.
 */
${#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)}

/*
 * Obtain externalized messages or null. Null is returned instead of a default
 * message if a message for the specified key is not found.
 */
${#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)}

URI/URL

  • #uris:在Thymeleaf标准表达式中执行URI/URL操作(特别是转义/反转义)的工具对象。
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Uris
 * ======================================================================
 */

/*
 * Escape/Unescape as a URI/URL path
 */
${#uris.escapePath(uri)}
${#uris.escapePath(uri, encoding)}
${#uris.unescapePath(uri)}
${#uris.unescapePath(uri, encoding)}

/*
 * Escape/Unescape as a URI/URL path segment (between '/' symbols)
 */
${#uris.escapePathSegment(uri)}
${#uris.escapePathSegment(uri, encoding)}
${#uris.unescapePathSegment(uri)}
${#uris.unescapePathSegment(uri, encoding)}

/*
 * Escape/Unescape as a Fragment Identifier (#frag)
 */
${#uris.escapeFragmentId(uri)}
${#uris.escapeFragmentId(uri, encoding)}
${#uris.unescapeFragmentId(uri)}
${#uris.unescapeFragmentId(uri, encoding)}

/*
 * Escape/Unescape as a Query Parameter (?var=value)
 */
${#uris.escapeQueryParam(uri)}
${#uris.escapeQueryParam(uri, encoding)}
${#uris.unescapeQueryParam(uri)}
${#uris.unescapeQueryParam(uri, encoding)}

类型转换

  • #conversions:允许在模板任意位置执行转换服务conversion service
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Conversions
 * ======================================================================
 */

/*
 * Execute the desired conversion of the 'object' value into the
 * specified class.
 */
${#conversions.convert(object, 'java.util.TimeZone')}
${#conversions.convert(object, targetClass)}

日期

  • #dates:针对java.util.DateString
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Dates
 * ======================================================================
 */

/*
 * Format date with the standard locale format
 * Also works with arrays, lists or sets
 */
${#dates.format(date)}
${#dates.arrayFormat(datesArray)}
${#dates.listFormat(datesList)}
${#dates.setFormat(datesSet)}

/*
 * Format date with the ISO8601 format
 * Also works with arrays, lists or sets
 */
${#dates.formatISO(date)}
${#dates.arrayFormatISO(datesArray)}
${#dates.listFormatISO(datesList)}
${#dates.setFormatISO(datesSet)}

/*
 * Format date with the specified pattern
 * Also works with arrays, lists or sets
 */
${#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')}

/*
 * Obtain date properties
 * Also works with arrays, lists or sets
 */
${#dates.day(date)}                    // also arrayDay(...), listDay(...), etc.
${#dates.month(date)}                  // also arrayMonth(...), listMonth(...), etc.
${#dates.monthName(date)}              // also arrayMonthName(...), listMonthName(...), etc.
${#dates.monthNameShort(date)}         // also arrayMonthNameShort(...), listMonthNameShort(...), etc.
${#dates.year(date)}                   // also arrayYear(...), listYear(...), etc.
${#dates.dayOfWeek(date)}              // also arrayDayOfWeek(...), listDayOfWeek(...), etc.
${#dates.dayOfWeekName(date)}          // also arrayDayOfWeekName(...), listDayOfWeekName(...), etc.
${#dates.dayOfWeekNameShort(date)}     // also arrayDayOfWeekNameShort(...), listDayOfWeekNameShort(...), etc.
${#dates.hour(date)}                   // also arrayHour(...), listHour(...), etc.
${#dates.minute(date)}                 // also arrayMinute(...), listMinute(...), etc.
${#dates.second(date)}                 // also arraySecond(...), listSecond(...), etc.
${#dates.millisecond(date)}            // also arrayMillisecond(...), listMillisecond(...), etc.

/*
 * Create date (java.util.Date) objects from its components
 */
${#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)}

/*
 * Create a date (java.util.Date) object for the current date and time
 */
${#dates.createNow()}

${#dates.createNowForTimeZone()}

/*
 * Create a date (java.util.Date) object for the current date (time set to 00:00)
 */
${#dates.createToday()}

${#dates.createTodayForTimeZone()}

日历

  • #calendars:类似于#dates#datesjava.util.CalendarString
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Calendars
 * ======================================================================
 */

/*
 * Format calendar with the standard locale format
 * Also works with arrays, lists or sets
 */
${#calendars.format(cal)}
${#calendars.arrayFormat(calArray)}
${#calendars.listFormat(calList)}
${#calendars.setFormat(calSet)}

/*
 * Format calendar with the ISO8601 format
 * Also works with arrays, lists or sets
 */
${#calendars.formatISO(cal)}
${#calendars.arrayFormatISO(calArray)}
${#calendars.listFormatISO(calList)}
${#calendars.setFormatISO(calSet)}

/*
 * Format calendar with the specified pattern
 * Also works with arrays, lists or sets
 */
${#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')}

/*
 * Obtain calendar properties
 * Also works with arrays, lists or sets
 */
${#calendars.day(date)}                // also arrayDay(...), listDay(...), etc.
${#calendars.month(date)}              // also arrayMonth(...), listMonth(...), etc.
${#calendars.monthName(date)}          // also arrayMonthName(...), listMonthName(...), etc.
${#calendars.monthNameShort(date)}     // also arrayMonthNameShort(...), listMonthNameShort(...), etc.
${#calendars.year(date)}               // also arrayYear(...), listYear(...), etc.
${#calendars.dayOfWeek(date)}          // also arrayDayOfWeek(...), listDayOfWeek(...), etc.
${#calendars.dayOfWeekName(date)}      // also arrayDayOfWeekName(...), listDayOfWeekName(...), etc.
${#calendars.dayOfWeekNameShort(date)} // also arrayDayOfWeekNameShort(...), listDayOfWeekNameShort(...), etc.
${#calendars.hour(date)}               // also arrayHour(...), listHour(...), etc.
${#calendars.minute(date)}             // also arrayMinute(...), listMinute(...), etc.
${#calendars.second(date)}             // also arraySecond(...), listSecond(...), etc.
${#calendars.millisecond(date)}        // also arrayMillisecond(...), listMillisecond(...), etc.

/*
 * Create calendar (java.util.Calendar) objects from its components
 */
${#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)}

/*
 * Create a calendar (java.util.Calendar) object for the current date and time
 */
${#calendars.createNow()}

${#calendars.createNowForTimeZone()}

/*
 * Create a calendar (java.util.Calendar) object for the current date (time set to 00:00)
 */
${#calendars.createToday()}

${#calendars.createTodayForTimeZone()}

时间(java.time)

  • #temporals:处理JDK8+java.timejava.time
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Temporals
 * ======================================================================
 */

/*
 *
 * Format date with the standard locale format
 * Also works with arrays, lists or sets
 */
${#temporals.format(temporal)}
${#temporals.arrayFormat(temporalsArray)}
${#temporals.listFormat(temporalsList)}
${#temporals.setFormat(temporalsSet)}

/*
 * Format date with the standard format for the provided locale
 * Also works with arrays, lists or sets
 */
${#temporals.format(temporal, locale)}
${#temporals.arrayFormat(temporalsArray, locale)}
${#temporals.listFormat(temporalsList, locale)}
${#temporals.setFormat(temporalsSet, locale)}

/*
 * Format date with the specified pattern
 * SHORT, MEDIUM, LONG and FULL can also be specified to used the default java.time.format.FormatStyle patterns
 * Also works with arrays, lists or sets
 */
${#temporals.format(temporal, 'dd/MMM/yyyy HH:mm')}
${#temporals.format(temporal, 'dd/MMM/yyyy HH:mm', 'Europe/Paris')}
${#temporals.arrayFormat(temporalsArray, 'dd/MMM/yyyy HH:mm')}
${#temporals.listFormat(temporalsList, 'dd/MMM/yyyy HH:mm')}
${#temporals.setFormat(temporalsSet, 'dd/MMM/yyyy HH:mm')}

/*
 * Format date with the specified pattern and locale
 * Also works with arrays, lists or sets
 */
${#temporals.format(temporal, 'dd/MMM/yyyy HH:mm', locale)}
${#temporals.arrayFormat(temporalsArray, 'dd/MMM/yyyy HH:mm', locale)}
${#temporals.listFormat(temporalsList, 'dd/MMM/yyyy HH:mm', locale)}
${#temporals.setFormat(temporalsSet, 'dd/MMM/yyyy HH:mm', locale)}

/*
 * Format date with ISO-8601 format
 * Also works with arrays, lists or sets
 */
${#temporals.formatISO(temporal)}
${#temporals.arrayFormatISO(temporalsArray)}
${#temporals.listFormatISO(temporalsList)}
${#temporals.setFormatISO(temporalsSet)}

/*
 * Obtain date properties
 * Also works with arrays, lists or sets
 */
${#temporals.day(temporal)}                    // also arrayDay(...), listDay(...), etc.
${#temporals.month(temporal)}                  // also arrayMonth(...), listMonth(...), etc.
${#temporals.monthName(temporal)}              // also arrayMonthName(...), listMonthName(...), etc.
${#temporals.monthNameShort(temporal)}         // also arrayMonthNameShort(...), listMonthNameShort(...), etc.
${#temporals.year(temporal)}                   // also arrayYear(...), listYear(...), etc.
${#temporals.dayOfWeek(temporal)}              // also arrayDayOfWeek(...), listDayOfWeek(...), etc.
${#temporals.dayOfWeekName(temporal)}          // also arrayDayOfWeekName(...), listDayOfWeekName(...), etc.
${#temporals.dayOfWeekNameShort(temporal)}     // also arrayDayOfWeekNameShort(...), listDayOfWeekNameShort(...), etc.
${#temporals.hour(temporal)}                   // also arrayHour(...), listHour(...), etc.
${#temporals.minute(temporal)}                 // also arrayMinute(...), listMinute(...), etc.
${#temporals.second(temporal)}                 // also arraySecond(...), listSecond(...), etc.
${#temporals.nanosecond(temporal)}             // also arrayNanosecond(...), listNanosecond(...), etc.

/*
 * Create temporal (java.time.Temporal) objects from its components
 */
${#temporals.create(year,month,day)}                                // return a instance of java.time.LocalDate
${#temporals.create(year,month,day,hour,minute)}                    // return a instance of java.time.LocalDateTime
${#temporals.create(year,month,day,hour,minute,second)}             // return a instance of java.time.LocalDateTime
${#temporals.create(year,month,day,hour,minute,second,nanosecond)}  // return a instance of java.time.LocalDateTime

/*
 * Create a temporal (java.time.Temporal) object for the current date and time
 */
${#temporals.createNow()}                      // return a instance of java.time.LocalDateTime
${#temporals.createNowForTimeZone(zoneId)}     // return a instance of java.time.ZonedDateTime
${#temporals.createToday()}                    // return a instance of java.time.LocalDate
${#temporals.createTodayForTimeZone(zoneId)}   // return a instance of java.time.LocalDate

/*
 * Create a temporal (java.time.Temporal) object for the provided date
 */
${#temporals.createDate(isoDate)}              // return a instance of java.time.LocalDate
${#temporals.createDateTime(isoDate)}          // return a instance of java.time.LocalDateTime
${#temporals.createDate(isoDate, pattern)}     // return a instance of java.time.LocalDate
${#temporals.createDateTime(isoDate, pattern)} // return a instance of java.time.LocalDateTime

数字

  • #numbers:针对数字对象的工具方法:
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Numbers
 * ======================================================================
 */

/*
 * ==========================
 * Formatting integer numbers
 * ==========================
 */

/* 
 * Set minimum integer digits.
 * Also works with arrays, lists or sets
 */
${#numbers.formatInteger(num,3)}
${#numbers.arrayFormatInteger(numArray,3)}
${#numbers.listFormatInteger(numList,3)}
${#numbers.setFormatInteger(numSet,3)}


/* 
 * Set minimum integer digits and thousands separator: 
 * 'POINT', 'COMMA', 'WHITESPACE', 'NONE' or 'DEFAULT' (by locale).
 * Also works with arrays, lists or sets
 */
${#numbers.formatInteger(num,3,'POINT')}
${#numbers.arrayFormatInteger(numArray,3,'POINT')}
${#numbers.listFormatInteger(numList,3,'POINT')}
${#numbers.setFormatInteger(numSet,3,'POINT')}


/*
 * ==========================
 * Formatting decimal numbers
 * ==========================
 */

/*
 * Set minimum integer digits and (exact) decimal digits.
 * Also works with arrays, lists or sets
 */
${#numbers.formatDecimal(num,3,2)}
${#numbers.arrayFormatDecimal(numArray,3,2)}
${#numbers.listFormatDecimal(numList,3,2)}
${#numbers.setFormatDecimal(numSet,3,2)}

/*
 * Set minimum integer digits and (exact) decimal digits, and also decimal separator.
 * Also works with arrays, lists or sets
 */
${#numbers.formatDecimal(num,3,2,'COMMA')}
${#numbers.arrayFormatDecimal(numArray,3,2,'COMMA')}
${#numbers.listFormatDecimal(numList,3,2,'COMMA')}
${#numbers.setFormatDecimal(numSet,3,2,'COMMA')}

/*
 * Set minimum integer digits and (exact) decimal digits, and also thousands and 
 * decimal separator.
 * Also works with arrays, lists or sets
 */
${#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')}


/* 
 * =====================
 * Formatting currencies
 * =====================
 */

${#numbers.formatCurrency(num)}
${#numbers.arrayFormatCurrency(numArray)}
${#numbers.listFormatCurrency(numList)}
${#numbers.setFormatCurrency(numSet)}


/* 
 * ======================
 * Formatting percentages
 * ======================
 */

${#numbers.formatPercent(num)}
${#numbers.arrayFormatPercent(numArray)}
${#numbers.listFormatPercent(numList)}
${#numbers.setFormatPercent(numSet)}

/* 
 * Set minimum integer digits and (exact) decimal digits.
 */
${#numbers.formatPercent(num, 3, 2)}
${#numbers.arrayFormatPercent(numArray, 3, 2)}
${#numbers.listFormatPercent(numList, 3, 2)}
${#numbers.setFormatPercent(numSet, 3, 2)}


/*
 * ===============
 * Utility methods
 * ===============
 */

/*
 * Create a sequence (array) of integer numbers going
 * from x to y
 */
${#numbers.sequence(from,to)}
${#numbers.sequence(from,to,step)}

字符串

  • #strings:针对StringString
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Strings
 * ======================================================================
 */

/*
 * Null-safe toString()
 */
${#strings.toString(obj)}                           // also array*, list* and set*

/*
 * Check whether a String is empty (or null). Performs a trim() operation before check
 * Also works with arrays, lists or sets
 */
${#strings.isEmpty(name)}
${#strings.arrayIsEmpty(nameArr)}
${#strings.listIsEmpty(nameList)}
${#strings.setIsEmpty(nameSet)}

/*
 * Perform an 'isEmpty()' check on a string and return it if false, defaulting to
 * another specified string if true.
 * Also works with arrays, lists or sets
 */
${#strings.defaultString(text,default)}
${#strings.arrayDefaultString(textArr,default)}
${#strings.listDefaultString(textList,default)}
${#strings.setDefaultString(textSet,default)}

/*
 * Check whether a fragment is contained in a String
 * Also works with arrays, lists or sets
 */
${#strings.contains(name,'ez')}                     // also array*, list* and set*
${#strings.containsIgnoreCase(name,'ez')}           // also array*, list* and set*

/*
 * Check whether a String starts or ends with a fragment
 * Also works with arrays, lists or sets
 */
${#strings.startsWith(name,'Don')}                  // also array*, list* and set*
${#strings.endsWith(name,endingFragment)}           // also array*, list* and set*

/*
 * Substring-related operations
 * Also works with arrays, lists or sets
 */
${#strings.indexOf(name,frag)}                      // also array*, list* and set*
${#strings.substring(name,3,5)}                     // also array*, list* and set*
${#strings.substringAfter(name,prefix)}             // also array*, list* and set*
${#strings.substringBefore(name,suffix)}            // also array*, list* and set*
${#strings.replace(name,'las','ler')}               // also array*, list* and set*

/*
 * Append and prepend
 * Also works with arrays, lists or sets
 */
${#strings.prepend(str,prefix)}                     // also array*, list* and set*
${#strings.append(str,suffix)}                      // also array*, list* and set*

/*
 * Change case
 * Also works with arrays, lists or sets
 */
${#strings.toUpperCase(name)}                       // also array*, list* and set*
${#strings.toLowerCase(name)}                       // also array*, list* and set*

/*
 * Split and join
 */
${#strings.arrayJoin(namesArray,',')}
${#strings.listJoin(namesList,',')}
${#strings.setJoin(namesSet,',')}
${#strings.arraySplit(namesStr,',')}                // returns String[]
${#strings.listSplit(namesStr,',')}                 // returns List<String>
${#strings.setSplit(namesStr,',')}                  // returns Set<String>

/*
 * Trim
 * Also works with arrays, lists or sets
 */
${#strings.trim(str)}                               // also array*, list* and set*

/*
 * Compute length
 * Also works with arrays, lists or sets
 */
${#strings.length(str)}                             // also array*, list* and set*

/*
 * Abbreviate text making it have a maximum size of n. If text is bigger, it
 * will be clipped and finished in "..."
 * Also works with arrays, lists or sets
 */
${#strings.abbreviate(str,10)}                      // also array*, list* and set*

/*
 * Convert the first character to upper-case (and vice-versa)
 */
${#strings.capitalize(str)}                         // also array*, list* and set*
${#strings.unCapitalize(str)}                       // also array*, list* and set*

/*
 * Convert the first character of every word to upper-case
 */
${#strings.capitalizeWords(str)}                    // also array*, list* and set*
${#strings.capitalizeWords(str,delimiters)}         // also array*, list* and set*

/*
 * Escape the string
 */
${#strings.escapeXml(str)}                          // also array*, list* and set*
${#strings.escapeJava(str)}                         // also array*, list* and set*
${#strings.escapeJavaScript(str)}                   // also array*, list* and set*
${#strings.unescapeJava(str)}                       // also array*, list* and set*
${#strings.unescapeJavaScript(str)}                 // also array*, list* and set*

/*
 * Null-safe comparison and concatenation
 */
${#strings.equals(first, second)}
${#strings.equalsIgnoreCase(first, second)}
${#strings.concat(values...)}
${#strings.concatReplaceNulls(nullValue, values...)}

/*
 * Random
 */
${#strings.randomAlphanumeric(count)}

对象

  • #objects:通用对象的工具方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Objects
 * ======================================================================
 */

/*
 * Return obj if it is not null, and default otherwise
 * Also works with arrays, lists or sets
 */
${#objects.nullSafe(obj,default)}
${#objects.arrayNullSafe(objArray,default)}
${#objects.listNullSafe(objList,default)}
${#objects.setNullSafe(objSet,default)}

布尔

  • #bools:布尔求值的工具方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Bools
 * ======================================================================
 */

/*
 * Evaluate a condition in the same way that it would be evaluated in a th:if tag
 * (see conditional evaluation chapter afterwards).
 * Also works with arrays, lists or sets
 */
${#bools.isTrue(obj)}
${#bools.arrayIsTrue(objArray)}
${#bools.listIsTrue(objList)}
${#bools.setIsTrue(objSet)}

/*
 * Evaluate with negation
 * Also works with arrays, lists or sets
 */
${#bools.isFalse(cond)}
${#bools.arrayIsFalse(condArray)}
${#bools.listIsFalse(condList)}
${#bools.setIsFalse(condSet)}

/*
 * Evaluate and apply AND operator
 * Receive an array, a list or a set as parameter
 */
${#bools.arrayAnd(condArray)}
${#bools.listAnd(condList)}
${#bools.setAnd(condSet)}

/*
 * Evaluate and apply OR operator
 * Receive an array, a list or a set as parameter
 */
${#bools.arrayOr(condArray)}
${#bools.listOr(condList)}
${#bools.setOr(condSet)}

数组

  • #arrays:针对数组的工具方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Arrays
 * ======================================================================
 */

/*
 * Converts to array, trying to infer array component class.
 * Note that if resulting array is empty, or if the elements
 * of the target object are not all of the same class,
 * this method will return Object[].
 */
${#arrays.toArray(object)}

/*
 * Convert to arrays of the specified component class.
 */
${#arrays.toStringArray(object)}
${#arrays.toIntegerArray(object)}
${#arrays.toLongArray(object)}
${#arrays.toDoubleArray(object)}
${#arrays.toFloatArray(object)}
${#arrays.toBooleanArray(object)}

/*
 * Compute length
 */
${#arrays.length(array)}

/*
 * Check whether array is empty
 */
${#arrays.isEmpty(array)}

/*
 * Check if element or elements are contained in array
 */
${#arrays.contains(array, element)}
${#arrays.containsAll(array, elements)}

列表

  • #lists:针对列表的工具方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Lists
 * ======================================================================
 */

/*
 * Converts to list
 */
${#lists.toList(object)}

/*
 * Compute size
 */
${#lists.size(list)}

/*
 * Check whether list is empty
 */
${#lists.isEmpty(list)}

/*
 * Check if element or elements are contained in list
 */
${#lists.contains(list, element)}
${#lists.containsAll(list, elements)}

/*
 * Sort a copy of the given list. The members of the list must implement
 * comparable or you must define a comparator.
 */
${#lists.sort(list)}
${#lists.sort(list, comparator)}

集合

  • #sets:针对集合的工具方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Sets
 * ======================================================================
 */

/*
 * Converts to set
 */
${#sets.toSet(object)}

/*
 * Compute size
 */
${#sets.size(set)}

/*
 * Check whether set is empty
 */
${#sets.isEmpty(set)}

/*
 * Check if element or elements are contained in set
 */
${#sets.contains(set, element)}
${#sets.containsAll(set, elements)}

映射

  • #maps:针对映射的工具方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Maps
 * ======================================================================
 */

/*
 * Compute size
 */
${#maps.size(map)}

/*
 * Check whether map is empty
 */
${#maps.isEmpty(map)}

/*
 * Check if key/s or value/s are contained in maps
 */
${#maps.containsKey(map, key)}
${#maps.containsAllKeys(map, keys)}
${#maps.containsValue(map, value)}
${#maps.containsAllValues(map, value)}

聚合

  • #aggregates:用于创建数组或集合上的聚合的工具方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Aggregates
 * ======================================================================
 */

/*
 * Compute sum. Returns null if array or collection is empty
 */
${#aggregates.sum(array)}
${#aggregates.sum(collection)}

/*
 * Compute average. Returns null if array or collection is empty
 */
${#aggregates.avg(array)}
${#aggregates.avg(collection)}

ID

  • #ids:用于处理可能重复的idid
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Ids
 * ======================================================================
 */

/*
 * Normally used in th:id attributes, for appending a counter to the id attribute value
 * so that it remains unique even when involved in an iteration process.
 */
${#ids.seq('someId')}

/*
 * Normally used in th:for attributes in <label> tags, so that these labels can refer to Ids
 * generated by means if the #ids.seq(...) function.
 *
 * Depending on whether the <label> goes before or after the element with the #ids.seq(...)
 * function, the "next" (label goes before "seq") or the "prev" function (label goes after 
 * "seq") function should be called.
 */
${#ids.next('someId')}
${#ids.prev('someId')}

属性的工具方法(例如,由于迭代导致的结果)。

Thymeleaf的标记选择器直接来自于Thymeleaf的解析库:AttoParser.

这些选择器的语法与XPath、CSS和jQuery中的选择器非常相似,这使得大多数用户能够轻松使用它们。你可以在AttoParser文档.

基本语法包括:<div>例如,以下选择器会选择标记中每个位置具有类content的所有

<div th:insert="~{mytemplate :: //div[@class='content']}">...</div>

The basic syntax includes:

  • /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的元素,reference是一种th:refth:fragment属性)。

  • 只要包含参数的定义,选择器也可以不带元素名/引用。因此,[@class='oneclass']是一个有效的选择器,查找任何类属性值为"oneclass".

的元素(标签)。

  • 除了=(等于),其他比较运算符也有效:!=(不等于),^=(以...开头)和$=(以...结尾)。例如:x[@class^='section']表示名称为x并且属性class的值以section.

  • 属性既可以以@开头指定(XPath 风格),也可以不加(jQuery 风格)。因此,x[z='v']th:each="err : ${#fields.allErrors()}"x[@z='v'].

  • 多个属性修饰符既可以用and连接起来使用(XPath 风格),也可以通过链式调用多个修饰符来实现(jQuery 风格)。所以,x[@z1='v1' and @z2='v2']实际上等价于x[@z1='v1'][@z2='v2'](也等价于x[z1='v1'][z2='v2'])。

直接类似 jQuery 的选择器:

  • x.oneclassth:each="err : ${#fields.allErrors()}"x[class='oneclass'].

  • .oneclassth:each="err : ${#fields.allErrors()}"[class='oneclass'].

  • x#oneidth:each="err : ${#fields.allErrors()}"x[id='oneid'].

  • #oneidth:each="err : ${#fields.allErrors()}"[id='oneid'].

  • x%oneref表示<x>具有th:ref="oneref"th:fragment="oneref"属性

  • %oneref的任何具有th:ref="oneref"th:fragment="oneref"属性的标签。请注意,这实际上等同于直接使用oneref因为可以使用引用代替元素名称。

  • 直接选择器和属性选择器可以混合使用:a.external[@href^='https'].

因此上面的 Markup Selector 表达式:

<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 匹配

Markup Selectors 认为 class 属性是多值类型,因此即使元素有多个 class 值,也允许对该属性应用选择器。

例如,div.two将匹配<div class="one two three" />

无噪 Logo
无噪文档
中文文档 · 复刻官网
查看所有 ↗