Bootstrap

Thymeleaf 中文文档----全译版

文章目录

1 引入Thymeleaf

1.1 Thymeleaf是什么?

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

Thymeleaf的主要目标是提供一种优雅的和高度可维护的方式来创建模板。为了实现这一点,它构建在自然模板的概念上,以不影响模板作为设计原型使用的方式将其逻辑注入模板文件。这改进了设计的交流,并在设计和开发团队之间架起了桥梁。

Thymeleaf的设计从一开始就考虑了Web标准——尤其是HTML5——允许你创建完全验证模板,如果你需要的话。

1.2 Thymeleaf可以处理什么样的模板?

开箱即用,Thymeleaf允许您处理六种模板,其中每一种被称为模板模式:

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

有两种标记模板模式(HTML和XML)、三种文本模板模式(文本、JAVASCRIPT和CSS)和一种无操作模板模式(RAW)。

HTML模板模式将允许任何类型的HTML输入,包括HTML5、HTML 4和XHTML。将不执行任何验证或格式良好性检查,并且将在输出中尽可能尊重模板代码/结构。

XML模板模式将允许XML输入。在这种情况下,代码应该是格式良好的—没有未关闭的标记,没有未引用的属性,等等—如果发现格式良好性违规,解析器将抛出异常。注意,将不执行任何验证(针对DTD或XML模式)。

文本模板模式将允许对非标记性质的模板使用特殊语法。此类模板的示例可能是文本电子邮件或模板化文档。注意,HTML或XML模板也可以作为文本处理,在这种情况下,它们不会被解析为标记,而每个标记、DOCTYPE、注释等都将被视为纯文本。

JAVASCRIPT模板模式将允许在Thymeleaf应用程序中处理JAVASCRIPT文件。这意味着能够像在HTML文件中一样在JavaScript文件中使用模型数据,但是要使用特定于JavaScript的集成,比如专门的转义或自然脚本。JAVASCRIPT模板模式被认为是文本模式,因此使用与文本模板模式相同的特殊语法。

CSS模板模式将允许处理Thymeleaf应用程序中涉及的CSS文件。与JAVASCRIPT模式类似,CSS模板模式也是一种文本模式,并使用来自文本模板模式的特殊处理语法。

原始模板模式根本不会处理模板。它用于将未触及的资源(文件、URL响应等)插入正在处理的模板中。例如,可以将HTML格式的外部非控制资源包含到应用程序模板中,但要确保这些资源可能包含的任何Thymeleaf代码都不会被执行。

1.3 方言:标准方言

Thymeleaf是一个非常可扩展的模板引擎(事实上它可以被称为模板引擎框架),它允许你定义和自定义的方式,你的模板将被处理到一个精细的细节级别。

将一些逻辑应用到标记工件(标记、一些文本、注释,如果模板不是标记,则仅仅是占位符)的对象称为处理程序,这些处理程序的集合—加上一些额外的工件—通常是方言的组成部分。Thymeleaf的核心库提供了一种称为标准方言的方言,这对大多数用户来说应该足够了。

注意,方言实际上可能没有处理器,并且完全由其他类型的工件组成,但是处理器绝对是最常见的用例。

本教程介绍标准方言。在下面的页面中,您将了解到的每个属性和语法特性都是由这种方言定义的,即使没有明确提到。

当然,如果希望在利用库的高级特性的同时定义自己的处理逻辑,用户可以创建自己的方言(甚至扩展标准的方言)。Thymeleaf也可以配置成同时使用几种方言。

官方thymeleaf-spring3和thymeleaf-spring4集成包都定义一个方言称为“SpringStandard方言”,大部分是一样的标准方言,但小适应更好地利用Spring框架的一些特性(例如,通过使用Spring表达式语言或图像代替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 GitHub repository.

2.1 一个杂货店的网站

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

这个应用程序是一个虚拟杂货店的web站点,它将为我们提供许多场景来展示Thymeleaf的许多特性。

首先,我们的应用程序需要一组简单的模型实体:通过创建订单向客户销售的产品。我们还将管理这些产品的评论:

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

public class ProductService {
 
    ...
 
    public List<Product> findAll() {
        return ProductRepository.getInstance().findAll();
    }
 
    public Product findById(Integer id) {
        return ProductRepository.getInstance().findById(id);
    }
    
}

在web层,我们的应用程序将有一个过滤器,根据请求URL将执行委托给thymeleaf启用的命令:

private boolean process(HttpServletRequest request, HttpServletResponse response)
        throws ServletException {
    
    try {
 
        // This prevents triggering engine executions for resource URLs
        if (request.getRequestURI().startsWith("/css") ||
                request.getRequestURI().startsWith("/images") ||
                request.getRequestURI().startsWith("/favicon")) {
            return false;
        }
 
        
        /*
         * Query controller/URL mapping and obtain the controller
         * that will process the request. If no controller is available,
         * return false and let other filters/servlets process the request.
         */
        IGTVGController controller = this.application.resolveControllerForRequest(request);
        if (controller == null) {
            return false;
        }
 
        /*
         * Obtain the TemplateEngine instance.
         */
        ITemplateEngine templateEngine = this.application.getTemplateEngine();
 
        /*
         * Write the response headers
         */
        response.setContentType("text/html;charset=UTF-8");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
 
        /*
         * Execute the controller and process view template,
         * writing the results to the response writer. 
         */
        controller.process(
                request, response, this.servletContext, templateEngine);
        
        return true;
        
    } catch (Exception e) {
        try {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final IOException ignored) {
            // Just ignore this
        }
        throw new ServletException(e);
    }
    
}

这是我们的IGTVGController接口:

public interface IGTVGController {
 
    public void process(
            HttpServletRequest request, HttpServletResponse response,
            ServletContext servletContext, ITemplateEngine templateEngine);    
    
}

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

最后,它看起来是这样的:
示例应用程序主页

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

2.2 创建和配置模板引擎

我们的过滤器中的process(…)方法包含这一行:

ITemplateEngine templateEngine = this.application.getTemplateEngine();

这意味着GTVGApplication类负责创建和配置Thymeleaf应用程序中最重要的对象之一:TemplateEngine实例(ITemplateEngine接口的实现)。

我们的org.thymeleaf.TemplateEngine对象初始化如下:

public class GTVGApplication {
  
    
    ...
    private final TemplateEngine templateEngine;
    ...
    
    
    public GTVGApplication(final ServletContext servletContext) {
 
        super();
 
        ServletContextTemplateResolver templateResolver = 
                new ServletContextTemplateResolver(servletContext);
        
        // HTML is the default mode, but we set it anyway for better understanding of code
        templateResolver.setTemplateMode(TemplateMode.HTML);
        // This will convert "home" to "/WEB-INF/templates/home.html"
        templateResolver.setPrefix("/WEB-INF/templates/");
        templateResolver.setSuffix(".html");
        // Template cache TTL=1h. If not set, entries would be cached until expelled
        templateResolver.setCacheTTLMs(Long.valueOf(3600000L));
        
        // Cache is set to true by default. Set to false if you want templates to
        // be automatically updated when modified.
        templateResolver.setCacheable(true);
        
        this.templateEngine = new TemplateEngine();
        this.templateEngine.setTemplateResolver(templateResolver);
        
        ...
 
    }
 
}

配置TemplateEngine对象的方法有很多,但是现在,这几行代码就足以告诉我们所需的步骤。

模板解析器

让我们从模板解析器开始:

ServletContextTemplateResolver templateResolver = 
        new ServletContextTemplateResolver(servletContext);

模板解析器是实现来自调用的Thymeleaf API的接口的对象

org.thymeleaf.templateresolver.ITemplateResolver

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

这些对象负责决定如何访问模板,在这个GTVG应用程序中,org.thymeleaf.templateresolver.ServletContextTemplateResolver 意味着我们要从Servlet上下文中获取模板文件作为资源:应用程序范围的javax.servlet.ServletContext对象存在于每个Java web应用程序中,它从web应用程序根目录解析资源。

但这还不是我们对模板解析器所能说的全部,因为我们可以在它上面设置一些配置参数。

一、模板模式:

templateResolver.setTemplateMode(TemplateMode.HTML);

HTML是ServletContextTemplateResolver的默认模板模式,但是最好还是建立它,以便我们的代码清楚地记录正在发生的事情。

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.ITemplateEngine,我们创建了一个实例,它在这里:

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

它在模板处理中没有任何影响,但是作为一个incantation,这就避免了我们的IDE抱怨th:*这些属性缺少命名空间定义。

那么,如果我们想让这个模板html5有效呢?简单:切换到Thymeleaf的数据属性语法,使用数据前缀的属性名称和连字符(-)分隔符,而不是分号(😃:

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

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

这两种表示法完全等价并且可以互换,但是为了代码示例的简单性和紧凑性,本教程将使用名称空间表示法(th:*)。此外,th:*符号更通用,在每个Thymeleaf模板模式(XML,文本…),而数据符号只允许在HTML模式。

使用th:text和 externalizing text

外部化文本是从模板文件中提取模板代码片段,以便将它们保存在单独的文件中(通常是.properties文件),并且可以用其他语言编写的等价文本轻松替换它们(这个过程称为国际化,简称i18n)。外部化的文本片段通常称为“消息”。

消息总是有一个标识它们的键,而Thymeleaf允许您指定文本应该与#{…}语法:

<p th:text="#{home.welcome}">Welcome to our grocery store!</p>

我们在这里看到的实际上是Thymeleaf标准方言的两个不同的特点:

  • th:text属性,它计算其值表达式并将结果设置为主机标记的主体,有效地替换了“欢迎光临我们的杂货店!”“我们在代码中看到的文本。
  • #{home.welcome}表达式,在标准表达式语法中指定,指示th:text属性使用的文本应该是带有home.welcome的消息。对应于我们正在处理模板的任何语言环境的键。

现在,这个外化的文本在哪里?

Thymeleaf中外部化文本的位置是完全可配置的,并且将取决于org.thymeleaf.messageresolver.IMessageResolver所使用的特定实现。通常,.properties将使用基于文件的实现,但是例如,如果我们想从数据库中获取消息,则可以创建自己的实现。

但是,我们尚未在初始化期间为模板引擎指定消息解析器,这意味着我们的应用程序使用的是由实现的 标准消息解析器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控制器。

语境(上下文)

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

public class HomeController implements IGTVGController {
 
    public void process(
            final HttpServletRequest request, final HttpServletResponse response,
            final ServletContext servletContext, final ITemplateEngine templateEngine)
            throws Exception {
        
        WebContext ctx = 
                new WebContext(request, response, servletContext, request.getLocale());
        
        templateEngine.process("home", ctx, response.getWriter());
        
    }
 
}

我们首先看到的是上下文的创建。Thymeleaf上下文是实现org.thymeleaf.context.IContext接口的对象。上下文应在变量映射中包含执行模板引擎所需的所有数据,并且还应引用必须用于外部化消息的语言环境。

public interface IContext {
 
    public Locale getLocale();
    public boolean containsVariable(final String name);
    public Set<String> getVariableNames();
    public Object getVariable(final String name);
    
}

该接口有一个专门的扩展,org.thymeleaf.context.IWebContext可以在基于ServletAPI的Web应用程序(如SpringMVC)中使用。

public interface IWebContext extends IContext {
    
    public HttpServletRequest getRequest();
    public HttpServletResponse getResponse();
    public HttpSession getSession();
    public ServletContext getServletContext();
    
}

Thymeleaf核心库提供了以下每个接口的实现:

  • org.thymeleaf.context.Context 实施 IContext
  • org.thymeleaf.context.WebContext 实施 IWebContext

正如您在控制器代码中看到的那样,这WebContext是我们使用的代码。实际上,我们必须这样做,因为使用a ServletContextTemplateResolver要求我们使用上下文实现IWebContext

WebContext ctx = new WebContext(request, response, servletContext, request.getLocale());

这四个构造函数参数中只有三个是必需的,因为如果未指定默认语言环境,则将使用系统的默认语言环境(尽管您绝对不应在实际应用程序中让这种情况发生)。

我们可以使用一些专门的表达式从WebContext模板中的获取请求参数以及请求,会话和应用程序属性。例如:

  • ${x}将返回x存储在Thymeleaf上下文中或作为请求属性的变量。
  • ${param.x}将返回一个名为(可能是多值)的请求参数x。
  • ${session.x}将返回名为的会话属性x。
  • ${application.x}将返回名为的Servlet上下文属性x。

执行模板引擎

准备好上下文对象之后,现在我们可以告诉模板引擎使用上下文处理模板(按其名称),并将其传递给响应编写器,以便可以将响应写入其中:

templateEngine.process("home", ctx, response.getWriter());

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

<!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 HttpServletRequest request, final HttpServletResponse response,
            final ServletContext servletContext, final ITemplateEngine templateEngine)
            throws Exception {
        
    SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy");
    Calendar cal = Calendar.getInstance();
        
    WebContext ctx = 
            new WebContext(request, response, servletContext, request.getLocale());
    ctx.setVariable("today", dateFormat.format(cal.getTime()));
        
    templateEngine.process("home", ctx, response.getWriter());
        
}

我们在上下文中添加了一个String类型的变量today,现在我们可以在模板中显示它:

<body>
 
  <p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
 
  <p>Today is: <span th:text="${today}">13 February 2011</span></p>
  
</body>

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

${today}表达式仅表示“获取今天调用的变量”,但是这些表达式可能更复杂(例如${user.name}“获取称为用户的变量并调用其getName()方法”)。

属性值有很多可能性:消息,变量表达式等等。下一章将向我们展示所有这些可能性。

4 标准表达式语法

我们将在杂货店虚拟商店的开发中稍作休息,以了解Thymeleaf标准方言最重要的部分之一:Thymeleaf Standard Expression语法。

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

<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.*包中类指定的格式格式化数字和日期。

为了给我们的参数指定一个值,并给定一个HTTP会话属性user,我们可以拥有:

<p th:utext="#{home.welcome(${session.user.name})}">
  Welcome to our grocery store, Sebastian Pepper!
</p>

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

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

消息密钥本身可以来自变量:

<p th:utext="#{${welcomeMsgKey}(${session.user.name})}">
  Welcome to our grocery store, Sebastian Pepper!
</p>

4.2 变量

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

有关OGNL语法和功能的详细信息,请阅读 《OGNL语言指南》
在启用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:上下文语言环境。
  • #request:(仅在Web上下文中)HttpServletRequest对象。
  • #response:(仅在Web上下文中)HttpServletResponse对象。
  • #session:(仅在Web上下文中)HttpSession对象。
  • #servletContext:(仅在Web上下文中)ServletContext对象。

因此,我们可以这样做:

Established locale country: <span th:text="${#locale.country}">US</span>.

您可以在附录A中阅读这些对象的完整参考。

表达工具对象

除了这些基本对象之外,Thymeleaf将为我们提供一组实用程序对象,这些对象将帮助我们在表达式中执行常见任务。

  • #execInfo:有关正在处理的模板的信息。
  • #messages:用于获取变量表达式内的外部化消息的方法,与使用#{…}语法获得消息的方法相同。
  • #uris:用于转义部分URL / URI的方法
  • #conversions:用于执行已配置的转换服务(如果有)的方法。
  • #datesjava.util.Date对象的方法:格式化,组件提取等。
  • #calendars:类似于#dates,但用于java.util.Calendar对象。
  • #numbers:格式化数字对象的方法。
  • #strings:String对象的方法:包含,startsWith,前置/追加等。
  • #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(request, servletContext, request.getLocale());
ctx.setVariable("today", dateFormat.format(cal.getTime()));
 
templateEngine.process("home", ctx, response.getWriter());

…我们可以做到这一点:

WebContext ctx = 
    new WebContext(request, response, servletContext, request.getLocale());
ctx.setVariable("today", Calendar.getInstance());
 
templateEngine.process("home", ctx, response.getWriter());

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

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

4.3 选择表达式(星号语法)

变量表达式不仅可以写成${...},而且还可以写成*{...}

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

什么是选定对象?使用该th:object属性的表达式的结果。让我们在用户个人资料(userprofile.html)页面中使用一个:

  <div th:object="${session.user}">
    <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
    <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
    <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
  </div>

完全等同于:

<div>
  <p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div>

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

<div th:object="${session.user}">
  <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

选择对象后,选定的对象也可以作为#object表达式变量用于美元表达式:

<div th:object="${session.user}">
  <p>Name: <span th:text="${#object.firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

如前所述,如果未执行任何对象选择,则美元和星号语法是等效的。

<div>
  <p>Name: <span th:text="*{session.user.name}">Sebastian</span>.</p>
  <p>Surname: <span th:text="*{session.user.surname}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{session.user.nationality}">Saturn</span>.</p>
</div>

4.4 链接网址

由于它们的重要性,URL是Web应用程序模板中的一等公民,而Thymeleaf Standard Dialect对它们有一种特殊的语法,该@语法为:@{...}

URL有不同类型:

  • 绝对网址: http://www.thymeleaf.org
  • 相对URL,可以是:
    • 相对页面: user/login.html
    • 上下文相关:(/itemdetails?id=3服务器中的上下文名称将自动添加)
    • 相对于服务器:(~/billing/processInvoice允许在同一服务器中的另一个上下文(=应用程序)中调用URL。
    • 相对协议网址: //code.jquery.com/jquery-2.0.3.min.js

这些表达式的实际处理以及它们到将要输出的URL的转换是通过org.thymeleaf.linkbuilder.ILinkBuilder注册到ITemplateEngine所使用的对象的接口实现来完成的。

默认情况下,该类的该接口的单个实现被注册org.thymeleaf.linkbuilder.StandardLinkBuilder,这对于脱机(非Web)和基于Servlet API的Web场景都足够。其他场景(例如与非ServletAPI Web框架集成)可能需要链接构建器接口的特定实现。

让我们使用这种新语法。符合th:href属性:

<!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) -->
<a href="details.html" 
   th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>
 
<!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) -->
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
 
<!-- Will produce '/gtvg/order/3/details' (plus rewriting) -->
<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>

这里要注意一些事情:

  • th:href是修饰符属性:处理后,它将计算要使用的链接URL,并将该值设置href为<a>标记的属性。
  • 我们被允许对URL参数使用表达式(如您在中所见orderId=${o.id})。所需的URL参数编码操作也将自动执行。
  • 如果需要几个参数,这些参数将以逗号分隔:@{/order/process(execId=${execId},execType='FAST')}
  • URL路径中也允许使用变量模板: @{/order/{orderId}/details(orderId=${orderId})}
  • 以/(例如:)开头的相对URL /order/details将自动以应用程序上下文名称作为前缀。
  • 如果未启用Cookie或尚不知道,则";jsessionid=…"可能会将后缀添加到相对URL中,以便保留会话。这称为URL重写,Thymeleaf允许您通过使用response.encodeURL(...)Servlet API中的机制为每个URL 插入自己的重写过滤器。
  • th:href属性允许我们(可选)href在模板中具有有效的静态属性,以便当直接打开原型进行原型设计时,模板链接仍可被浏览器导航。

与消息语法(#{...})一样,URL基也可以是求值另一个表达式的结果:

<a th:href="@{${url}(orderId=${o.id})}">view</a>
<a th:href="@{'/details/'+${user.login}(orderId=${o.id})}">view</a>

主页的菜单

现在,我们知道了如何创建链接URL,如何在主页中为站点中的其他页面添加一个小菜单?

<p>Please select an option</p>
<ol>
  <li><a href="product/list.html" th:href="@{/product/list}">Product List</a></li>
  <li><a href="order/list.html" th:href="@{/order/list}">Order List</a></li>
  <li><a href="subscribe.html" th:href="@{/subscribe}">Subscribe to our Newsletter</a></li>
  <li><a href="userprofile.html" th:href="@{/userprofile}">See User Profile</a></li>
</ol>

服务器根目录相对URL

可以使用其他语法来创建相对于服务器根目录的URL(而不是上下文根目录的URL),以便链接到同一服务器中的不同上下文。这些网址的指定方式如下@{~/path/to/something}

4.5 碎片

片段表达式是表示标记片段并将其在模板中移动的简便方法。这使我们能够复制它们,并将它们作为参数传递给其他模板,依此类推。

最常见的用途是使用th: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 y是写在花括号外的,因此Thymeleaf负责处理。如果将其写在花括号内,则OGNL / SpringEL引擎应负责:

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

空文本

该null文本也可用于:

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

文字代币

实际上,数字,布尔值和null文字是文字标记的一种特殊情况。

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

好的部分?令牌不需要任何引号引起来。因此,我们可以这样做:

<div th:class="content">...</div>

代替:

<div th:class="'content'">...</div>

4.7 附加文本

文本,无论它们是文字还是评估变量或消息表达式的结果,都可以使用+运算符轻松附加:

<span th:text="'The name of the user is ' + ${user.name}">

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规定,不得在属性值中使用<和>符号,因此应将其替换为<>

<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,then和else)本身的表达式,这意味着它们可以是变量(${...},*{...})消息(#{...})网址(@{...})文字('...')

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

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

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

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

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

一个默认的表情是一种特殊的条件值的没有那么一部分。它等效于某些语言(如Groovy)中出现的Elvis运算符,可让您指定两个表达式:如果第一个表达式的计算结果不为null,则使用第一个表达式;如果第二个表达式使用,则使用第二个表达式。

让我们在我们的用户个人资料页面中看到它的实际效果:

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

如您所见,运算符为?:,并且仅当求*{age}值结果为null时,才在此处使用它来指定名称的默认值(在这种情况下为文字值)。因此,这等效于:

<p>Age: <span th:text="*{age != null}? *{age} : '(no age specified)'">27</span>.</p>

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

<p>
  Name: 
  <span th:text="*{firstName}?: (*{admin}? 'Admin' : #{default.username})">Sebastian</span>
</p>

4.13 无操作令牌

No-Operation令牌由下划线符号(_)表示。

该标记背后的想法是指定表达式的期望结果什么也不做,即完全就像可处理属性(例如th:text)根本不存在一样。

除其他可能性外,这还使开发人员可以将原型文本用作默认值。例如,代替:

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

…我们可以直接将“未经用户身份验证”用作原型文本,从设计的角度来看,这使得代码既简洁又通用:

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

4.14 数据转换/格式化

Thymeleaf 为变量()和选择()表达式定义了双括号语法,使我们能够通过配置的转换服务来应用数据转换。${...} *{...}

它基本上是这样的:

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

注意到那里有双括号了吗?:${{...}}。指示Thymeleaf将user.lastAccessDate表达式的结果传递给转换服务,并要求它执行格式操作(将转换为String),然后再写入结果。

假设user.lastAccessDate类型为java.util.Calendar,如果已注册转换服务(的实现IStandardConversionService)并且包含的有效转换Calendar -> String,则将应用。

IStandardConversionService(StandardConversionService该类)的默认实现仅对.toString()转换为的任何对象执行String。有关如何注册自定义转换服务实现的更多信息,请参见 更多关于配置部分。

官方的thymeleaf-spring3和thymeleaf-spring4集成软件包将Thymeleaf的转换服务机制与Spring自己的转换服务基础结构透明地集成在一起,以便在Spring配置中声明的转换服务和格式化程序将自动提供给${{...}}*{{...}}表达式。

4.15 预处理

除了用于表达式处理的所有这些功能之外,Thymeleaf还具有预处理表达式的功能。

预处理是在普通表达式之前执行的表达式的执行,该表达式允许修改最终将要执行的表达式。

预处理表达式与普通表达式完全一样,但是出现了一个双下划线符号(如__${expression}__)。

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

[email protected]@translateToFrench({0})

Messages_es.properties equivalent

[email protected]@translateToSpanish({0})

我们可以创建一个标记片段,该片段根据语言环境评估一个表达式或另一个表达式。为此,我们将首先选择表达式(通过预处理),然后让Thymeleaf执行它:

<p th:text="${__#{article.text('textVar')}__}">Some text here...</p>

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

<p th:text="${@myapp.translator.Translator@translateToFrench(textVar)}">Some text here...</p>

可以使用来在属性中对预处理字符串进行转义

5 设置属性值

本章将说明在标记中设置(或修改)属性值的方法。

5.1 设置任何属性的值

假设我们的网站发布了新闻通讯,我们希望用户能够订阅该新闻通讯,所以我们创建/WEB-INF/templates/subscribe.html具有以下形式的模板:

<form action="subscribe.html">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" />
  </fieldset>
</form>

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

然后输入th:attr属性及其更改设置的标签属性值的能力:

<form action="subscribe.html" th:attr="action=@{/subscribe}">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
  </fieldset>
</form>

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

<form action="/gtvg/subscribe">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="¡Suscríbe!"/>
  </fieldset>
</form>

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

但是,如果我们想一次设置多个属性怎么办?XML规则不允许您在标记中设置属性两次,因此th:attr将采用逗号分隔的分配列表,例如:

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

给定所需的消息文件,将输出:

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

5.2 将值设置为特定属性

到现在为止,您可能会认为类似:

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

…是相当难看的标记。在属性值内指定分配可能非常实用,但如果必须始终这样做,则不是创建模板的最优雅的方法。

Thymeleaf同意您的观点,这就是为什么th:attr模板中很少使用它的原因。通常,您将使用其他th:*属性,这些属性的任务是设置特定的标记属性(而不仅仅是像的任何属性th:attr)。

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

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

看起来好多了!让我们尝试action对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:abbr	th:accept	th:accept-charset
th:accesskey	th:action	th:align
th:alt	th:archive	th:audio
th:autocomplete	th:axis	th:background
th:bgcolor	th:border	th:cellpadding
th:cellspacing	th:challenge	th:charset
th:cite	th:class	th:classid
th:codebase	th:codetype	th:cols
th:colspan	th:compact	th:content
th:contenteditable	th:contextmenu	th:data
th:datetime	th:dir	th:draggable
th:dropzone	th:enctype	th:for
th:form	th:formaction	th:formenctype
th:formmethod	th:formtarget	th:fragment
th:frame	th:frameborder	th:headers
th:height	th:high	th:href
th:hreflang	th:hspace	th:http-equiv
th:icon	th:id	th:inline
th:keytype	th:kind	th:label
th:lang	th:list	th:longdesc
th:low	th:manifest	th:marginheight
th:marginwidth	th:max	th:maxlength
th:media	th:method	th:min
th:name	th:onabort	th:onafterprint
th:onbeforeprint	th:onbeforeunload	th:onblur
th:oncanplay	th:oncanplaythrough	th:onchange
th:onclick	th:oncontextmenu	th:ondblclick
th:ondrag	th:ondragend	th:ondragenter
th:ondragleave	th:ondragover	th:ondragstart
th:ondrop	th:ondurationchange	th:onemptied
th:onended	th:onerror	th:onfocus
th:onformchange	th:onforminput	th:onhashchange
th:oninput	th:oninvalid	th:onkeydown
th:onkeypress	th:onkeyup	th:onload
th:onloadeddata	th:onloadedmetadata	th:onloadstart
th:onmessage	th:onmousedown	th:onmousemove
th:onmouseout	th:onmouseover	th:onmouseup
th:onmousewheel	th:onoffline	th:ononline
th:onpause	th:onplay	th:onplaying
th:onpopstate	th:onprogress	th:onratechange
th:onreadystatechange	th:onredo	th:onreset
th:onresize	th:onscroll	th:onseeked
th:onseeking	th:onselect	th:onshow
th:onstalled	th:onstorage	th:onsubmit
th:onsuspend	th:ontimeupdate	th:onundo
th:onunload	th:onvolumechange	th:onwaiting
th:optimum	th:pattern	th:placeholder
th:poster	th:preload	th:radiogroup
th:rel	th:rev	th:rows
th:rowspan	th:rules	th:sandbox
th:scheme	th:scope	th:scrolling
th:size	th:sizes	th:span
th:spellcheck	th:src	th:srclang
th:standby	th:start	th:step
th:style	th:summary	th:tabindex
th:target	th:title	th:type
th:usemap	th:value	th:valuetype
th:vspace	th:width	th:wrap
th:xmlbase	th:xmllang	th: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" />

标准方言中还有两个特定的附加属性:th:classappendth:styleappend属性,用于在元素上添加CSS类或样式片段而不覆盖现有属性:

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

(不必担心该th:each属性。它是一个迭代属性,我们将在后面讨论。)

5.5 固定值布尔属性

HTML具有布尔属性的概念,即没有值的属性,并且首字母缩写为1表示值是“ true”。在XHTML中,这些属性仅取1值,这就是它本身。

例如checked

<input type="checkbox" name="option2" checked /> <!-- HTML -->
<input type="checkbox" name="option1" checked="checked" /> <!-- XHTML -->

标准方言包括一些属性,这些属性使您可以通过评估条件来设置这些属性,因此,如果评估为true,则该属性将设置为其固定值;如果评估为false,则将不设置该属性:

<input type="checkbox" name="active" th:checked="${user.active}" />

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

 
th:async	th:autofocus	th:autoplay
th:checked	th:controls	th:declare
th:default	th:defer	th:disabled
th:formnovalidate	th:hidden	th:ismap
th:loop	th:multiple	th:novalidate
th:nowrap	th:open	th:pubdate
th:readonly	th:required	th:reversed
th:scoped	th:seamless	th:selected

5.6 设置任何属性的值(默认属性处理器)

Thymeleaf提供了一个默认的属性处理器,即使我们在标准方言中没有为其定义任何特定的处理器,它也允许我们设置任何属性的值th:*

所以像这样:

<span th:whatever="${user.name}">...</span>

将导致:

<span whatever="John Apricot">...</span>

5.7 支持HTML5友好的属性和元素名称

也可以使用完全不同的语法,以更友好的HTML5方式将处理器应用于模板。

<table>
    <tr data-th-each="user : ${users}">
        <td data-th-text="${user.login}">...</td>
        <td data-th-text="${user.name}">...</td>
    </tr>
</table>

data-{prefix}-{name}语法是标准的方式在HTML5写自定义属性,而无需开发人员使用任何命名空间的名称,如th:*。Thymeleaf使此语法自动适用于所有方言(不仅限于标准方言)。

还有一种用于指定自定义标记的语法:{prefix}-{name},它遵循W3C自定义元素规范(是更大的W3C Web组件规范的一部分)。例如,这可以用于th:block元素(或th-block),这将在后面的部分中进行说明。

重要提示:此语法是命名空间语法的补充th:*,它不会替代它。完全没有打算将来弃用命名空间的语法。

6 迭代

到目前为止,我们已经创建了一个主页,一个用户个人资料页面以及一个允许用户订阅我们的新闻通讯的页面……但是我们的产品呢?为此,我们将需要一种方法来遍历集合中的项目以构建我们的产品页面。

6.1 迭代基础

为了在我们的/WEB-INF/templates/product/list.html页面中显示产品,我们将使用一个表格。我们的每种产品都将显示在一行(一个<tr>元素)中,因此对于我们的模板,我们需要创建一个模板行 -一个示例行,以举例说明我们希望每种产品的显示方式,然后指示Thymeleaf重复该行,每个产品一次。

标准方言为我们提供了一个确切的属性:th:each

每个使用

对于我们的产品列表页面,我们将需要一个控制器方法,该方法将从服务层检索产品列表并将其添加到模板上下文中:

public void process(
        final HttpServletRequest request, final HttpServletResponse response,
        final ServletContext servletContext, final ITemplateEngine templateEngine)
        throws Exception {
    
    ProductService productService = new ProductService();
    List<Product> allProducts = productService.findAll(); 
    
    WebContext ctx = new WebContext(request, response, servletContext, request.getLocale());
    ctx.setVariable("prods", allProducts);
    
    templateEngine.process("product/list", ctx, response.getWriter());
    
}

然后,我们将th:each在模板中使用它来遍历产品列表:

<!DOCTYPE html>
 
<html xmlns:th="http://www.thymeleaf.org">
 
  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
  </head>
 
  <body>
 
    <h1>Product list</h1>
  
    <table>
      <tr>
        <th>NAME</th>
        <th>PRICE</th>
        <th>IN STOCK</th>
      </tr>
      <tr th:each="prod : ${prods}">
        <td th:text="${prod.name}">Onions</td>
        <td th:text="${prod.price}">2.41</td>
        <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
      </tr>
    </table>
  
    <p>
      <a href="../home.html" th:href="@{/}">Return to home</a>
    </p>
 
  </body>
 
</html>

prod : ${prods}你看到上述手段的属性值“为在评估的结果的每个元素${prods},重复模板的该片段中,使用一个称为PROD可变当前元素”。让我们给每一个看到的事物命名:

我们将调用${prods}的迭代式或迭代变量。
我们将调用prod的迭代变量或者干脆ITER变量。

请注意,proditer变量的作用域为<tr>元素,这意味着它可用于内部标记(如)<td>

迭代值

java.util.List班是不是可以在Thymeleaf用于迭代onlyvalue。有相当完整的一组对象,这些对象被属性视为可迭代的th:each

  • 任何实现的对象 java.util.Iterable
  • 任何实现的对象java.util.Enumeration。
  • 实现的任何对象java.util.Iterator,其值将由迭代器返回,而无需在内存中缓存所有值。
  • 任何实现的对象java.util.Map。迭代地图时,迭代变量将属于class java.util.Map.Entry。
  • 任何数组。
  • 任何其他对象都将被视为包含该对象本身的单值列表。

6.2 保持迭代状态

使用时th:each,Thymeleaf提供了一种用于跟踪迭代状态的有用机制:status变量。

状态变量在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变量本身之后写入名称,并用逗号分隔。就像iter变量一样,status变量的范围也由持有th:each属性的标签所定义的代码片段组成。

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

<!DOCTYPE html>
 
<html>
 
  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
    <link rel="stylesheet" type="text/css" media="all" href="/gtvg/css/gtvg.css" />
  </head>
 
  <body>
 
    <h1>Product list</h1>
  
    <table>
      <tr>
        <th>NAME</th>
        <th>PRICE</th>
        <th>IN STOCK</th>
      </tr>
      <tr class="odd">
        <td>Fresh Sweet Basil</td>
        <td>4.99</td>
        <td>yes</td>
      </tr>
      <tr>
        <td>Italian Tomato</td>
        <td>1.25</td>
        <td>no</td>
      </tr>
      <tr class="odd">
        <td>Yellow Bell Pepper</td>
        <td>2.50</td>
        <td>yes</td>
      </tr>
      <tr>
        <td>Old Cheddar</td>
        <td>18.75</td>
        <td>yes</td>
      </tr>
    </table>
  
    <p>
      <a href="/gtvg/" shape="rect">Return to home</a>
    </p>
 
  </body>
  
</html>

请注意,我们的迭代状态变量运行良好,odd仅对奇数行建立了CSS类。

如果您未明确设置状态变量,则Thymeleaf将始终通过Stat为迭代变量的名称添加后缀来为您创建一个:

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
  </tr>
  <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
  </tr>
</table>

6.3 通过延迟检索数据进行优化

有时我们可能想优化数据集合的检索(例如从数据库中),以便仅在真正要使用这些集合时才检索这些集合。

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

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

context.setVariable(
     "users",
     new LazyContextVariable<List<User>>() {
         @Override
         protected List<User> loadValue() {
             return databaseRepository.findAllUsers();
         }
     });

可以在不了解其惰性的情况下使用此变量,例如:

<ul>
  <li th:each="u : ${users}" th:text="${u.name}">user name</li>
</ul>

但是同时,loadValue()如果在以下代码中condition计算为,则将永远不会初始化(永远不会调用其方法)false:

<ul th:if="${condition}">
  <li th:each="u : ${users}" th:text="${u.name}">user name</li>
</ul>

7 条件评估

7.1 简单条件:“如果”和“除非”

有时,您需要模板的一部分才能仅在满足特定条件的情况下出现在结果中。

例如,假设我们要在产品表中显示一列,其中包含每个产品的评论数量,如果有评论,则指向该产品的评论详细信息页面的链接。

为了做到这一点,我们将使用以下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以下规则评估指定的表达式:

  • 如果value不为null:
    • 如果value是一个布尔值并且是true。
    • 如果value是一个数字且非零
    • 如果value是一个字符并且非零
    • 如果value是一个String且不是“ false”,“ off”或“ no”
    • 如果value不是布尔值,数字,字符或字符串。
  • (如果value为null,则th:if的值为false)。

另外,th:if还有一个inverse属性,th:unless我们可以在前面的示例中使用它,而不是not在OGNL表达式内部使用:

<a href="comments.html"
   th:href="@{/comments(prodId=${prod.id})}" 
   th:unless="${#lists.isEmpty(prod.comments)}">view</a>

7.2 切换语句

还有一种方法可以使用Java 中的开关结构的等效条件来显示内容:th:switch/ th:case属性集。

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

请注意,一旦一个th:case属性的值为true,th:case同一切换上下文中的所有其他属性的值为false。

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

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

8 模板布局

8.1 包括模板片段

定义和引用片段

在我们的模板中,我们经常需要包含其他模板中的部分,例如页脚,页眉,菜单等部分。

为了做到这一点,Thymeleaf需要我们定义这些要包含的部分“片段”,可以使用该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:insert或th:replace属性之一轻松地将其包含在主页中(并且th:include,尽管从Thymeleaf 3.0开始不再建议使用它):

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

请注意,th:insert需要一个片段表达式({…}),这是一个产生片段的表达式。但是,在上面的示例中,它是一个非复杂的片段表达式,({,})包围是完全可选的,因此上面的代码等效于:

<body>
 
  ...
 
  <div th:insert="footer :: copy"></div>
  
</body>

片段规范语法

片段表达式的语法非常简单。有三种不同的格式:

  • "{templatename::selector}"包括在名为的模板上应用指定的标记选择器所产生的片段templatename。请注意,该selector名称可能仅仅是片段名称,因此您可以{templatename::fragmentname}像~{footer :: copy}上面一样指定简单的名称。

标记选择器语法由基础的AttoParser解析库定义,并且类似于XPath表达式或CSS选择器。有关更多信息,请参见附录C。

  • "~{templatename}"包括名为的完整模板templatename。

请注意,您在th:insert/ th:replace标记中使用的模板名称必须由模板引擎当前正在使用的模板解析器解析。

  • {::selector}“或”{this::selector}"从同一模板插入一个片段,匹配selector。如果在出现表达式的模板上未找到,则将模板调用(插入)堆栈遍历到原始处理的模板(root),直到selector在某个级别上匹配为止。

双方templatename并selector在上面的例子可以是全功能的表达式(甚至条件语句!),如:

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

再次注意周围的~{…}信封在th:insert/中是可选的th:replace。

片段可以包含任何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:insert和th:replace(和th:include)之间的差异

和之间有什么区别th:insertth:replace(和th:include,因为3.0不推荐)?

th:insert 最简单:它将简单地将指定的片段作为其host标签的主体插入。

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

th:include与相似th:insert,但不插入片段,而是仅插入该片段的内容。

因此,HTML片段如下所示:

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

…在主机<div>标签中包含了3次,如下所示:

<body>
 
  ...
 
  <div th:insert="footer :: copy"></div>
 
  <div th:replace="footer :: copy"></div>
 
  <div th:include="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>
 
  <div>
    &copy; 2011 The Good Thymes Virtual Grocery
  </div>
  
</body>

8.2 可参数化的片段签名

为了为模板片段创建更类似于函数的机制,使用定义的片段th:fragment可以指定一组参数:

<div th:fragment="frag (onevar,twovar)">
    <p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>

这需要使用以下两种语法之一来从th:insert或调用片段th:replace:

<div th:replace="::frag (${value1},${value2})">...</div>
<div th:replace="::frag (onevar=${value1},twovar=${value2})">...</div>

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

<div th:replace="::frag (twovar=${value2},onevar=${value1})">...</div>

片段局部变量,不带片段参数

即使片段没有这样的参数定义:

<div th:fragment="frag">
    ...
</div>

我们可以使用上面指定的第二种语法来调用它们(并且只有第二种):

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

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

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

请注意,这种对片段的局部变量的规范(无论它是否具有参数签名)都不会导致上下文在执行之前被清空。片段仍将能够像当前一样访问在调用模板中使用的每个上下文变量。

声明模板内断言

该th:assert属性可以指定一个逗号分隔的表达式列表,应对其进行评估,并为每次评估生成true,否则将引发异常。

<div th:assert="${onevar},(${twovar} != 43)">...</div>

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

<header th:fragment="contentheader(title)" th:assert="${!#strings.isEmpty(title)}">...</header>

8.3 灵活的布局:不仅仅是插入片段

借助片段表达式,我们可以为片段指定参数,这些参数不是文本,数字,bean对象……而是标记片段。

这允许我们以一种方式来创建我们的片段,以便可以使用来自调用模板的标记来丰富它们,从而产生非常灵活的模板布局机制。

请注意以下片段中title和links变量的使用:

<head th:fragment="common_header(title,links)">
 
  <title th:replace="${title}">The awesome application</title>
 
  <!-- 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>标记作为title和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>
 
  <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>
...

使用无操作令牌

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

...
<head th:replace="base :: common_header(_,~{::link})">
 
  <title>Awesome - Main</title>
 
  <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
  <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
 
</head>
...

查看title参数(common_header片段的第一个参数)如何设置为no-op(_),这将导致片段的这一部分根本不执行(title= no-operation):

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

因此结果是:

...
<head>
 
  <title>The awesome application</title>
 
  <!-- Common styles and scripts -->
  <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
  <link rel="shortcut icon" href="/awe/images/favicon.ico">
  <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>
 
  <link rel="stylesheet" href="/awe/css/bootstrap.min.css">
  <link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css">
 
</head>
...

片段的高级条件插入

空片段和无操作令牌的可用性使我们能够以非常容易且优雅的方式有条件地插入片段。

例如,我们可以做到这一点,以便插入我们的common :: adminhead片段只有当用户是管理员,并插入任何内容(空片段)如果不是:

...
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : ~{}">...</div>
...

同样,我们可以使用no-operation令牌来仅在满足指定条件时插入片段,而在不满足条件的情况下不做任何修改就保留标记:

...
<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-first或none)。

这意味着删除可能是有条件的,例如:

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

还要注意,它th:remove考虑null了的同义词none,因此以下内容与上面的示例相同:

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

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

8.5 布局继承

为了能够将单个文件作为布局,可以使用片段。具有title和content使用th:fragment和的简单布局的示例th:replace:

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

此示例声明一个名为layout的片段,其中标题和内容为参数。在下面的示例中,这两者都将在页面上被继承的片段表达式替换,并继承它。

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

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

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

9 局部变量

Thymeleaf将局部变量称为为模板的特定片段定义的变量,并且仅可用于该片段内部的评估。

我们已经看到的示例是prod产品列表页面中的iter变量:

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

该prod变量仅在<tr>标记范围内可用。特别:

  • 该th:*标签可用于在该标签中执行的所有其他属性,该属性的优先级低于th:each(这意味着它们将在之后执行th:each)。
  • 它将可用于<tr>标签的任何子<td>元素,例如任何元素。

Thymeleaf为您提供了一种使用th:with属性声明局部变量而无需迭代的方法,其语法类似于属性值分配的语法:

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

当th:with被处理时,该firstPer变量被创建为一个局部变量,并加入到变量映射从上下文来,使得它可用于评估与在上下文中声明的任何其它变量一起,但仅在含有的边界<div>标记。

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

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

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

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

让我们在杂货店的首页中使用它!还记得我们编写的用于输出格式化日期的代码吗?

<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属性都定义了数字优先级,从而确定了它们在标签中执行的顺序。该顺序是:
img

这种优先机制意味着,如果属性位置反转,则上述迭代片段将给出完全相同的结果(尽管可读性稍差):

<ul>
  <li th:text="${item.description}" th:each="item : ${items}">Item description here...</li>
</ul>

11 注释和块(Comments and Blocks)

11.1 标准HTML / XML注释

<!-- ... -->Thymeleaf模板中的任何位置都可以使用标准的HTML / XML注释。这些注释中的所有内容均不会被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:textor th:utext属性中也有效的任何类型的表达式。

请注意,尽管[[...]]对应于th:text(即结果将被HTML转义),但[(...)]对应于th:utext并且将不执行任何HTML转义。因此msg = 'This is <b>great!</b>',给定该片段,使用诸如的变量:

<p>The message is "[(${msg})]"</p>

结果将使那些<b>标签未转义,因此:

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

而如果像这样逃脱了:

<p>The message is "[[${msg}]]"</p>

结果将转义为HTML:

<p>The message is "This is &lt;b&gt;great!&lt;/b&gt;"</p>

请注意,默认情况下,文本内联在标记中每个标签的主体(而不是标签本身)中处于活动状态,因此我们无需执行任何操作即可启用它。

内联与自然模板

如果您来自以这种方式输出文本为标准的其他模板引擎,您可能会问:我们为什么不从一开始就这样做?比所有这些 属性更少的代码th:text !

好吧,要小心,因为尽管您可能会发现内联非常有趣,但是您应该始终记住,当静态打开内联表达式时,它们会逐字显示在HTML文件中,因此您可能无法将它们用作设计原型不再!

浏览器不使用内联静态显示代码片段的方式之间的区别…

Hello, Sebastian!

…并使用它…

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

……在设计实用性方面非常清楚。

禁用内联

不过,可以禁用此机制,因为实际上在某些情况下,我们确实希望输出[[...]]or [(...)]序列而不将其内容作为表达式处理。为此,我们将使用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内联允许<script>在以HTML模板方式处理的模板中更好地集成JavaScript 块。

与文本内联一样,这实际上等同于将脚本内容当作JAVASCRIPT模板模式下的模板来处理,因此,文本模板模式的所有功能(请参阅下一章)将近在咫尺。但是,在本节中,我们将重点介绍如何使用它将Thymeleaf表达式的输出添加到JavaScript块中。

必须使用th:inline="javascript"以下命令显式启用此模式:

<script th:inline="javascript">
    ...
    var username = [[${session.user.name}]];
    ...
</script>

这将导致:

<script th:inline="javascript">
    ...
    var username = "Sebastian \"Fruity\" Applejuice";
    ...
</script>

上面的代码中有两点需要注意:

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

其次,发生这种情况是因为我们将${session.user.name}表达式输出为转义的,即使用双括号表达式:[[${session.user.name}]]。如果相反,我们使用未转义的形式:

<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语法正确编写以下类型的对象:

  • Strings
  • Numbers
  • Booleans
  • Arrays
  • Collections
  • Maps
  • Beans (objects with getter and setter methods)
    例如,如果我们有以下代码:
<script th:inline="javascript">
    ...
    var user = /*[[${session.user}]]*/ null;
    ...
</script>

${session.user}表达式将求值为User对象,Thymeleaf会将其正确转换为Javascript语法:

<script th:inline="javascript">
    ...
    var user = {"age":null,"firstName":"John","lastName":"Apricot",
                "name":"John Apricot","nationality":"Antarctica"};
    ...
</script>

完成该JavaScript序列化的方式是通过org.thymeleaf.standard.serializer.IStandardJavaScriptSerializer接口的实现,该接口可以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,JAVASCRIPT和CSS。这将它们与标记模板模式区分开:HTML和XML。

文本模板模式和标记模式之间的主要区别在于,在文本模板中,没有标签可以插入属性形式的逻辑,因此我们必须依靠其他机制。

这些机制的第一个也是最基本的是内联,我们已经在上一章中进行了详细介绍。内联语法是在文本模板模式下输出表达式结果的最简单方法,因此,这是文本电子邮件的完美有效模板。

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

即使没有标签,上面的示例也是一个完整且有效的Thymeleaf模板,可以在TEXT模板模式下执行。

但是,为了包含比单纯的输出表达式更复杂的逻辑,我们需要一种新的非基于标记的语法:

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

实际上是更冗长的精简版本:

[#th:block th:each="item : ${items}"]
  - [#th:block th:utext="${item}" /]
[/th:block]

请注意,这种新语法是如何基于声明为的元素(即可处理标签)[#element ...]<element ...>。元素的打开方式类似于[#element ...]和闭合的方式一样[/element],并且可以通过将open元素最小化来声明独立标签/,该方式几乎等同于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页面调用该结果。请注意,这不是<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模板内部的-mode内联),Thymeleaf 3.0允许转义其文本语法中元素的属性。所以:

  • TEXT模板模式下的属性将采用HTML转换格式。
  • JAVASCRIPT模板模式下的属性将是JavaScript非转义的。
  • CSS模板模式下的属性将采用CSS换码。

因此,这在TEXT-mode模板中是完全可以的(请注意>):

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

当然,<在实际的文本模板中这没有任何意义,但是如果我们正在使用th:inline="text"包含上面代码的代码块处理HTML模板,并且要确保我们的浏览器不会将它<user.age用作名称的话,这是一个好主意。静态打开文件作为原型时的open标签。

13.2 可扩展性

这种语法的优点之一是它与标记语法一样可扩展。开发人员仍然可以使用自定义元素和属性来定义自己的方言,为它们应用前缀(可选),然后在文本模板模式下使用它们:

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

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

在JAVASCRIPT和CSS模板模式(不适用于TEXT),允许包括一个特殊的注释语法之间的代码/*[+...+]*/,这样Thymeleaf会处理模板时自动取消注释这样的代码:

var x = 23;
 
/*[+
 
var msg  = "This is a working application";
 
+]*/
 
var f = function() {
    ...

将执行为:

var x = 23;
 
var msg  = "This is a working application";
 
var f = function() {
...

您可以在这些注释中包含表达式,它们将被评估:

var x = 23;
 
/*[+
 
var msg  = "Hello, " + [[${session.user.name}]];
 
+]*/
 
var f = function() {
...

13.4 文本解析器级注释块:删除代码

在类似于仅原型的注释块的方式,所有三个文本模板模式(TEXT,JAVASCRIPT和CSS)使其能够指示Thymeleaf特殊之间移除代码/*[- *//* -]*/标志,就像这样:

var x = 23;
 
/*[- */
 
var msg  = "This is shown only when executed statically!";
 
/* -]*/
 
var f = function() {
...

或在TEXT模式下:

...
/*[- Note the user is obtained from the session, which must exist -]*/
Welcome [(${session.user.name})]!
...

13.5 自然的JavaScript和CSS模板

如上一章所述,JavaScript和CSS内联提供了将内联表达式包含在JavaScript / CSS注释中的可能性,例如:

...
var username = /*[[${session.user.name}]]*/ "Sebastian Lychee";
...

…这是有效的JavaScript,执行后的外观如下:

...
var username = "John Apricot";
...

实际上,可以将这种将内联表达式包含在注释中的相同技巧可用于整个文本模式语法:

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

如果模板是静态打开的(因为它是100%有效的JavaScript),并且如果用户是管理员运行模板,则将在上面的代码中显示该警报。它等效于:

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

…实际上是模板解析期间将初始版本转换为的代码。

但是请注意,在注释中包装元素并不会;像内联输出表达式那样清除它们所在的行(直到找到a为止,一直在右边)。该行为仅保留给内联输出表达式。

因此Thymeleaf 3.0允许以自然模板的形式开发复杂的JavaScript脚本和CSS样式表,这些模板既可以作为原型也可以作为工作模板使用。

14 杂货店的更多页面

现在我们对使用Thymeleaf有了很多了解,我们可以在我们的网站上添加一些新页面以进行订单管理。

请注意,我们将专注于HTML代码,但是如果您想查看相应的控制器,则可以查看捆绑的源代码。

14.1 订单清单

让我们从创建订单列表页面开始/WEB-INF/templates/order/list.html

<!DOCTYPE html>
 
<html xmlns:th="http://www.thymeleaf.org">
 
  <head>
 
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
  </head>
 
  <body>
 
    <h1>Order list</h1>
  
    <table>
      <tr>
        <th>DATE</th>
        <th>CUSTOMER</th>
        <th>TOTAL</th>
        <th></th>
      </tr>
      <tr th:each="o : ${orders}" th:class="${oStat.odd}? 'odd'">
        <td th:text="${#calendars.format(o.date,'dd/MMM/yyyy')}">13 jan 2011</td>
        <td th:text="${o.customer.name}">Frederic Tomato</td>
        <td th:text="${#aggregates.sum(o.orderLines.{purchasePrice * amount})}">23.32</td>
        <td>
          <a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
        </td>
      </tr>
    </table>
  
    <p>
      <a href="../home.html" th:href="@{/}">Return to home</a>
    </p>
    
  </body>
  
</html>

除了这点OGNL魔法外,这里没有什么让我们感到惊讶的:

<td th:text="${#aggregates.sum(o.orderLines.{purchasePrice * amount})}">23.32</td>

这样做是针对订单中的每个订单行(OrderLine对象),将其purchasePrice和amount属性相乘(通过调用相应的getPurchasePrice()和getAmount()方法),然后将结果返回到数字列表中,然后由该#aggregates.sum(...)函数进行汇总,以获取订单总数价钱。

您必须喜欢OGNL的强大功能。

14.2 订单明细

现在进入订单详细信息页面,在该页面中,我们将大量使用星号语法:

<!DOCTYPE html>
 
<html xmlns:th="http://www.thymeleaf.org">
 
  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
  </head>
 
  <body th:object="${order}">
 
    <h1>Order details</h1>
 
    <div>
      <p><b>Code:</b> <span th:text="*{id}">99</span></p>
      <p>
        <b>Date:</b>
        <span th:text="*{#calendars.format(date,'dd MMM yyyy')}">13 jan 2011</span>
      </p>
    </div>
 
    <h2>Customer</h2>
 
    <div th:object="*{customer}">
      <p><b>Name:</b> <span th:text="*{name}">Frederic Tomato</span></p>
      <p>
        <b>Since:</b>
        <span th:text="*{#calendars.format(customerSince,'dd MMM yyyy')}">1 jan 2011</span>
      </p>
    </div>
  
    <h2>Products</h2>
  
    <table>
      <tr>
        <th>PRODUCT</th>
        <th>AMOUNT</th>
        <th>PURCHASE PRICE</th>
      </tr>
      <tr th:each="ol,row : *{orderLines}" th:class="${row.odd}? 'odd'">
        <td th:text="${ol.product.name}">Strawberries</td>
        <td th:text="${ol.amount}" class="number">3</td>
        <td th:text="${ol.purchasePrice}" class="number">23.32</td>
      </tr>
    </table>
 
    <div>
      <b>TOTAL:</b>
      <span th:text="*{#aggregates.sum(orderLines.{purchasePrice * amount})}">35.23</span>
    </div>
  
    <p>
      <a href="list.html" th:href="@{/order/list}">Return to order list</a>
    </p>
 
  </body>
  
</html>

除了嵌套对象选择之外,这里没有太多新的东西:

<body th:object="${order}">
 
  ...
 
  <div th:object="*{customer}">
    <p><b>Name:</b> <span th:text="*{name}">Frederic Tomato</span></p>
    ...
  </div>
 
  ...
</body>

*{name}等于

<p><b>Name:</b> <span th:text="${order.customer.name}">Frederic Tomato</span></p>

15 有关配置的更多信息

15.1 模板解析器

对于我们的Good Thymes虚拟杂货店,我们选择了一个ITemplateResolver实现ServletContextTemplateResolver,该实现允许我们从Servlet上下文中获取模板作为资源。

除了使我们能够通过实现ITemplateResolver,Thymeleaf 来创建自己的模板解析器之外,还包括以下四种实现:

  • org.thymeleaf.templateresolver.ClassLoaderTemplateResolver,它将模板解析为类加载器资源,例如:
return Thread.currentThread().getContextClassLoader().getResourceAsStream(template);
  • org.thymeleaf.templateresolver.FileTemplateResolver,它将模板解析为来自文件系统的文件,例如:
return new FileInputStream(new File(template));
  • org.thymeleaf.templateresolver.UrlTemplateResolver,它将模板解析为URL(甚至是非本地的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.setEncoding("UTF-8");
  • 使用的模板模式:
// Default is HTML
templateResolver.setTemplateMode("XML");
  • 模板缓存的默认模式,以及用于定义特定模板是否可缓存的模式:
// Default is true
templateResolver.setCacheable(false);
templateResolver.getCacheablePatternSpec().addPattern("/users/*");
  • 源自此模板解析器的已解析模板缓存条目的TTL(以毫秒为单位)。如果未设置,则从缓存中删除条目的唯一方法是超过缓存的最大大小(最旧的条目将被删除)。
// Default is no TTL (only cache size exceeded would remove entries)
templateResolver.setCacheTTLMs(60000L);

Thymeleaf + Spring集成软件包提供了一个SpringResourceTemplateResolver实现,该实现使用所有Spring基础结构来访问和读取应用程序中的资源,这是在支持Spring的应用程序中推荐的实现。

链模板解析器

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

ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
 
ServletContextTemplateResolver servletContextTemplateResolver = 
        new ServletContextTemplateResolver(servletContext);
servletContextTemplateResolver.setOrder(Integer.valueOf(2));
 
templateEngine.addTemplateResolver(classLoaderTemplateResolver);
templateEngine.addTemplateResolver(servletContextTemplateResolver);

当应用多个模板解析器时,建议为每个模板解析器指定模式,以便Thymeleaf可以快速丢弃那些不打算解析模板的模板解析器,从而提高性能。并不是必须这样做,而是建议:

ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
// This classloader will not be even asked for any templates not matching these patterns 
classLoaderTemplateResolver.getResolvablePatternSpec().addPattern("/layout/*.html");
classLoaderTemplateResolver.getResolvablePatternSpec().addPattern("/menu/*.html");
 
ServletContextTemplateResolver servletContextTemplateResolver = 
        new ServletContextTemplateResolver(servletContext);
servletContextTemplateResolver.setOrder(Integer.valueOf(2));

如果未指定这些可解析的模式,我们将依赖于ITemplateResolver我们所使用的每个实现的特定功能。请注意,并非所有的实现都能够在解析之前确定模板的存在,因此始终可以将模板视为可解析的,并打破了解析链(不允许其他解析器检查同一模板),但是却无法阅读实际资源。

所有ITemplateResolver附带核心Thymeleaf实现包括一种机制,将使我们能够使解析器真正检查如果资源考虑之前存在解析。它是checkExistence标志,其工作方式如下:

ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
classLoaderTempalteResolver.setCheckExistence(true);

该checkExistence标志强制解析器在解析阶段对资源是否存在进行真正的检查(如果存在检查返回false,则调用链中的以下解析器)。尽管这在每种情况下听起来都不错,但在大多数情况下,这将意味着对资源本身的双重访问(一次检查是否存在,另一次读取它),并且在某些情况下可能会成为性能问题,例如,基于远程URL模板资源–潜在的性能问题可能会通过使用模板缓存而在很大程度上得到缓解(在这种情况下,仅在首次访问模板时才能解决模板问题)。

15.2 邮件解析器

我们没有为Grocery应用程序明确指定Message Resolver实现,并且如前所述,这意味着所使用的实现是一个org.thymeleaf.messageresolver.StandardMessageResolver对象。

StandardMessageResolver是IMessageResolver接口的标准实现,但是如果需要,我们可以创建自己的接口,以适应应用程序的特定需求。

Thymeleaf + Spring集成软件包默认提供一种IMessageResolver实现,该实现使用标准Spring方法来检索外部化消息,方法是使用MessageSource在Spring Application Context声明的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 转换服务

该转换服务,使我们用的手段来进行数据转换和格式化操作双括号语法(${{...}})实际上是标准方言的特点,而不是Thymeleaf模板引擎本身。

这样,配置它的方法是通过IStandardConversionService直接将我们的接口的自定义实现设置StandardDialect为正配置到模板引擎中的接口的实例。喜欢:

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

templateEngine.setDialect(dialect);

请注意,thymeleaf-spring3和thymeleaf-spring4软件包包含SpringStandardDialect,并且该方言已经预先配置了IStandardConversionService将Spring自己的Conversion Service基础结构集成到Thymeleaf中的实现。

15.4 记录

Thymeleaf非常重视日志记录,并始终尝试通过其日志记录界面提供尽可能多的有用信息。

slf4j,实际上,所使用的日志记录库实际上充当了我们想要在应用程序中使用的任何日志记录实现的桥梁(例如log4j)。

Thymeleaf班会记录TRACE,DEBUG并INFO-level信息,这取决于我们希望的详细程度,并且除了一般的记录它会使用与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

使用的Thymeleaf日志记录基础结构的示例配置log4j可能是:

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

主要思想是模板逻辑将在单独的逻辑文件中定义(更确切地说是逻辑资源,因为它不必是file)。默认情况下,该逻辑资源将是与模板文件位于同一位置(例如,文件夹)的附加文件,其名称相同,但.th.xml扩展名为:

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

因此,该home.html文件可以完全没有逻辑。它可能看起来像这样:

<!DOCTYPE html>
<html>
  <body>
    <table id="usersTable">
      <tr>
        <td class="username">Jeremy Grapefruit</td>
        <td class="usertype">Normal User</td>
      </tr>
      <tr>
        <td class="username">Alice Watermelon</td>
        <td class="usertype">Administrator</td>
      </tr>
    </table>
  </body>
</html>

那里绝对没有Thymeleaf代码。这是没有Thymeleaf或模板知识的设计人员可以创建,编辑和/或理解的模板文件。或某些外部系统完全没有Thymeleaf钩子提供的HTML片段。

现在,home.html通过创建如下所示的其他home.th.xml文件,将该模板转换为Thymeleaf模板:

<?xml version="1.0"?>
<thlogic>
  <attr sel="#usersTable" th:remove="all-but-first">
    <attr sel="/tr[0]" th:each="user : ${users}">
      <attr sel="td.username" th:text="${user.name}" />
      <attr sel="td.usertype" th:text="#{|user.type.${user.type}|}" />
    </attr>
  </attr>
</thlogic>

在这里,我们可以看到<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 ServletContextTemplateResolver templateResolver = 
        new ServletContextTemplateResolver(servletContext);
...
templateResolver.setUseDecoupledLogic(true);

混合耦合和解耦逻辑

启用后,解耦模板逻辑不是必需的。启用后,这意味着引擎将查找包含解耦逻辑的资源,如果存在,则将其解析并与原始模板合并。如果解耦的逻辑资源不存在,则不会引发任何错误。

同样,在同一模板中,我们可以混合使用耦合逻辑和解耦逻辑,例如,通过在原始模板文件中添加一些Thymeleaf属性,而将其他属性留给单独的解耦逻辑文件。最常见的情况是使用new(在v3.0中)th:ref属性。

17.3 th:ref属性

th:ref只是标记属性。从处理的角度来看,它什么也没做,只是在处理模板后消失,但是它的作用在于它充当标记引用,即可以通过标记选择器中的名称来解析,就像标记名或片段一样。(th:fragment)。

因此,如果我们有一个选择器,例如:

  <attr sel="whatever" .../>

这将匹配:

  • 任何<whatever>标签。
  • 具有th:fragment="whatever"属性的任何标签。
  • 具有th:ref="whatever"属性的任何标签。

锚,这一事实最终可能会污染我们的输出。

从同样的意义上说,它的缺点是th:ref什么?好吧,很显然,我们将在模板中添加一些Thymeleaf逻辑(“逻辑”)。

请注意,该th:ref属性的适用性不仅适用于解耦的逻辑模板文件:它在其他类型的场景中也一样工作,例如在片段表达式(~{…})中。

17.4 解耦模板的性能影响

影响极小。当一个已解析的模板被标记为使用解耦逻辑并且不被缓存时,该模板逻辑资源将首先被解析,解析并处理为一系列内存中的指令:基本上是要注入到每个标记选择器的属性列表。

但这是唯一需要执行的附加步骤,因为在此之后,将解析真实模板,并且在解析这些模板时,由于AttoParser中节点选择的高级功能,这些属性将由解析器本身即时注入。。因此,已解析的节点将从解析器中出来,就像它们的注入属性写在原始模板文件中一样。

这样最大的优势?将模板配置为要缓存时,它将缓存已包含注入属性的模板。因此,一旦对高速缓存的模板使用解耦的模板进行缓存,其开销将绝对为零。

17.5 解耦逻辑的解析

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

此标准实现有什么作用?

  • 首先,它将a prefix和a 应用于模板资源suffix的基本名称(通过其ITemplateResource#getBaseName()方法获得)。前缀和后缀都可以配置,默认情况下,前缀为空,后缀为.th.xml。
  • 其次,它要求模板资源通过其方法来解析具有所计算名称的相对资源ITemplateResource#relative(String relativeLocation)。

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

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

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

某些对象和变量映射始终可被调用。让我们看看他们:

基础对象

  • #ctx:上下文对象。一种实现org.thymeleaf.context.IContext或org.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.Locale与当前请求关联的内容。
${#locale}

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

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

请注意,这些不是上下文对象,而是作为变量添加到上下文中的映射,因此我们不使用即可访问它们#。它们以某种方式充当命名空间。

  • param:用于检索请求参数。param.foo是String[]带有foorequest参数值的a,因此param.foo是String[]带有foorequest参数值的a,因此{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')}
...
  • application:用于检索应用程序/ servlet上下文属性。
/*
 * =============================================================================
 * See javadoc API for class org.thymeleaf.context.WebServletContextVariablesMap
 * =============================================================================
 */

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

请注意,无需指定用于访问请求属性的名称空间(与request参数相反),因为所有请求属性都作为变量自动添加到上下文根目录中的上下文中:

${myRequestAttribute}

Web上下文对象

在Web环境中,还可以直接访问以下对象(请注意,这些是对象,而不是映射/命名空间):

  • #request:直接访问javax.servlet.http.HttpServletRequest与当前请求关联的对象。
${#request.getAttribute('foo')}
${#request.getParameter('foo')}
${#request.getContextPath()}
${#request.getRequestName()}
...
  • #session:直接访问javax.servlet.http.HttpSession与当前请求关联的对象。
${#session.getAttribute('foo')}
${#session.id}
${#session.lastAccessedTime}
...
  • #servletContext:直接访问javax.servlet.ServletContext与当前请求关联的对象。
${#servletContext.getAttribute('foo')}
${#servletContext.contextPath}
...

19 附录B:Expression Utility对象

Execution Info

#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

#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

#conversions:实用程序对象,允许在模板的任何位置执行转换服务:

/*
 * ======================================================================
 * 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

#dates:java.util.Date对象的实用程序方法:

/*
 * ======================================================================
 * 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

#calendars:类似于#dates,但对于java.util.Calendar对象:

/*
 * ======================================================================
 * 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()}

Numbers

#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

#strings:String对象的实用方法:

/*
 * ======================================================================
 * 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

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

Booleans

#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

#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

#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

#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

#maps : utility methods for 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

#aggregates : utility methods for creating aggregates on arrays or collections

/*
 * ======================================================================
 * 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)}

IDs

#ids : utility methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).

/*
 * ======================================================================
 * 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')}

20 附录C:标记选择器语法

Thymeleaf的标记选择器直接从Thymeleaf的解析库AttoParser借用

该选择器的语法与XPath,CSS和jQuery中的选择器的语法有很大相似之处,这使它们对于大多数用户而言易于使用。您可以在AttoParser文档中查看完整的语法参考。

例如,以下选择器将在标记内的每个位置选择

class content的每个对象(请注意,这样做并不那么简洁,请继续阅读以了解原因):

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

基本语法包括:

  • /x 表示名称为x的当前节点的直接子代。
  • //x 表示任意深度的名称为x的当前节点的子代。
  • x[@z="v"] 表示名称为x的元素和名为z的属性,其值为“ v”。
  • x[@z1="v1" and @z2="v2"] 表示具有名称x的元素以及具有值“ v1”和“ v2”的属性z1和z2。
  • x[i] 表示名称x处于其兄弟姐妹中的第i个元素。
  • x[@z="v"][i] 表示元素名称为x,属性z的值为“ v”,并且在与该条件匹配的同级元素中位于第i个位置。

但是也可以使用更简洁的语法:

  • x完全等同于//x(x在任何深度级别搜索具有名称或引用的元素,引用是a th:ref或th:fragment属性)。
  • 选择器也可以不带元素名称/引用,只要它们包含参数说明即可。因此[@class=‘oneclass’],一个有效的选择器将查找具有value的class属性的任何元素(标签)“oneclass”。

高级属性选择功能:

  • 除=(等于)外,其他比较运算符也有效:(!=不等于),=(以开头)和$=(以结束)。例如:x[@class=‘section’]表示具有名称x和以class开头的属性值的元素section。
  • 既可以以@(XPath样式)开始也可以不以(jQuery样式)开始指定属性。所以x[z=‘v’]等于x[@z=‘v’]。
  • 多属性修饰符既可以与and(XPath样式)结合,也可以通过链接多个修饰符(jQuery样式)来结合。因此x[@z1=‘v1’ and @z2=‘v2’]实际上等效于x[@z1=‘v1’][@z2=‘v2’](并且也等效于x[z1=‘v1’][z2=‘v2’])。

直接类似于jQuery的选择器:

  • x.oneclass等同于x[class=‘oneclass’]。
  • .oneclass等同于[class=‘oneclass’]。
  • x#oneid等同于x[id=‘oneid’]。
  • #oneid等同于[id=‘oneid’]。
  • x%oneref表示<x>具有th:ref="oneref"或th:fragment="oneref"属性的标签。
  • %oneref表示任何具有th:ref="oneref"或th:fragment="oneref"属性的标签。请注意,这实际上等效于简单的oneref原因,因为可以使用引用代替元素名称。
  • 直接选择器和属性选择器可以混合使用:a.external[@href^=‘https’]。

因此,上面的标记选择器表达式:

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

可以写成:

<div th:insert="mytemplate :: div.content">...</div>

检查另一个示例,这是:

<div th:replace="mytemplate :: myfrag">...</div>

将寻找th:fragment="myfrag"片段签名(或th:ref引用)。但是,myfrag如果存在则还会搜索带有名称的标签(在HTML中不存在)。注意与以下内容的区别:

<div th:replace="mytemplate :: .myfrag">...</div>

…实际上将查找带有的任何元素class=“myfrag”,而无需关心th:fragment签名(或th:ref引用)。

多值类匹配

标记选择器了解要多值化的类属性,因此即使元素具有多个class值,也可以在该属性上应用选择器。

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

;