Thymeleaf

1 Thymeleaf 简介

1.1 什么是 Thymeleaf?

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

Thymeleaf 的主要目标是提供一种优雅且高度可维护的模板创建方式。为此,它以“自然模板”的概念为基础,以不影响模板用作设计原型的方式将其逻辑注入模板文件。这样可以改善设计沟通,并缩小设计团队与开发团队之间的差距。

Thymeleaf 的设计也从一开始就考虑了 Web 标准-特别是 HTML5 –如果需要,您可以创建完全验证的模板。

1.2 Thymeleaf 可以处理哪种模板?

开箱即用的 Thymeleaf 允许您处理六种模板,每种模板都称为 Template Mode

  • HTML

  • XML

  • TEXT

  • JAVASCRIPT

  • CSS

  • RAW

有两种标记模板模式(HTMLXML),三种文本模板模式(TEXTJAVASCRIPTCSS)和无操作模板模式(RAW)。

HTML 模板模式将允许任何类型的 HTMLImporting,包括 HTML5,HTML 4 和 XHTML。将不执行任何验证或格式正确性检查,并且模板代码/结构将在输出中得到最大程度的尊重。

XML 模板模式将允许 XMLImporting。在这种情况下,代码应该是格式正确的-没有未关闭的标签,没有未引用的属性等,并且如果发现格式错误,则解析器将引发异常。请注意,不会执行* validation *(针对 DTD 或 XML 模式)。

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

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

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

RAW 模板模式根本不会处理模板。它旨在用于将未修改的资源(文件,URL 响应等)插入正在处理的模板中。例如,可以安全地知道将不会执行这些资源可能包含的任何 Thymeleaf 代码,而将 HTML 格式的外部不受控制的资源包含在应用程序模板中。

1.3 方言:标准方言

Thymeleaf 是一个非常可扩展的模板引擎(实际上,它可以称为* template engine framework *),它允许您定义和自定义将模板处理到详细级别的方式。

将某种逻辑应用于标记工件(如果模板不是标记的话,标记,一些文本,Comments 或仅占位符)的对象称为* processor *,以及一组这些处理器-也许还有一些额外的工件-通常是“方言”的组成部分。开箱即用的 Thymeleaf 核心库提供了一种称为 Standard Dialect 的方言,对于大多数用户来说,这已经足够了。

Note

请注意,方言实际上可以没有处理器,而完全由其他类型的工件组成,但是处理器无疑是最常见的用例。

本教程介绍了标准方言。即使未明确提及,您在接下来的页面中将学习的每个属性和语法功能都由该方言定义。

当然,如果用户想在利用 Library 的高级功能的同时定义自己的处理逻辑,则可以创建自己的方言(甚至扩展标准方言)。 Thymeleaf 也可以配置为一次使用多种方言。

Note

官方的 thymeleaf-spring3 和 thymeleaf-spring4 集成软件包都定义了一种称为“ SpringStandard Dialect”的方言,该方言与 Standard Dialect 大致相同,但进行了较小的改动以更好地利用 Spring Framework 中的某些功能(例如,方法是使用 Spring Expression Language 或 SpringEL 而不是 OGNL)。因此,如果您是 Spring MVC 用户,那么您就不会浪费时间,因为您在此处学习的几乎所有内容都将在 Spring 应用程序中使用。

标准方言的大多数处理器都是属性处理器。这使浏览器甚至在处理之前也能正确显示 HTML 模板文件,因为它们将简单地忽略其他属性。例如,尽管使用标签库的 JSP 可能包含一段代码,但这些代码无法直接由浏览器显示:

<form:inputText name="userName" value="${user.name}" />

…Thymeleaf 标准方言将使我们能够通过以下方式实现相同的功能:

<input type="text" name="userName" value="James Carrot" th:value="${user.name}" />

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

这可以帮助您的设计人员和开发人员处理完全相同的模板文件,并减少将静态原型转换为工作模板文件所需的工作。执行此操作的功能是称为自然模板的功能。

2 The Good Thymes 虚拟杂货店

Good Thymes 虚拟杂货 GitHub 存储库中可以找到本指南以及本指南以后各章中显示的示例的源代码。

2.1 杂货店的网站

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

该应用程序是一个虚拟虚拟杂货店的网站,它将为我们提供许多方案来展示 Thymeleaf 的许多功能。

首先,我们需要为应用程序提供一组简单的模型实体:Products,它们通过创建Orders出售给Customers。我们还将针对ProductsManagementComments

示例应用程序模型

示例应用程序模型

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

public class ProductService {

    ...

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

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

在 Web 层,我们的应用程序将具有一个过滤器,该过滤器将根据请求 URL 将执行委派给启用 Thymeleaf 的命令:

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 Context *中检索模板文件作为资源:资源存在于应用程序范围内的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 之前仍可以从缓存中删除模板。

Note

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

关于模板解析器,还有很多要学习的知识,但是现在让我们看一下模板引擎对象的创建。

模板引擎

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

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

相当简单,不是吗?我们需要做的就是创建一个实例并将模板解析器设置为该实例。

模板解析器是TemplateEngine唯一的*“必需”参数,尽管稍后还会涉及许多其他参数(消息解析器,缓存大小等)。目前,这就是我们所需要的。

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

3 使用 Literals

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

但您可能还会注意到,此模板并不是 true 的有效 HTML5 文档,因为 HTML5 规范不允许我们以th:*形式使用的这些非标准属性。实际上,我们甚至在<html>标记中添加了xmlns:th属性,这绝对不符合 HTML5 规范:

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

…这在模板处理中完全没有影响,但是起“引诱”作用,可防止我们的 IDE 抱怨所有th:*属性缺少名称空间定义。

那么,如果我们想使此模板成为 HTML5-valid **怎么办?简单:使用属性名称和连字符(-)分隔符而不是分号(:)的前缀data-切换到 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>

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

Note

这两种符号是完全等效且可互换的,但是为了代码示例的简洁和紧凑,本教程将使用命名空间符号(th:*)。同样,th:*表示法更通用,并且在每种 Thymeleaf 模板模式(XMLTEXT…)中都允许使用,而data-表示法仅在HTML模式中允许使用。

使用 th:text 和外部化文本

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

消息始终具有用于标识消息的密钥,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文件的实现,但是如果我们想从数据库中获取消息,则可以创建自己的实现。

但是,我们没有在初始化期间为模板引擎指定消息解析器,这意味着我们的应用程序正在使用由org.thymeleaf.messageresolver.StandardMessageResolver实现的* Standard Message Resolver *。

标准消息解析器希望在与文件夹相同名称和模板名称的属性文件中找到/WEB-INF/templates/home.html的消息,例如:

  • /WEB-INF/templates/home_en.properties用于英文文本。

  • /WEB-INF/templates/home_es.properties用于西班牙语文本。

  • /WEB-INF/templates/home_pt_BR.properties用于葡萄牙语(巴西)语言 Literals。

  • /WEB-INF/templates/home.properties用于默认文本(如果语言环境不匹配)。

我们来看一下home_es.properties文件:

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

这就是使 Thymeleaf 加工成为模板所需要的。然后创建我们的 Home 控制器。

Contexts

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

public class HomeController implements IGTVGController {

    public void process(
            final 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());
        
    }

}

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

public interface IContext {

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

该接口有一个专门的 extensionsorg.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是我们使用的那个。实际上,我们必须这样做,因为使用ServletContextTemplateResolver要求我们使用实现IWebContext的上下文。

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

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

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

  • ${x}将返回存储在 Thymeleaf 上下文中或作为* request 属性*的变量x

  • ${param.x}将返回一个称为x请求参数(可能是多值)。

  • ${session.x}将返回名为x会话属性

  • ${application.x}将返回名为x的* servlet 上下文属性*。

执行模板引擎

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

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 有关文本和变量的更多信息

Unescaped Text

主页的最简单版本现在似乎已经准备就绪,但是有些事情我们还没有想到……如果我们收到这样的消息怎么办?

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());
        
}

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

<body>

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

  <p>Today is: <span th:text="${today}">13 February 2011</span></p>
  
</body>

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

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

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

4 标准表达语法

我们将在杂货店虚拟 Store 的开发中稍作 Rest,以了解 Thymeleaf Standard 方言最重要的部分之一: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>

但是有更多类型的表达式,还有更多有趣的细节来了解我们已经知道的表达式。首先,让我们看一下标准表达式功能的快速摘要:

  • Simple expressions:

  • 变量表达式:${...}

    • 选择变量表达式:*{...}

    • 讯息表达:#{...}

    • 链接 URL 表达式:@{...}

    • 片段表达式:~{...}

  • Literals

  • LiteralsLiterals:'one text''Another one!',...

    • 数字 Literals:0343.012.3,…

    • 布尔 Literals:truefalse

    • 空 Literals:null

    • Literals 标记:onesometextmain,...

  • Text operations:

  • 字符串串联:+

    • Literals 替换:|The name is ${name}|
  • Arithmetic operations:

  • 二进制运算符:+-*/%

    • 减号(一元运算符):-
  • Boolean operations:

  • 二进制运算符:andor

    • 布尔取反(一元运算符):!not
  • 比较和equal:

  • 比较器:><>=<=(gtltgele)

    • 等于运算符:==!=(eqne)
  • Conditional operators:

  • 如果-则:(if) ? (then)

    • 如果-则-其他:(if) ? (then) : (else)

    • 默认值:(value) ?: (defaultvalue)

  • Special tokens:

  • 无操作:_

所有这些功能都可以组合和嵌套:

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

4.1 Messages

众所周知,#{...}消息表达式使我们可以链接此链接:

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

…to this:

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

但是,我们仍然没有想到的一个方面:如果消息文本不是完全静态的,会发生什么?例如,如果我们的应用程序知道随时有谁在访问该站点,而我们想按名称向他们打招呼,该怎么办?

<p>¡Bienvenido a nuestra tienda de comestibles, John Apricot!</p>

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

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

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

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

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

Note

请注意,此处使用th:utext表示格式化的消息将不会被转义。本示例假定user.name已被转义。

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

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

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

4.2 Variables

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

Note

有关 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 表达式时,某些对象可用于表达式,以提高灵 Active。这些对象将以#符号开头(根据 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>.

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

表达式 Util 对象

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

  • #execInfo:有关正在处理的模板的信息。

  • #messages:用于获取变量表达式内的外部化消息的方法,与使用#\ {…}语法所获得的方法相同。

  • #uris:转义部分 URL/URI 的方法

  • #conversions:用于执行配置的转换服务(如果有)的方法。

  • #dates:用于java.util.Date对象的方法:格式化,组件提取等。

  • #calendars:类似于#dates,但适用于java.util.Calendar个对象。

  • #numbers:格式化数字对象的方法。

  • #stringsString个对象的方法:包含,startsWith,前置/附加等。

  • #objects:一般对象的方法。

  • #bools:布尔值评估的方法。

  • #arrays:数组的方法。

  • #lists:列表方法。

  • #sets:集合的方法。

  • #maps:Map 方法。

  • #aggregates:用于在数组或集合上创建聚合的方法。

  • #ids:用于处理可能重复的 id 属性的方法(例如,作为迭代的结果)。

您可以在Appendix B中检查这些 Util 对象提供的功能。

重新格式化首页中的日期

现在我们知道了这些 Util 对象,可以使用它们来更改在主页上显示日期的方式。而不是在我们的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属性的表达式的结果。让我们在用户 Profile(userprofile.html)页面中使用一个:

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

完全等同于:

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

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

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

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

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

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

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

4.4 链接网址

由于 URL 的重要性,URL 是 Web 应用程序模板中的一等公民,并且* Thymeleaf 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,并将该值设置为<a>标签的href属性。

  • 我们被允许对 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 允许您通过使用 Servlet API 中的response.encodeURL(...)机制为每个 URL 插入自己的重写过滤器。

  • th:href属性允许我们(可选)在模板中具有可使用的静态href属性,因此,当直接打开原型进行原型设计时,模板链接仍可被浏览器导航。

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

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

主页的菜单

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

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

服务器根目录相对 URL

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

4.5 Fragments

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

最常见的用途是使用th:insertth:replace进行片段插入(在后面的部分中有更多关于这些的信息):

<div th:insert="~{commons :: main}">...</div>

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

<div th:with="frag=~{footer :: #main/text()}">
  <p th:insert="${frag}">
</div>

在本教程的后面,将有一个完整的章节专门介绍“模板布局”,包括对片段表达式的更深入的说明。

4.6 Literals

Text literals

文本 Literals 只是在单引号之间指定的字符串。它们可以包含任何字符,但是您应该使用\'对其内的任何单引号进行转义。

<p>
  Now you are looking at a <span th:text="'working web application'">template file</span>.
</p>

Number literals

数字 Literals 就是:数字。

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

Boolean literals

布尔 Literals 是truefalse。例如:

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

在此示例中,== false写在大括号外,因此 Thymeleaf 负责处理。如果将其写在花括号内,则 OGNL/SpringEL 引擎应负责:

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

空 Literals

nullLiterals 也可以使用:

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

Literal tokens

实际上,数字,布尔值和 nullLiterals 是* literal tokens *的一种特殊情况。

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

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

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

instead of:

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

4.7 附加 Literals

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

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

4.8Literals 替代

Literals 替换可以轻松格式化包含变量值的字符串,而无需在 Literals 后面加上'...' + '...'

这些替换项必须用竖线(|)包围,例如:

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

等效于:

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

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

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

Note

|...|Literals 替换内仅允许变量/消息表达式(${...}*{...}#{...})。没有其他 Literals('...'),布尔/数字标记,条件表达式等。

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 比较器和equal

表达式中的值可以与><>=<=符号进行比较,并且==!=运算符可以用于检查是否相等(或是否相等)。请注意,XML 规定<>符号不应在属性值中使用,因此应将它们替换为&lt;&gt;

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

一个更简单的替代方法可能是使用以下某些运算符存在的文本别名:gt(>),lt(<),ge(>=),le(<=),not(!)。也是eq(==),neq/ne(!=)。

4.11 条件表达式

条件表达式旨在仅根据评估条件的结果来评估两个表达式之一(本身就是另一个表达式)。

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

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

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

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

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

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

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

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

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

让我们在用户 Profile 页面中看到它的实际效果:

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

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

<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 The No-Operation token

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

此令牌背后的想法是指定表达式的期望结果是“不执行任何操作”,即完全像处理属性(例如th:text)根本不存在一样。

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

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

…我们可以直接使用*'no user authenticated'*作为原型文本,这样从设计的角度来看,代码既简洁又通用:

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

4.14 数据转换/格式化

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

它基本上是这样的:

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

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

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

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

Note

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

4.15 Preprocessing

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

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

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

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

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

…和Messages_es.properties equivalent

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

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

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

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

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

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

5 设置属性值

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

5.1 设置任何属性的值

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

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

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

然后 Importingth:attr属性,并更改其设置的标签属性值的能力:

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

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

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

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

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

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

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

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

5.2 将值设置为特定属性

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

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

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

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

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

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

看起来好多了!让我们尝试对form标记中的action属性执行相同的操作:

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

您还记得我们以前在home.html中放入的th:href吗?它们是完全一样的属性:

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

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

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

5.3 一次设置多个值

有两个相当特殊的属性,分别称为th:alt-titleth:lang-xmllang,可用于将两个属性同时设置为相同的值。特别:

  • th:alt-title将设置alttitle

  • th:lang-xmllang将设置langxml:lang

对于我们的 GTVG 主页,这将使我们可以替换为:

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

…或与此等效的:

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

…with this:

<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 类或* style *的片段,而不会覆盖现有的:

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

(不必担心th:each属性.这是一个* iterating 属性*,我们将在后面讨论.)

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:asyncth:autofocusth:autoplay
th:checkedth:controlsth:declare
th:defaultth:deferth:disabled
th:formnovalidateth:hiddenth:ismap
th:loopth:multipleth:novalidate
th:nowrapth:openth:pubdate
th:readonlyth:requiredth:reversed
th:scopedth:seamlessth:selected

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

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

所以像这样:

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

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

6.1 迭代基础

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

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

Using 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 变量*。

请注意,prod iter 变量的作用域为<tr>元素,这意味着它可用于<td>之类的内部标记。

Iterable values

java.util.List类不是 Thymeleaf 中唯一可用于迭代的值。有相当完整的一组对象,它们被th:each属性视为“可迭代”:

  • 任何实现java.util.Iterable的对象

  • 任何实现java.util.Enumeration的对象。

  • 实现java.util.Iterator的任何对象,其值将由迭代器返回,而无需在内存中缓存所有值。

  • 任何实现java.util.Map的对象。迭代 Map 时,迭代变量将属于java.util.Map.Entry类。

  • Any array.

  • 任何其他对象都将被视为包含该对象本身的单值列表。

6.2 保持迭代状态

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

状态变量在th:each属性中定义,并包含以下数据:

  • 当前的迭代索引,从 0 开始。这是index属性。

  • 从 1 开始的当前* iteration index *。这是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 通过延迟检索数据进行优化

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

Note

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

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

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

在不了解其* lazyness *的情况下,可以在以下代码中使用此变量:

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

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

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

7 条件评估

7.1 简单条件:“if” 和 “unless”

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

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

为此,我们将使用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>

这将创建指向 Comment 页面(URL /product/comments)的链接,该链接的prodId参数设置为产品的id,但前提是该产品有任何 Comment。

让我们看一下结果标记:

<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具有逆属性th:unless,我们可以在前面的示例中使用它,而不是在 OGNL 表达式内使用not

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

7.2 switch 语句

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

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

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

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

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

8 模板布局

8.1 包括模板片段

定义和引用片段

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

为此,Thymeleaf 需要我们定义这些要包含的部分“片段”,可以使用th:fragment属性来完成。

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

<!DOCTYPE html>

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

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

上面的代码定义了一个名为copy的片段,我们可以使用th:insertth:replace属性之一以及th:include轻松地将其包含在我们的主页中,尽管自 Thymeleaf 3.0 起不再建议使用它:

<body>

  ...

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

请注意,th:insert期望一个* fragment 表达式*(~{...}),它是一个导致片段的表达式。但是,在上面的示例中,它是一个非复杂的* fragment 表达式*,(~{})包围是完全可选的,因此上述代码等效于:

<body>

  ...

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

片段规范语法

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

  • "~{templatename::selector}"包括将指定的标记 selectors 应用于名为templatename的模板所产生的片段。请注意,selector只能是片段名称,因此您可以指定与~{templatename::fragmentname}一样简单的名称,例如上面的~{footer :: copy}

Note

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

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

Note

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

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

上面示例中的templatenameselector都可以是功能齐全的表达式(甚至是条件表达式!),例如:

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

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

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

Note

这种片段处理方法的一大优势是,您可以将片段写在浏览器可以完美显示的页面中,并具有完整甚至有效的标记结构,同时仍保留使 Thymeleaf 将其包含在其他模板中的功能。

引用没有 th:fragment 的片段

借助标记 selectors 的功能,我们可以包含不使用任何th:fragment属性的片段。甚至可能是完全不了解 Thymeleaf 的来自不同应用程序的标记代码:

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

我们可以使用上面的片段,只需通过其id属性引用它即可,类似于 CSSselectors:

<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:includeth:insert类似,但是不插入片段,而是仅插入该片段的* content *。

因此,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:insertth:replace调用片段:

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

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

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

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

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

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

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

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

这等效于th:replaceth:with的组合:

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

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

th:声明模板内 assert

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

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

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

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

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

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

这使我们能够以某种方式创建片段,从而可以通过调用模板中的标记来“丰富”片段,从而产生非常灵活的 模板布局机制

请注意以下片段中titlelinks变量的使用:

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

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

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

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

</head>

我们现在可以将此片段称为:

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

  <title>Awesome - Main</title>

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

</head>
...

…结果将使用调用模板中的实际<title><link>标记作为titlelinks变量的值,从而导致在插入过程中对片段进行自定义:

...
<head>

  <title>Awesome - Main</title>

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

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

</head>
...

使用空片段

特殊的片段表达式空片段(~{})可用于指定无标记。使用前面的示例:

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

  <title>Awesome - Main</title>

</head>
...

请注意如何将片段的第二个参数(links)设置为空片段,因此对于<th:block th:replace="${links}" />块不会写入任何内容:

...
<head>

  <title>Awesome - Main</title>

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

</head>
...

使用The No-Operation token

如果我们只想让我们的片段使用其当前标记作为默认值,那么 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>
...

有条件的片段高级插入

“空片段”和“The No-Operation token”都可以使用,这使我们能够以非常容易和优雅的方式有条件地插入片段。

例如,我们可以这样做,以便在用户是 Management 员的情况下仅插入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标志检查模板资源是否存在,我们可以使用片段本身的存在作为* default *操作中的条件:

...
<!-- 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 应该删除其他两行。

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

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
    <th>COMMENTS</th>
  </tr>
  <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'">
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
    <td>
      <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
      <a href="comments.html" 
         th:href="@{/product/comments(prodId=${prod.id})}" 
         th:unless="${#lists.isEmpty(prod.comments)}">view</a>
    </td>
  </tr>
  <tr class="odd" th:remove="all">
    <td>Blue Lettuce</td>
    <td>9.55</td>
    <td>no</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr th:remove="all">
    <td>Mild Cinnamon</td>
    <td>1.99</td>
    <td>yes</td>
    <td>
      <span>3</span> comment/s
      <a href="comments.html">view</a>
    </td>
  </tr>
</table>

处理后,所有内容将再次恢复原样:

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
    <th>COMMENTS</th>
  </tr>
  <tr>
    <td>Fresh Sweet Basil</td>
    <td>4.99</td>
    <td>yes</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr class="odd">
    <td>Italian Tomato</td>
    <td>1.25</td>
    <td>no</td>
    <td>
      <span>2</span> comment/s
      <a href="/gtvg/product/comments?prodId=2">view</a>
    </td>
  </tr>
  <tr>
    <td>Yellow Bell Pepper</td>
    <td>2.50</td>
    <td>yes</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr class="odd">
    <td>Old Cheddar</td>
    <td>18.75</td>
    <td>yes</td>
    <td>
      <span>1</span> comment/s
      <a href="/gtvg/product/comments?prodId=4">view</a>
    </td>
  </tr>
</table>

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

  • all:删除包含标签及其所有子标签。

  • body:不要删除包含标签,而是删除其所有子标签。

  • tag:删除包含标签,但不要删除其子标签。

  • all-but-first:除去第一个标签之外的所有包含标签的子标签。

  • none:什么都不做。该值对于动态评估很有用。

all-but-first值有什么用?原型制作时,它可以让我们节省一些th:remove="all"

<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 标准表达式*,只要它返回允许的 String 值之一(alltagbodyall-but-firstnone)即可。

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

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

另请注意,th:removenull视为none的同义词,因此以下内容与上面的示例相同:

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

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

8.5 布局继承

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

<!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 的片段,其中带有* title content *作为参数。在下面的示例中,这两者都将在页面上被继承的片段表达式所替代。

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

在此文件中,html标签将由* layout *替换,但是在布局titlecontent中将分别由titlesection块替换。

如果需要,布局可以由几个片段组成,例如* 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变量将作为局部变量创建并添加到来自上下文的变量 Map 中,以便它可以与上下文中声明的任何其他变量一起用于求值,但只能在包含变量的范围内<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:withth:text具有更高的precedence,我们可以在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 标准没有给标记中的属性写入 Sequences 赋予任何含义,必须在属性本身中构建一个* precedence *机制,以确保它可以按预期工作。

因此,所有 Thymeleaf 属性都定义了数字优先级,从而确定了它们在标签中执行的 Sequences。该命令是:

OrderFeatureAttributes
1Fragment inclusionth:insert

th:replace
2片段迭代th:each
3条件评估th:if
th:unless
th:switch
th:case
4局部变量定义th:object
th:with
5常规属性修改th:attr
th:attrprepend
th:attrappend
6特定属性修改th:value
th:href
th:src
...
7文本(标签正文修改)th:text
th:utext
8片段规范th:fragment
9碎片去除th:remove

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

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

11Comments 和块

11.1. 标准 HTML/XMLComments

标准的 HTML/XMLComments<!-- ... -->可以在 Thymeleaf 模板中的任何位置使用。这些 Comments 中的所有内容都不会被 Thymeleaf 处理,并将逐字复制到结果中:

<!-- User info follows -->
<div th:text="${...}">
  ...
</div>

11.2. Thymeleaf 解析器级 Comments 块

解析器级 Comments 块是将在 Thymeleaf 解析时从模板中简单删除的代码。他们看起来像这样:

<!--/* This code will be removed at Thymeleaf parsing time! */-->

Thymeleaf 将删除<!--/**/-->之间的所有内容,因此,当模板静态打开时,这些 Comments 块也可用于显示代码,因为知道 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 仅原型 Comments 块

Thymeleaf 允许定义特殊 Comments 块的定义,当模板以静态方式打开(即作为原型)时,标记为 Comments,但 Thymeleaf 在执行模板时将其视为普通标记。

<span>hello!</span>
<!--/*/
  <div th:text="${...}">
    ...
  </div>
/*/-->
<span>goodbye!</span>

Thymeleaf 的解析系统将仅删除<!--/*//*/-->标记,但不会删除其内容,因此不会对其进行 Comments。因此,在执行模板时,Thymeleaf 实际上会看到以下内容:

<span>hello!</span>
 
  <div th:text="${...}">
    ...
  </div>
 
<span>goodbye!</span>

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

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>

与仅原型 Comments 块结合使用时特别有用:

<table>
    <!--/*/ <th:block th:each="user : ${users}"> /*/-->
    <tr>
        <td th:text="${user.login}">...</td>
        <td th:text="${user.name}">...</td>
    </tr>
    <tr>
        <td colspan="2" th:text="${user.address}">...</td>
    </tr>
    <!--/*/ </th:block> /*/-->
</table>

请注意,此解决方案如何使模板成为有效的 HTML(无需在<table>内添加禁止的<div>块),并且当在浏览器中作为原型静态打开时仍然可以正常使用!

12 Inlining

12.1 表达式内联

尽管标准方言允许我们使用标记属性来执行几乎所有操作,但是在某些情况下,我们更喜欢直接将表达式写到 HTML 文本中。例如,我们可能更喜欢这样编写:

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

…代替此:

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

[[...]][(...)]之间的表达式在 Thymeleaf 中被视为“内联表达式”,并且在它们内部,我们可以使用在th:textth:utext属性中也有效的任何类型的表达式。

请注意,虽然[[...]]对应于th:text(即结果将* HTML 转义*),但[(...)]则对应于th:utext并且不会执行任何 HTML 转义。因此,使用msg = 'This is <b>great!</b>'这样的变量,给出以下片段:

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

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

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

而如果像这样逃脱了:

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

结果将转义为 HTML:

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

请注意,我们标记中每个标签的正文(默认不是标签本身)中的“文本内联默认为活动状态”,因此我们无需执行任何操作即可启用它。

内联与自然模板

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

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

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

Hello, Sebastian!

…并使用它…

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

……在设计实用性方面非常明确。

Disabling inlining

不过,可以禁用此机制,因为实际上在某些情况下,我们确实希望输出[[...]][(...)]序列而不将其内容作为表达式处理。为此,我们将使用th:inline="none"

<p th:inline="none">A double array looks like this: [[1, 2, 3], [4, 5]]!</p>

这将导致:

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

12.2 文本内联

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

文本内联不仅使我们能够使用与刚才看到的相同的内联表达式,而且实际上可以像在TEXT模板模式下处理模板一样处理标签主体,这使我们能够执行基于文本的模板逻辑(而不是仅输出表达式)。

我们将在下一章有关“文本模板模式”的文章中看到更多有关此内容的信息。

12.3 JavaScript 内联

JavaScript 内联允许在HTML模板模式下处理的模板中更好地集成 JavaScript <script>块。

与* text inlining 一样,实际上等同于将脚本内容当作JAVASCRIPT模板模式下的模板一样进行处理,因此 text 模板模式*(请参阅下一章)的所有功能将近在咫尺。但是,在本节中,我们将重点介绍如何使用它将 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 对其内容进行转义,以便将表达式结果输出为“格式正确的 JavaScriptLiterals”。

第二,之所以会这样,是因为我们将${session.user.name}表达式输出为 转义 ,即使用双括号表达式[[${session.user.name}]]。相反,如果我们使用* unscaped *,例如:

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

结果如下所示:

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

…这是格式错误的 JavaScript 代码。但是,如果我们通过附加内联表达式来构建脚本的某些部分,则可能需要输出未转义的内容,因此手头有此工具是件好事。

JavaScript 自然模板

提到的 JavaScript 内联机制的“智能”远远超出了仅应用特定于 JavaScript 的转义并将表达式结果输出为有效 Literals 的范围。

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

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

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

<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

  • Bean(具有* getter setter *方法的对象)

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

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

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

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

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

此 JS 序列化机制的默认实现将在 Classpath 中查找Jackson library,如果存在,将使用它。如果没有,它将应用内置的序列化机制,该机制可以满足大多数方案的需求并产生相似的结果(但灵 Active 较差)。

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>标签以静态和动态方式工作,即通过将内联表达式包装在 Comments 中来作为 CSS 自然模板 。看到:

<style th:inline="css">
    .main\ elems {
      text-align: /*[[${align}]]*/ left;
    }
</style>

13 文本模板模式

13.1Literals 语法

Thymeleaf 的三个“模板模式”被认为是“文本的”:TEXTJAVASCRIPTCSS。这将它们与标记模板模式HTMLXML区别开来。

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

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

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 页面调用该结果。请注意,这不是 HTML 模板中的<script>块,而是单独作为模板处理的.js文件:

var greeter = function() {

    var username = [[${session.user.name}]];

    [# th:each="salut : ${salutations}"]    
      alert([[${salut}]] + " " + username);
    [/]

};

执行后,其结果可能类似于:

var greeter = function() {

    var username = "Bertrand \"Crunchy\" Pear";

      alert("Hello" + " " + username);
      alert("Ol\u00E1" + " " + username);
      alert("Hola" + " " + username);

};

转义的元素属性

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

  • TEXT模板模式下的属性将为* HTML-unscaped *。

  • JAVASCRIPT模板模式下的属性为* JavaScript-unscaped *。

  • CSS模板模式下的属性为* CSS-unscaped *。

因此,这在TEXT-模式模板(请注意&gt;)中完全可以:

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

当然,在一个“真实文本”模板中&lt;是没有意义的,但是如果我们正在处理一个包含上面代码的th:inline="text"块的 HTML 模板,并且要确保我们的浏览器不会采用该模板,则是个好主意。静态打开文件作为原型时,打开标记的名称为<user.age

13.2 Extensibility

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

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

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

JAVASCRIPTCSS模板模式(不适用于TEXT)允许在特殊 Comments 语法/*[+...+]*/之间包含代码,以便 Thymeleaf 在处理模板时会自动取消 Comments 此类代码:

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() {
...

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

var x = 23;

/*[+

var msg  = "Hello, " + [[${session.user.name}]];

+]*/

var f = function() {
...

13.4 文本解析器级别的 Comments 块:删除代码

以类似于仅原型 Comments 块的方式,所有三种文本模板模式(TEXTJAVASCRIPTCSS)都可以指示 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/CSSComments 中的可能性,例如:

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

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

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

实际上,可以在 Comments 中使用相同的“技巧”将内联表达式括起来,以用于整个文本模式语法:

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

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

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

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

但是请注意,在 Comments 中包装元素并不会像内联输出表达式那样清除它们所在的行(在找到;之前向右移动)。该行为仅保留给内联输出表达式。

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

14 我们的杂货店还有更多页面

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

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

14.1 订单清单

首先创建一个订单列表页面/WEB-INF/templates/order/list.html

<!DOCTYPE html>

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

  <head>

    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
  </head>

  <body>

    <h1>Order list</h1>
  
    <table>
      <tr>
        <th>DATE</th>
        <th>CUSTOMER</th>
        <th>TOTAL</th>
        <th></th>
      </tr>
      <tr th:each="o : ${orders}" th:class="${oStat.odd}? 'odd'">
        <td th:text="${#calendars.format(o.date,'dd/MMM/yyyy')}">13 jan 2011</td>
        <td th:text="${o.customer.name}">Frederic Tomato</td>
        <td th:text="${#aggregates.sum(o.orderLines.{purchasePrice * amount})}">23.32</td>
        <td>
          <a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
        </td>
      </tr>
    </table>
  
    <p>
      <a href="../home.html" th:href="@{/}">Return to home</a>
    </p>
    
  </body>
  
</html>

除了这点 OGNL 魔术外,这里没有什么可以令我们感到惊讶的:

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

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

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

14.2 订单详细信息

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

<!DOCTYPE html>

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

  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
  </head>

  <body th:object="${order}">

    <h1>Order details</h1>

    <div>
      <p><b>Code:</b> <span th:text="*{id}">99</span></p>
      <p>
        <b>Date:</b>
        <span th:text="*{#calendars.format(date,'dd MMM yyyy')}">13 jan 2011</span>
      </p>
    </div>

    <h2>Customer</h2>

    <div th:object="*{customer}">
      <p><b>Name:</b> <span th:text="*{name}">Frederic Tomato</span></p>
      <p>
        <b>Since:</b>
        <span th:text="*{#calendars.format(customerSince,'dd MMM yyyy')}">1 jan 2011</span>
      </p>
    </div>
  
    <h2>Products</h2>
  
    <table>
      <tr>
        <th>PRODUCT</th>
        <th>AMOUNT</th>
        <th>PURCHASE PRICE</th>
      </tr>
      <tr th:each="ol,row : *{orderLines}" th:class="${row.odd}? 'odd'">
        <td th:text="${ol.product.name}">Strawberries</td>
        <td th:text="${ol.amount}" class="number">3</td>
        <td th:text="${ol.purchasePrice}" class="number">23.32</td>
      </tr>
    </table>

    <div>
      <b>TOTAL:</b>
      <span th:text="*{#aggregates.sum(orderLines.{purchasePrice * amount})}">35.23</span>
    </div>
  
    <p>
      <a href="list.html" th:href="@{/order/list}">Return to order list</a>
    </p>

  </body>
  
</html>

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

<body th:object="${order}">

  ...

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

  ...
</body>

…使*{name}等效于:

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

15 有关配置的更多信息

15.1 模板解析器

对于我们的 Good Thymes 虚拟杂货店,我们选择了一个名为ServletContextTemplateResolverITemplateResolver实现,该实现使我们能够从 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(甚至是 nonlocal 的 URL),例如:
return (new URL(template)).openStream();
  • org.thymeleaf.templateresolver.StringTemplateResolver,它直接将模板解析为String被指定为template(或* template name *,在这种情况下显然不仅仅是一个简单的名称):
return new StringReader(templateName);

ITemplateResolver的所有预 Binding 实现均允许使用相同的配置参数集,其中包括:

  • 前缀和后缀(如前所述):
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
  • 模板别名允许使用与文件名不直接对应的模板名。如果后缀/前缀和别名都存在,则别名将在前缀/后缀之前应用:
templateResolver.addTemplateAlias("adminHome","profiles/admin/home");
templateResolver.setTemplateAliases(aliasesMap);
  • 读取模板时要应用的编码:
templateResolver.setCharacterEncoding("UTF-8");
  • 使用的模板模式:
// Default is HTML
templateResolver.setTemplateMode("XML");
  • 模板缓存的默认模式,以及用于定义特定模板是否可缓存的模式:
// Default is true
templateResolver.setCacheable(false);
templateResolver.getCacheablePatternSpec().addPattern("/users/*");
  • 源自此模板解析器的已解析模板缓存条目的 TTL(以毫秒为单位)。如果未设置,则从缓存中删除条目的唯一方法是超过缓存的最大大小(最旧的条目将被删除)。
// Default is no TTL (only cache size exceeded would remove entries)
templateResolver.setCacheTTLMs(60000L);

Note

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

链接模板解析器

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

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实现的特定功能。请注意,并非所有实现都能够在解析之前确定模板的存在,因此始终可以将模板视为“可解析” *并 break 解析链(不允许其他解析器检查同一模板),但是无法读取真实资源。

Thymeleaf 核心随附的所有ITemplateResolver实现都包括一种机制,使我们可以让解析程序在考虑可解析之前,先检查资源是否存在。它是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对象。

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

Note

Thymeleaf Spring 集成软件包默认情况下提供IMessageResolver实现,该实现使用标准 Spring 方法通过使用在 Spring Application Context 声明的MessageSource bean 来检索外部消息。

标准邮件解析器

那么StandardMessageResolver如何查找在特定模板上请求的消息?

如果模板名称为home且位于/WEB-INF/templates/home.html,并且请求的语言环境为gl_ES,则此解析器将按以下 Sequences 在以下文件中查找消息:

  • /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);

为什么我们要拥有多个消息解析器?出于与模板解析器相同的原因:Order 了消息解析器,如果第一个无法解析特定的消息,则将询问第二个,然后询问第三个,依此类推。

15.3 转换服务

通过* double-brace 语法(${{...}})使我们能够执行数据转换和格式化操作的转换服务*实际上是标准方言的功能,而不是 Thymeleaf 模板引擎本身的功能。

因此,配置它的方法是直接将我们自定义的IStandardConversionService接口设置直接设置到正在模板引擎中配置的StandardDialect实例中。喜欢:

IStandardConversionService customConversionService = ...

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

templateEngine.setDialect(dialect);

Note

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

15.4 Logging

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

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

Thymeleaf 类将记录TRACEDEBUGINFO级别的信息,具体取决于我们希望的详细程度,除常规记录外,它将使用与 TemplateEngine 类关联的三个特殊 Logger,我们可以分别针对不同目的进行配置:

  • org.thymeleaf.TemplateEngine.CONFIG将在初始化期间输出库的详细配置。

  • org.thymeleaf.TemplateEngine.TIMER将输出有关处理每个模板所需时间的信息(可用于基准测试!)

  • org.thymeleaf.TemplateEngine.cache是一组 Logger 的前缀,这些 Logger 输出有关缓存的特定信息。尽管缓存 Logger 的名称可由用户配置,因此可以更改,但默认情况下为:

  • org.thymeleaf.TemplateEngine.cache.TEMPLATE_CACHE

    • org.thymeleaf.TemplateEngine.cache.EXPRESSION_CACHE

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

log4j.logger.org.thymeleaf=DEBUG
log4j.logger.org.thymeleaf.TemplateEngine.CONFIG=TRACE
log4j.logger.org.thymeleaf.TemplateEngine.TIMER=TRACE
log4j.logger.org.thymeleaf.TemplateEngine.cache.TEMPLATE_CACHE=TRACE

16 模板缓存

Thymeleaf 的工作要归功于一组解析器(用于标记和文本),该解析器将模板解析为事件序列(打开标签,文本,关闭标签,Comments 等)和一系列处理器(每种需要一种行为)应用–修改模板解析的事件序列,以便通过将原始模板与我们的数据结合来创建我们期望的结果。

默认情况下,它还包括存储已解析模板的缓存;在处理模板文件之前读取和解析模板文件所导致的事件 Sequences。在 Web 应用程序中工作时,此功能特别有用,它基于以下概念:

  • Importing/输出几乎始终是所有应用程序中最慢的部分。相比之下,内存中处理非常快。

  • 克隆现有的内存中事件序列总是比读取模板文件,对其进行解析并为其创建新的事件序列要快得多。

  • Web 应用程序通常只有几十个模板。

  • 模板文件大小不一,在应用程序运行时不会被修改。

所有这些都导致了这样的想法,即在不浪费大量内存的情况下在 Web 应用程序中缓存最常用的模板是可行的,并且这将节省大量时间,而这些时间将花费在少量文件的 Importing/输出操作上实际上,这永远不会改变。

以及我们如何控制此缓存?首先,我们已经了解到可以在模板解析器上启用或禁用它,甚至只对特定模板起作用:

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

另外,我们可以通过构建自己的* Cache Manager *对象来修改其配置,该对象可以是默认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模板模式下创建“完全无逻辑的标记模板”。

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

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

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

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

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

现在,通过创建这样的其他home.th.xml文件,将home.html模板转换为 Thymeleaf 模板:

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

在这里,我们可以看到thlogic块中有很多<attr>标签。那些<attr>标记通过其sel属性在原始模板的节点上执行属性注入,这些属性包含 Thymeleaf 标记 selectors(实际上* AttoParser 标记 selectors*)。

另请注意,可以嵌套<attr>标签,以便其 selectors 被“附加”。例如,上面的sel="/tr[0]"将被处理为sel="#usersTable/tr[0]"。用户名<td>的 selectors 将被处理为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,因此从设计的角度来看,具有更好的可维护性。

当然,仍然需要设计人员或开发人员之间的某些Contract –用户<table>将需要id="usersTable" –这一事实,但是在许多情况下,纯 HTML 模板将成为设计团队和开发团队之间更好的交流工件。

17.2 配置解耦的模板

启用解耦的模板

默认情况下,不会期望每个模板都具有解耦逻辑。取而代之的是,配置的模板解析器(ITemplateResolver的实现)将需要使用“解耦逻辑”将它们解析为的模板专门标记为*。

StringTemplateResolver(不允许解耦逻辑)外,所有其他其他现成的ITemplateResolver实现都将提供一个名为useDecoupledLogic的标志,该标志会将该解析器解析的所有模板标记为可能存在其全部或部分逻辑单独的资源:

final ServletContextTemplateResolver templateResolver = 
        new ServletContextTemplateResolver(servletContext);
...
templateResolver.setUseDecoupledLogic(true);

混合耦合和解耦逻辑

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

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

17.3 th:ref 属性

th:ref只是标记属性。从处理的角度来看,它什么也没做,只是在处理模板时消失,但是它的作用在于它充当* markup reference 的事实,即可以像 一样通过 markupselectors的名称来解析它。标签名称片段(th:fragment)。

因此,如果我们有一个 selectors,例如:

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

这将匹配:

  • 任何<whatever>个标记。

  • 具有th:fragment="whatever"属性的所有标签。

  • 具有th:ref="whatever"属性的所有标签。

th:ref相对于使用纯 HTML id属性有何优势?仅仅是我们可能不想在标签中添加太多idclass属性来充当逻辑锚,这最终可能污染我们的输出。

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

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

17.4 解耦的模板对性能的影响

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

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

这样最大的优势?将模板配置为要缓存时,它将缓存已包含注入属性的模板。因此,一旦缓存了模板,将解耦模板用于可缓存模板的开销将绝对为

17.5 解耦逻辑的解析

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

该标准实现有什么作用?

  • 首先,它将prefixsuffix应用于模板资源的“基本名称”(通过ITemplateResource#getBaseName()方法获得)。前缀和后缀都可以配置,默认情况下,前缀为空,后缀为.th.xml

  • 其次,它要求模板资源通过其ITemplateResource#relative(String relativeLocation)方法解析具有所计算名称的“相对资源”。

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

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

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

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

Base objects

  • #ctx :上下文对象。 org.thymeleaf.context.IContextorg.thymeleaf.context.IWebContext的实现取决于我们的环境(独立或网络)。

注意#vars#root是同一对象的同义词,但建议使用#ctx

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

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

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

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

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

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

Note

请注意,这些不是* context 对象*,而是作为变量添加到上下文中的 Map,因此我们无需#即可访问它们。它们以某种方式充当* namespaces *。

  • param :用于检索请求参数。 ${param.foo}是具有foo请求参数值的String[],因此${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 parameters *相对),因为所有请求属性都作为变量自动添加到上下文根目录中:

${myRequestAttribute}

Web 上下文对象

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

  • #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:表达式 Util 对象

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 :Util 方法,用于获取变量表达式中的外部化消息,其方式与使用#{...}语法获得消息的方式相同。
/*
 * ======================================================================
 * 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)}

URIs/URLs

  • #uris :在 Thymeleaf 标准表达式内执行 URI/URL 操作(尤其是转义/不转义)的 Util 对象。
/*
 * ======================================================================
 * 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 :Util 对象,允许在模板的任何位置执行* Conversion Service *:
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Conversions
 * ======================================================================
 */

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

Dates

  • #datesjava.util.Date个对象的 Util 方法:
/*
 * ======================================================================
 * 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

  • #stringsString个对象的 Util 方法:
/*
 * ======================================================================
 * 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 :Map 的实用方法
/*
 * ======================================================================
 * 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 :在数组或集合上创建聚合的 Util 方法
/*
 * ======================================================================
 * 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 :处理id属性的 Util 方法,这些属性可能会重复(例如,作为迭代的结果)。
/*
 * ======================================================================
 * 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:标记 selectors 语法

Thymeleaf 的标记 selectors 直接从 Thymeleaf 的解析库AttoParser借用。

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

例如,以下 selectors 将在标记内的每个位置选择类别为content的每个<div>(请注意,这样做并不那么简洁,请 continue 阅读以了解原因):

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

基本语法包括:

  • /x表示名称为 x 的当前节点的直接子代。

  • //x表示任意深度的名称为 x 的当前节点的子代。

  • x[@z="v"]表示名称为 x 的元素和名为 z 的属性,其值为“ v”。

  • x[@z1="v1" and @z2="v2"]表示名称为 x 的元素以及属性 z1 和 z2 的值分别为“ v1”和“ v2”。

  • x[i]表示名称 x 位于其同级兄弟中编号 i 的元素。

  • x[@z="v"][i]表示元素 x 的名称,属性 z 的值为“ v”,并且在与该条件匹配的同级元素中位于第 i 个位置。

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

  • x//x完全等效(在任何深度级别搜索名称或参考x的元素,* reference *为th:refth:fragment属性)。

  • selectors 也可以不带元素名称/引用,只要它们包含参数说明即可。因此[@class='oneclass']是一个有效的 selectors,它会查找具有值为"oneclass"的 class 属性的任何元素(标签)。

高级属性选择功能:

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

  • 既可以以@(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 的 selectors:

  • x.oneclass等效于x[class='oneclass']

  • .oneclass等效于[class='oneclass']

  • x#oneid等效于x[id='oneid']

  • #oneid等效于[id='oneid']

  • x%oneref表示具有th:ref="oneref"th:fragment="oneref"属性的<x>个标记。

  • %oneref表示所有具有th:ref="oneref"th:fragment="oneref"属性的标签。注意,这实际上等效于oneref,因为可以使用引用代替元素名称。

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

因此,上面的标记 selectors 表达式:

<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引用)。

多值类匹配

标记 selectors 将 class 属性理解为 multivalued ,因此即使元素具有多个 class 值,也允许在该属性上应用 selectors。

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