Thymeleaf3.0内容

Thymeleaf简介

什么是Thymeleaf

Thymeleaf是网站或者独立应用程序的新式的服务端java模板引擎,可以执行HTML,XML,JavaScript,CSS甚至纯文本模板。

Thymeleaf的主要目标是提供一个以优雅的高可维护型的方式创建模板,为了达到这个目的,他建立了自然模板的概念,以一种不影响模板设计原型的方式将逻辑注入到模板文件中,可以显著的减少设计和开发之间的沟通成本。

在需要的情况下,Thymeleaf可以创建一个各种校验模式的模板(尤其是基于HTML5)

什么样的模板可以使用Thymeleaf处理

Thymeleaf 允许使用六种模板,每个被称为一种模板模式:

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

这其中有两种标记语言模板模式(HTML,XML),三种文本语言模板模式(TEXT,JAVASCRIPT,CSS),和一种无操作模板模式(RAW)

其中:
HTML模板模式可以使用任何HTML输入,包括HTML5,HTML4或XHTML,全验证或无验证都将可以被执行,模板中的代码和结构将被尽最大可能性的解析输出。

XML模板模式将被用来执行XML的输入,在这种模式下,如果被要求严格验证,则比如没有闭合标签,属性没有引号等,都将输出异常,注意一点是,无验证(通过DTD或XMLSchema)仍然是可以执行的。

文本模板模式将可以使用一种非标记性的特殊语法。它可能是一个电子邮件的模板或者文本模板等模板文件。注意:HTML模式或XML模式也可以使用文本模板模式处理,但在这种情况下,他们不会被解析为标记语言,每个标记,DOCTYPE,comment等都将视为单纯的文本。

JavaScript模板模式可以使用Thymeleaf处理Js文件。这意味着它能够在JS文件中以HTML文件同样的方式使用数据模型。但它还整合了一些JS的专有属性,如一些特定编码和脚本语言。JavaScript模板是一种支持特殊语法的文本模板模式。

CSS模板模式和JavaScript模板模式类似,即可以使用Thymeleaf处理CSS文件,它也是用支持特殊语法的文本模板模式。

RAW模板模式是一种非执行模板,他用了处理一些保持原样的资源(如文件,某Url的响应等),比如,在一些格式之外,可能会有一些信息需要原封不动的展现出来,这些应该明确的告知Thymeleaf不要讲它们进行执行。

方言:标准方言

Thymeleaf 是一种可扩展的模板引擎(其实应该称呼为模板引擎框架),它允许您自由的定义制作一个可以精确到任何程度的模板。

一个对象,运用一些逻辑(如标签,文本,注释,或仅仅是一个占位符),被称为处理器,这些处理器加一些额外的东西,被称为方言。其中Thymeleaf的核心库提供了一个一目了然的方言被称为标准方言,他应该能够满足大多数用户的需求。

应明确一点就是,方言实际上可以是没有处理器,完全有其他的类型的东西注册,但处理器是一种最常见的使用方式。

本教程即使用标准方言,你可以在下面页中了解这个方言的每一个属性和语法功能的定义,即使他没有明确提及。

当然,如果用户想要定义自己的处理逻辑,和使用库中的各种先进功能,那么,可以定义自己的方言(甚至扩展标准方言),而模板引擎可以一次配置多种方言。

官方thymeleaf-spring3和thymeleaf-spring4集成包都定义了另一种方言被称为“String标准方言”,大多数相当于标准方言,但有一小部分为了适应Spring框架的某些功能(如利用Spring的EL表达式而不是Thymeleaf标准的OGNL)所以,如果你是一个Spring mvc用户,请你不要在浪费时间抓紧学习,因为那你在这里学习的所有东西,都讲在你的Spring项目中使用

Thymeleaf标准方言可以在任何模式的模板中使用,尤其适合面向web的模板模式(如HTML5和XHTML),除了HTML5,他可以支持和验证一下的XHTML格式:XHTML 1.0 Transitional, XHTML 1.0 Strict, XHTML 1.0, 和 XHTML 1.1.

标准方言的大多数处理器都是属性处理器。他们在浏览器处理HTML模板文件之前,他会简单处理额外的属性,如,在jsp中使用标签库可以包括代码段,但代码段浏览器并不会直接显示一样:

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

Thymeleaf中,可以实现同样的功能:

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

这个会在浏览器中被正确的显示,同时,也容许我们指定一个默认值(可选)("niufennan"),当静态文件在浏览器中打开的时候,显示的是Thymeleaf使用模板中${user.name}的值对value中的值进行了替换的结果。

这将可以使设计人员和开发人员使用几乎相同的模板文件,并且可以使用极少的工作,就使一个静态文件转换为开发模板文件。有这样能力的模板通常称之为自然模板

古泰虚拟商店

当前及未来示例代码下载地点

商店站点

为了更好的说明Thymeleaf的各种概念,教程所使用的Demo可以从项目网站下载。

这个项目是一个假想的购物网站,将提供足够的场景来演示Thymeleaf的各种不同的特点。

这个购物网站需要一个非常简单的实体模型,产品(Products)销售给客户(Customers),同时创建订单(Orders),并且,还需要我们管理用户对产品的评论(Coomments)

模型示意图

一个简单的服务层:

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 {
           
        //静态资源不使用框架
        if(request.getRequestURI().startsWith("/css")||
                request.getRequestURI().startsWith("/images")||
                request.getRequestURI().startsWith("/favicon")
                )
        {
            return false;
        }
        /*
         * 根据controller/url的映射规则,获取将要执行的控制权的request
         * 如果没有控制权可用,返回false,让其他的filter或者servlet来执行
         */
        IGTVGController controller = application.getTemplateEngine().resolveControllerForRequest(request);
        if (controller == null) {
            return false;
        }
        /*
         * 获取 TemplateEngine 实例.
         */
        ITemplateEngine templateEngine = application.getTemplateEngine();
            
        /*
         * 写 response headers
         */
        response.setContentType("text/html;charset=UTF-8");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);

        /*
         * 执行控制权并处理模板
         * 将结果写入response
         */
        controller.process(
                request, response, this.servletContext, templateEngine);

        return true;
            
    } catch (Exception e) {
        throw new ServletException(e);
    }
}    

还有IGTVGController 接口

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

现在要做的就是IGTVGController接口的实现,主要实现了从服务中检索数据和处理模板使用的ITemplateEngine 对象

最后,他的效果图如下
效果图
但是,首先让我们看看模板引擎是如何初始化的。

创建并且配置一个模板引擎

在过滤器process方法中加入如下的代码:

ITemplateEngine templateEngine = application.getTemplateEngine();

这意味着GTVGApplication类中在加载的时候创建和配置了Thymeleaf启动应用的最重要对象:模板引擎实例(ITemplateEngine接口的实现)

我们的org.thymeleaf.TemplateEngine对象的初始化就像是这样:

public class GTVGApplication {


    ...
    private static TemplateEngine templateEngine;
    ...
    
    public GTVGApplication(ServletContext servletContext) {
        
        ServletContextTemplateResolver templateResolver = 
            new ServletContextTemplateResolver(servletContext);
        //有String类型重载
        templateResolver.setTemplateMode(TemplateMode.HTML);
        // 这段将把 "home" 转换为 "/WEB-INF/templates/home.html"
        templateResolver.setPrefix("/WEB-INF/templates/");
        templateResolver.setSuffix(".html");
        //模板的缓存的生存时间默认为1小时,如果没有设置,将被缓存到LRU(缓存淘汰算法)中
        templateResolver.setCacheTTLMs(3600000L);
        
        //缓存默认为true,若希望修改模板后自动更改(如调试时),可设置为false
        templateResolver.setCacheable(true);
        
        templateEngine = new TemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);

    }
    
    ...

}

当然,配置TemplateEngine对象的方式有很多,但这几行代码将让我们学会足够的必要步骤。

模板解释器

从模板解释器开始:

ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver();

模板解释器对象从如下接口实现:

org.thymeleaf.templateresolver.ITemplateResolver:

public interface ITemplateResolver {

    ...
  
    /*
     * 模板通过名字或内容来解析。星期如果他有主模板的情况下还将尝试将此解析为另一个模板的片段
     * 如果这个模板不能通过解析,将返回null.
     */
    public TemplateResolution resolveTemplate(
        final IEngineConfiguration configuration,
        final String ownerTemplate, final String template,
        final Map<String, Object> templateResolutionAttributes);

}

有这些解释器对象决定我们的模板在GTVG应用中将被如何访问,并在org.thymeleaf.templateresolver.ServletContextTemplateResolver中实现,我们将在Servlet的上下文中检索制定的模板文件资源。Servlet上下文指的是javax.servlet.ServletContext对象,他广泛的在每一个java web应用中存在。并将使用web应用程序的根作为资源文件的根路径。

但这不能说所有的模板都如此解析,因为我们可以给他设置一些配置参数。首先,模板模式中,其中一个标准为:

templateResolver.setTemplateMode("HTML");

HTML是ServletContextTemplateResolver的默认模板模式,
但出于代码可读性还是显示设置。

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

顾名思义,这是模板路径的前缀和后缀,通过模板的名称,可以将真实的路径传递到引擎来使用它。

使用这个配置:模板名称"product/list"的路径将为:

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

通过配置为cachtTTLMS属性配置一个总的时间量,可以配置模板解释器在缓存中存活的时间:

templateResolver.setCacheTTLMs(3600000L);

当然,如果缓存达到了最大缓存,并且此模板是最早的缓存条目,那么在达到TTL之前,他还是有可能被清理出缓存的。

缓存的行为和大小,都可以由用户自己定义,既可以实现ICacheManager接口自己实现规则,也可以通过修改StandardCacheManager对象设置缓存默认管理规则。

我们将在以后学习模板解释器,现在让我们来看看模板引擎对象。

模板引擎

模板引擎对象是接口org.thymeleaf.ITemplateENgine的实现,Themeleaf的core包提供一个默认的实现:org.thymeleaf.TemplateEngine,下边是创建引擎的一个例子:

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

很简单,是吧,我们所需要的仅仅是创建一个实例并设置一个模板解释器。

一个模板解释器是一个TemplateEngine必须的参数,当然,还需要其他的参数,将在后边介绍(如消息解释器,缓存大小等),现在,这些已经满足我们需要了。

我们的模板引擎现在已经可以启动,并可以使用Thymeleaf创建我们的页面了。

使用文本

多语言欢迎页

我们的第一个任务是为我们的商店网站创建一个首页。

第一个版本,将是一个非常简单的页面:仅仅有个title和一个欢迎信息。在/WEB-INF/template/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}">欢迎您光临本店!</p>
  
  </body>

</html>

首先你会注意到,这个文件是HTML5标准的DOCTYPE格式,他可以在主流浏览器中正确的显示(浏览器会忽略掉一下他不认识的属性,如th:text)。

但是,你也可能注意到,这个模板并不是一个完全有效的HTML5文档,因为有HTML5规格中没有的非标准属性,如th:*属性,事实上,我们添加了一个xmlns:th属性在html标签,用来分类非html5的标记或属性。

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

那么,如何使用一个完全符合HTML5标准的模板呢?也很容易,使用Thymeleaf数据属性语法即可,它使用data-前缀的方式来使用:

<!DOCTYPE html>

<html>

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

  <body>
  
    <p data-th-text="#{home.welcome}">欢迎您光临本店!</p>
  
  </body>

</html>

自定义的data-前缀属性是html5完全支持的语法,这样,上面这段代码就是一个完全符合HTML5定义的文档

这两种语法是完全等价的,可以无缝替换,但为了使代码更为简洁,本教程使用命名空间符号(th:),另外,th:语法是更常用的语法,在每一种Thymeleaf模板模式都是可以使用,但data-这种方式只能在HTML模式中使用。

使用th:text和外部文本

外部文本通常是模板文件中需要替换的代码,通常保存在外部的独立文件(通常是特定的.properties文件)中。他们可以很容易的设置其他语言的等效文本(这一过程通常称为国际化)。这些外在的文本片段通常被称为"messages"

Messages需要一个唯一性的Key进行标识,Thymeleaf允许你通过指定一个#{...}语法格式的Message对应一个具体的文本。如:

<p th:text="#{home.welcome}">欢迎您光临本店!</p>

我们可以看到Thymeleaf标准方言的两个主要的语言点:

  • th:text属性,他声明设置表达式的值,并使表达式返回的值来填充标签内容,替换或设置标签内部的内容,当前例子中即替换“欢迎光临本店”这些字。

  • #{home.welcome}表达式,一个标准的表达式语法,指出在模板中,th:text属性说对应Message的key,即使用home.welcome对应的value替换现有内容。

现在,对应的外部文本在哪?

在Thymeleaf中这些文件的位置都是可配的,他通过实现org.thymeleaf.messageresolver.IMessageResolver接口进行配置,通常会实现一个基于.properties文件的实现,当然也同样可以根据实际情况进行自定义实现,如将Message存储在数据库中。

如果我们在模板引擎初始化的时候没有配置任何消息解释器的实现,那就意味着我们使用的是默认的一个消息解析实现,实现类为:org.thymeleaf.messageresolver.StandardMessageResolver.

这个消息解释器会扫描/WEB-INF/template/home.html相同目录下的相同名称的.properties文件,比如:

  • /WEB-INF/templates/home_en.properties 英文
  • /WEB-INF/templates/home_fr.properties 法语
  • /WEB-INF/templates/home_jp.properties 日语
  • /WEB-INF/templates/home.properties 默认

其中的一个properties文件如下(en):

home.welcome=Welcome to our grocery store!

我们已经完成了我们所需要的Thymeleaf框架的模板,接下来创建Home的Controller。

上下文

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

public class HomeController implements IGTVGController {
    @Override
    public void process(HttpServletRequest request,
            HttpServletResponse response, 
            ServletContext servletContext,
            TemplateEngine templateEngine) throws Exception  {
        // TODO Auto-generated method stub
        WebContext ctx=new WebContext(request,response,servletContext,
                request.getLocale());
        templateEngine.process("home", ctx,response.getWriter());
        
    }
}

可以看到,第一件事就是创建了一个context,一个实现了org.thymeleaf.context.IContext接口的Thymeleaf上下文。上下文应该包含了模板引擎所执行所有所需数据的变量的map,并且引用了Locale所需的外部文件。

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

并且扩展了一个专用接口,org.thymeleaf.context.IWebContext,用于基于ServletApi的应用使用,如SpringMVC:

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

Thymeleaf核心库为每个接口给出了一个实现:

  • org.thymeleaf.context.Context implements IContext
  • org.thymeleaf.context.WebContext implements IWebContext

正如在controller的代码中看到的那样,我们使用了WebContext,事实上我们也必须使用它,因为这是ServletContextTemplateResolver必须使用一个IWebContext的实现类。

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

这个构造函数的四个参数中有三个是必须的,第四个参数系统可以设置一个默认的本地区域,如果没有设置,则使用系统默认。

从WebContext还提供了一些专用的表达式,可以在模板中获得request的参数,request,session,application的属性等,比如:

  • ${x}:返回一个Thymeleaf的context中存储的变量或request的属性。
  • ${param.x} 返回request的参数x(可以为复合值)
  • ${session.x} 返回session的属性x
  • ${application.x} 返回一个servlet上下文的属性x

就在执行前,一个特殊的变量被设置为包含了Context和WebContext的全context对象(即所有实现了IContext的对象),被称为执行信息(execInfo),这个变量有两个您可以从模板中使用的数据。

  • 模板名:#{execInfo.templateName},一个引擎执行时的特定名称,对应正在执行的模板。
  • 当前的日期时间:#{execInfo.now},一个Calender对象,对应引擎开始执行的时刻值。

执行模板引擎

随着context对象已经准备好,剩下只需要指定模板名称和context,以执行模板引擎,兵传递给response的writer,以写入响应。

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

看看执行结果(图):

更多的文本和变量

非转义文本

一个最简版的Home页面似乎已经准备好了,但是,如果有这样的需求怎么办呢:

home.welcome=欢迎光临这个<b>超级棒</b>的商店!

如果是这样的,输出结果为:

home.welcome=欢迎光临这个&lt;b&gt;超级棒 &lt;/b&gt;的商店!

很显热,这并不是我希望看见的,我们希望b作为一个标签来处理,而不是转义后显示在浏览器上。

这是浏览器对th:text的默认行为。如果我们希望Thymeleaf直接使用html标签,而不是转义他们,我们可以使用不同的属性th:utext("unescaped text")

<p th:utext="#{home.welcome}">欢迎您光临本店!</p>
这样输出信息就是所需要的了:
<p>欢迎光临这个<b>超级棒</b>的商店!</p>

使用和显示变量

如果我想在首页多现实一下信息,比如显示日期,就像下边这样:

欢迎这个超级棒的网店

当前日期为:2016-9-2

那么首先,我们应该回到首页,添加当前时间到context变量:

WebContext ctx=new WebContext(request,response,servletContext,
            request.getLocale());
//时间
SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd");
Calendar cal=Calendar.getInstance();
ctx.setVariable("today",sdf.format(cal.getTime()));
templateEngine.process("home", ctx,response.getWriter());

我们已经添加了一个String变量在context中,现在修改模板:

<p th:utext="#{home.welcome}">欢迎您光临本店!</p>

<p>当前日期为: <span th:text="${today}">2016年9月5日</span></p>

可以看到,我们仍然使用th:text属性,但是语法稍有不同,这次使用的是${...}而不是#{...},这是一个用来显示变量值的表达式。它使用OGNL语言来映射context中的变量。

${today}表达式只是简单的表示为:获取一个叫today的变量,但是这个表达式也可以变得很复杂,如${user.name}的意思是获取一个user变量,并调用他的getName()方法。

属性值有相当多的可能性,如:消息,变量表达式等等,下一章将展示这些都是什么.

标准表达式语法

在将这个小demo发展成一个真正的网店之前,先休息一下,熟悉一下Thymeleaf标准方言中最重要的组成部分:Thymeleaf标准表达式语法。

在之前,已经看到过两种有效的属性表达式:消息和变量表达式:

<p th:utext="#{home.welcome}">欢迎您光临本店!</p>

<p>当前日期为: <span th:text="${today}">2016年9月5日</span></p>

但还有更多的类型和有趣的细节,我们都还不知道,下面我们首先来一个标准表达式的快速总结:

  • 简单表达式
    • 变量表达式:${...}
    • 选定变量表达式:*{...}
    • 信息表达式:#{...}
    • 链接表达式:@{...}
    • 片段表达式:~{...}
  • 字面值
    • 文本值:'one text','Another one!',...
    • 数值:1,34,3.0,12.3...
    • 布尔值:true,false
    • Null值:null
    • 标记符号值(token):one,sometext,main,...(?)
  • 操作符
    • 字符串连接:+
    • 文本替换:|The name is ${name}|
  • 算数运算符:
    • 基本操作符:+,-,*,/,%
    • 取负值(一元运算符):-
  • boolean运算符:
    • 基本二元操作符:and,or
    • 一元操作符:!,not
  • 比较和判断:
    • 比较运算符:>,<,>=,<=,(gt,le,ge,le)
    • 判断相等运算符:==,!=(eq,ne)
  • 条件运算符
    • if-then:(if)?(then)
    • if-then-else:(if)?(then):(else)
    • default:(value)?:(defaultvalue)
  • 特殊标记
    • 无操作:_

所有这些还可以合并嵌套使用:

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

信息

我们已经知道,消息表达式许可我们将:

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

连接到资源,将内容转换为:

home.welcome=欢迎您光临本店!

但是,如果文本内容不是完全静态的怎么办?例如,我们想实现在已知某位用访问本站的时候(登录后),在页面显示他的名字:

<p>张三,您好!欢迎您光临本店!</p>

这意味着我们需要一个参数:

home.welcome={0},您好!欢迎您光临本店!

参数是根据java.text.MessageFormat标准语法指定,意味着你可以添加数字,日期等api指定的格式。

为了给参数赋值,可以给一个session的属性为用户:

<p th:utext="#{home.welcome(${session.user.name})}">
    张三,您好!欢迎您光临本店!
</p>

如果需要,可以指定多个参数,用逗号分隔,事实上,消息的key本身也可以是一个变量:

<p th:utext="#{${welcomeMsgKey}(${session.user.name})}">
   张三,您好!欢迎您光临本店!
</p>

变量

我们已经知道${...}表达式实际上是OGNL表达式在执行context中映射的变量。

更多关于OGNL语法和功能的详细信息,可以阅读http://commons.apache.org/ognl/

在SpringMVC的应用中,OGNL将被SpringEL替代,但两种语法非常相似,常用的语法完全相同。

通过OGNL定义,我们可以指定

<p>当前日期为:: <span th:text="${today}">2016-8-13</span>.</p>

事实上,是这样实现的:

ctx.getVariables("today");

OGNL也允许我们创建更强大的表达式,比如说这样:

<p th:utext="#{home.welcome(${session.user.name})}">
   张三,您好!欢迎您光临本店!
</p>

实际上他是这样执行的:

((User)ctx.getVariables("session").get("user")).getName();

通过getter方法导航是OGNL语法的一大特点:下面演示更多用法:

//访问属性使用(.)进行访问,相当于调用getter方法
${person.father.name}

//访问属性也可以通过方括号[]进行访问,属性名作为一个变量通过单引号(')或双引号(")写在方括号中
${person['father']['name']}

//如果对象是一个map,可以用引号和方括号的语法将相当于直营一个调用它的key获取对象的get方法。
${countriesByCode.ES}
${personsByName['张三'].age}

//对于数组或集合的索引也用方括号来执行
${personsArray[0].name}

//可以调用各种方法,无论是否有参数
${person.createCompleteName()}
${person.createCompleteNameWithSeparator('-')}

基本对象表达式

当使用OGNL表达式的context变量的时候,可以使用更方便的表达方式。这些对象也是用来#符号:

  • #ctx: context对象
  • #vars:context属性
  • #locale:context本地化信息
  • #request:(仅限WebContext) HttpServletRequest对象
  • #response:(仅限WebContext) HttpServletResponse对象
  • #session:(仅限WebContext)HttpSession对象
  • #servletContext:(仅限WebContext)ServletContext对象

所以我们可以这样使用

这里是:<span th:text="${#locale.country}">中国</span>.

你可以在附录1中查看这些对象的全部参考

工具对象表达式

除了基本对象,Thymeleaf还为我们提供了一套实用对象,可以帮助我们在执行表达式中解决一下常见的任务:

  • #execInfo:正在处理的模板信息
  • #messages:获取外部信息的内部变量的一个实用方法,同时也可以用#{...}获取
  • #uris:针对URL或URI进行一些转码的方法
  • #conversions:根据配置执行一些转换方法
  • #dates:针对java.util.Date对象的实用方法:包括日期格式化,日期提取等。
  • #calendars:与#dates相似,但针对的是java.util.Calendar对象。
  • #numbers:针对numeric对象格式化的实用方法
  • #strings:针对String对象的实用方法,包括包含,判断起始,前/后追加等方法
  • #objects:针对object类的一般实用方法
  • #boolean:针对boolean运算的一些实用方法
  • #arrays:针对数组的实用方法
  • #lists:针对list的实用方法
  • #sets:针对set的实用方法
  • #map:针对map的实用方法
  • #aggregates:在数组或集合中创建聚合的一些实用方法
  • #ids:用于处理可能重复的标识属性的使用方法,例如,作为迭代的变量。

    你可以在附录2中查看这些工具对象的全部参考

在首页中使用格式化日期

现在,我们知道了这些工具对象表达式,就可以改变首页中显示日期的方式,首先,改变我们HomeController中的代码,将:

SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd");
Calendar cal=Calendar.getInstance();
ctx.setVariable("today",sdf.format(cal.getTime()));

这三行可以合并为1行

ctx.setVariable("today",Calendar.getInstance());

然后,修改模板文件的相应行为:

<p>
  当前日期为: <span th:text="${#calendars.format(today,'yyyy-MM-dd')}">2016年9月5日</span>
</p>

选定变量表达式(或使用{...}) ###
变量表达式不但可以使用${...}表达式,同时也可以使用
{...}表达式。

他们有个最重要的区别:*{...}表达式的值是在选定的对象而不是整个context的map。也就是说,如果没有选定的对象,*{...}和${...}没有区别

那么问题来了:什么是选定对象?一个th:object对象属性,使用用户权限页面来演示一下:

<div th:object="${session.user}">
    <p>姓名:<span th:text="*{firstName}">张三</span></p>
    <p>年龄:<span th:text="*{age}">26</span></p>
    <p>国籍:<span th:text="*{nationlity}">中国</span></p>
</div>

这相当于:

<div>
    <p>姓名:<span th:text="${session.user.firstName}">张三</span></p>
    <p>年龄:<span th:text="${session.user.age}">26</span></p>
    <p>国籍:<span th:text="${session.user.nationlity}">中国</span></p>
</div>

当然,也可以混合使用

<div th:object="${session.user}">
    <p>姓名:<span th:text="*{firstName}">张三</span></p>
    <p>年龄:<span th:text="${session.user.age}">26</span></p>
    <p>国籍:<span th:text="*{nationlity}">中国</span></p>
</div>

当一个使用选定对象的地方,选定的对象其实就是使用了#object表达式的${...}表达式

<div th:object="${session.user}">
    <p>姓名:<span th:text="${#object.firstName}">张三</span></p>
    <p>年龄:<span th:text="${session.user.age}">26</span></p>
    <p>国籍:<span th:text="${#object.nationlity}">中国</span></p>
</div>

也就是说,如果没有已经完成的选定对象,那么,*{...}和${...}两种表达式是完全等价的。

<div>
    <p>姓名:<span th:text="*{session.user.firstName}">张三</span></p>
    <p>年龄:<span th:text="*{session.user.age}">26</span></p>
    <p>国籍:<span th:text="*{session.user.nationlity}">中国</span></p>
</div>

Url链接

由于其重要性,URL是web应用程序模板的一等公民,Thymeleaf的标准方言都为它定义了特殊语法:@{...}

他有不同类型的网址:

  • 绝对地址,比如:https://niufennan.github.io/
  • 相对地址,可以有如下方式:
    • 相对于页的,比如:user/login.html
    • 相对于上下午的,比如:/itemdetails?d=3(将自动添加服务器的上下文名称)
    • 相对于服务器的,比如:~/billing/processInvoice(可以在同一服务器中的不同上下文(即application)中使用)
    • 相对于协议的,比如://cdn.bootcss.com/jquery/2.2.3/jquery.min.js

真正将这些表达式转换并输出为URL的工具,是一个注册在ITemplateEngine中的一个org.thymeleaf.linkbuilder.ILinkBuilder接口的实现类。

Thymeleaf可以在任何情况下处理绝对地址,但相应的,也会要求你给予一个实现了IWebContext接口的context对象,他包含了一些创建链接需要的来自Http请求的相关信息。

在默认情况下,注册的实现是类org.thymeleaf.linkbuilder.StandardLinkBuilder,它对于基于Servlet API的网络或离线应用已经足够了,而其他情况(如非ServletAPI的web项目)则可能需要自己创建接口的实现。

举例说明一下,需使用th:href属性:

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

<!-- 输出: '/gtvg/order/details?orderId=3' -->
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>

<!-- 输出: '/gtvg/order/3/details' -->
<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>

注意:

  • th:href是一个修饰属性,一旦设置这个属性,它会计算链接,并设置<a>标签内的href属性的url值。
  • 可以对url的参数使用表达式(比如orderId=${o.id}),url上所需的编码工作,也会自动执行。
  • 如果需要多个参数,用逗号(,)分开即可如:(@{/order/process(execId=${execId},execType='FAST')})
  • 网络路径中也可以使用变量模板,如:@{/order/{orderId}/details(orderId=${orderId})}
  • 相对URL使用/开始,如/order/details,将自动适应上下文的名词前缀。
  • 如果不清楚cookie是否被启用,一个jsessionid=...的后缀可能被加入到url中,用于回话保存,这就是所谓的URL重写。Thymeleaf允许利用Servlet的api为每一个url,使用response.encoding来扩展重写过滤器。
  • th:href允许有一个静态工作的url配置在模板中,这样,我们的模板即使在不工作时,仍然可以通过浏览器互相连接。

就像#{...}一样,URL表达式中的值也可以是另一个表达式的结果:

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

为首页制作一个简单菜单

现在已经知道了如何创建一个连接,是时候给首页增加一个小菜单了,以告诉大家这个站点还有些什么内容。

<p>请选择:</p>
<ol>
  <li><a href="product/list.html" th:href="@{/product/list}">产品列表</a></li>
  <li><a href="order/list.html" th:href="@{/order/list}">订单列表</a></li>
  <li><a href="subscribe.html" th:href="@{/subscribe}">订阅新闻</a></li>
  <li><a href="userprofile.html" th:href="@{/userprofile}">用户权限</a></li>
</ol>

相对于服务器根的URL

还有一个附加的语法,可用于指向一个服务器的根地址(而不是上下午的根地址),以便于指向同一服务器中的不同上下文。他的语法为:@{~/path/to/something}

片段

片段表达式是用一种简单的方式来标识一个片段的标记,并可以将他移动的其余模板,它允许我们执行重写模板,传递给其他模板参数等操作。
做常用的片段使用方式是插入使用,属性值为th:insert或th:replace(更多内容见后边部分)

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

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

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

本教程稍后的部分有一篇完整的关于模板布局的介绍,其中包含变量表达式更深层次的介绍。

字面值

文本值

字面值指的是包含在单引号之间字符,它可以使任何字符,但应该尽量避免使用'


现在你看到的是模板文件.

数值

顾名思义,数值显示的就是一个数字:

<p>今年是<span th:text="2016">1942</span>.</p>
<p>两年后,将是<span th:text="2013 + 2">1944</span>.</p>

布尔值

布尔值只有true和false两个值,举例:

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

注意,在这个例子中,==false是写在了${...}的外边,所以使Thymeleaf本身在支持它,如果写在了${...}的里边,则变为由OGNL或SpringEL库来支持它。

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

null值

null值可以这样使用

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

标记符号值(token)

实际上,数值,布尔值,null值都是一种特殊的token值。

这些token值允许为标准表达式进行一点点的简化,他们的工作方式和文本值完全一样,但是只能使用字符(a-z,A-Z),数值(0-9),括号即[ ]和( ),
所以不能有空格,括号等。

并且,他可以不被包含在单引号中:

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

而不是这样(这样为文本值):

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

追加文本

一段文本,无论是一段字面值,变量的返回值,还是信息表达式,都可以使用+来追加一段文本

<div th:text="'这个人的名字为: ' + ${user.name}">

字面值替换

还可以直接替换一段字面值中包含的字符串信息,而无需通过操作符+
这些替换必须被竖线(|)包围,如:

<span th:text="|欢迎光临这个应用, ${user.name}!|">

即相当于:

<span th:text="'欢迎光临这个应用, ' + ${user.name} + '!'">

格式替换也可以和其他表达式相结合使用:

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

注意:只有变量表达式${...}可以出现在|...|中,其他的如布尔,数值或文字的token,条件表达式等都不可以使用。

算数运算

在表达式中还可以使用一些算数运算,如:+,-,*,/,%

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

注意:同布尔值的例子一样,这个同样可以写成使用OGNL或SpringEL支持的方式:

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

注意:有两个运算符存在别名:div(/),mod(%)

比较和判断

表达式中的值可以通过>,<,>=,<=进行比较,也可以通过==和!=操作符进行相等的判断。注意在xml中使用的时候,不应该直接使用<,>,应当使用&lt
和&gt来代替

<div th:if="${prodStat.count} &gt; 1">
<div th:text="'执行模式为 ' + ( (${execMode} == 'dev')? 'Development' : 'Production')">

注意,这些都存在着别名:gt(>),lt(<),ge(>=),le(<=),not(!),eq(==),neq/ne(!=)

条件表达式

条件表达式为根据条件(另一个表达式)来选择两个表达式中的一个。

看一个示例:

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

条件表达式的所有的三个部分均在一个自表达式中,意味着他可以用在变量${...},*{...},信息#{...},URL@{...}或字面量('...')中.

他还可以使用()来进行嵌套

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

else部分可以省略,如果条件为否,则为一个null值

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

默认表达式(Elvis运算符)

默认表达式是一种特殊的没有then部分的条件表达式。在一些语言,如Groovy中,它相当于Elvis运算符,它允许有两个表达式,第二个只有在第一个返回null的时候才执行。

修改一下用户权限页,如:

<div th:object="${session.user}">
  ...
  <p>年龄: <span th:text="*{age}?: '(没有输入)'">27</span>.</p>
</div>

正如看到的那样,使用?:操作符,我们在指定当年龄无效的时候,给定一个默认值(使用字面值),这条代码相当于:

<p>年龄: <span th:text="*{age != null}? *{age} : '(没有输入)'">27</span>.</p>

和条件值一样,他也可以使用括号来实现嵌套:

<p>
  姓名: 
  <span th:text="*{name}?: (*{admin}? 'Admin' : #{default.username})">张三</span>
</p>

无操作标记

无操作标记是使用下划线表示(_)

这背后是想指定一个标记来表达期望的结果是什么也不做,也就是说,如果处理的属性(如th:text)没有值。

这将允许开发人员使用原型文本作为默认值,如:

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

将可以改为:

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

直接使用原型,可以使使代码更加灵活

日期的转换与格式化

Thymeleaf可以使用双大括号的方式,为变量${...}和选择*{...}表达式通过配置转换服务进行数据转换。

它基本上是这样的:

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

这个双大括号${{}}通知Thymeleaf对user.createTime表达式的结果进行转换服务,并在输出结果之前进行格式化操作。

转换服务器默认使用IStandardConversionService的实现类(StandardConversionService类),它只能够简单的执行一个对象的toString(),即将一个对象转为字符串服务,有关注册一个转换服务的更多信息,可以查看稍后更多配置部分。

在thymeleaf-spring3和thymeleaf-spring4有一整套基于Spring转换服务的Thymeelaf转换服务配置,因此他可以自动实现${{}}和*{{}}的服务。

预处理

Thymeleaf表达式除了这些特征,还为我们提供了预处理的功能。

预处理是指在正常的完成一个表达式之前执行的工作。他允许对最终执行的实际表达式进行修改.

一个预处理表达式和一个正常表达式几乎一样,但他在双下划线之间(__$(表达式)__)

比如,国际化的配置文件message_jp.properties的一个条目包含了一个OGNL表达式用于调用一个静态方法:

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

以及另一个等效的Message_en.properties:

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

对此,可以先创建一个用于标记的表达式,这个表达式的值取决于区域设置,为此,使用预处理,然后让Thymeleaf去执行它:

<p th:text="${__#{article.text('textVar')}__}">一些文字...</p>

例如日本,上边预处理与如下内容等效:

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

预处理中如有字符串__可以使用\_\_来进行转义。

设置属性值

本章将介绍如何设置或修改标签属性的值,下一章将讲解如何设置内容的值。

属性值通用设置方式

我们的网站会不定期发布一些新闻,我们希望我们的用户能够订阅他,所以我们创建一个/WEB-INF/templates/subscribe.html的表单模板:

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

这个看起来不错,但是,这个更像是一个普通的html页面,首先,这个表单的action是指向了模板文件本身,并没有重写URL,第二,提交按钮的值,他是一个普通的文本,而我们希望他是一个国际化的。

使用th:attr属性,它具有设置或修改一个标签属性值的能力。

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

用法很简单:th:attr将是一个值对应一个属性的表达式,在转换处理后,将会返回如下结果:

<form action="/gtvg/subscribe">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="subscribe me!"/>
  </fieldset>
</form>

除了更新了属性值,还可以看到,应用的已经自动将url更新为context前缀的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" alt="这里是logo" />

特定属性值设定的方式

到了现在,你可能会发现,像:

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

这种,看起来是一个非常难看的模板。这种指定属性值的赋值方式很明显不是最优雅的一种创建方式模板。

事实也的确如此,这也就是为什么th:attr很少出现在实际应用的模板中,实际中经常使用的是th:*来实现标签中特定的属性。

在Thymeleaf的标准方言中,通常都是很直观的方式,如设置一个按钮的value值,使用th:value表达式,如:

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

看起来感觉好多了,然后在看一下表单中的action属性:

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

在之前home.html模板中的th:href,其实就在使用的这个语法:

<li><a href="product/list.html" th:href="@{/product/list}">产品列表</a></li>

Thymeleaf针对每一个属性都有一个特定的设置语法:

th:abbrth:acceptth:accept-charsetth:accesskey
th:actionth:alignth:altth:archive
th:audioth:autocompleteth:axisth:background
th:bgcolorth:borderth:cellpaddingth:cellspacing
th:challengeth:charsetth:citeth:class
th:classidth:codebaseth:codetypeth:cols
th:colspanth:compactth:contentth:contenteditable
th:contextmenuth:datath:datetimeth:dir
th:draggableth:dropzoneth:enctypeth:for
th:formth:formactionth:formenctypeth:formmethod
th:formtargetth:fragmentth:frameth:frameborder
th:headersth:heightth:highth:href
th:hreflangth:hspaceth:http-equivth:icon
th:idth:inlineth:keytypeth:kind
th:labelth:langth:listth:longdesc
th:lowth:manifestth:marginheightth:marginwidth
th:maxth:maxlengthth:mediath:method
th:minth:nameth:onabortth:onafterprint
th:onbeforeprintth:onbeforeunloadth:onblurth:oncanplay
th:oncanplaythroughth:onchangeth:onclickth:oncontextmenu
th:ondblclickth:ondragth:ondragendth:ondragenter
th:ondragleaveth:ondragoverth:ondragstartth:ondrop
th:ondurationchangeth:onemptiedth:onendedth:onerror
th:onfocusth:onformchangeth:onforminputth:onhashchange
th:oninputth:oninvalidth:onkeydownth:onkeypress
th:onkeyupth:onloadth:onloadeddatath:onloadedmetadata
th:onloadstartth:onmessageth:onmousedownth:onmousemove
th:onmouseoutth:onmouseoverth:onmouseupth:onmousewheel
th:onofflineth:ononlineth:onpauseth:onplay
th:onplayingth:onpopstateth:onprogressth:onratechange
th:onreadystatechangeth:onredoth:onresetth:onresize
th:onscrollth:onseekedth:onseekingth:onselect
th:onshowth:onstalledth:onstorageth:onsubmit
th:onsuspendth:ontimeupdateth:onundoth:onupload
th:onvolumechangeth:onwaitingth:optimumth:pattern
th:placeholderth:posterth:preloadth:radiogroup
th:relth:revth:rowsth:rowspan
th:rulesth:sandboxth:schemeth:scope
th:scrollingth:sizeth:sizesth:span
th:spellcheckth:srcth:srclangth:standby
th:startth:stepth:styleth:summary
th:tabindexth:targetth:titleth:type
th:usemapth:valueth:valuetypeth:vspace
th:widthth:wrapth:xmlbaseth:xmllang
th:xmlspace

同时设置多个值

还有两个比较特殊的属性即th:alt-title和
th:lang-xmllang,他们能同时设置两个属性:

  • th:alt-title同时设置alt和title
  • th:lang-xmllang同时设置lang和xml:lang

不然修改首页中刚刚的logo图标签:

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

修改为:

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

或者:

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

追加和重写

Thymeleaf还提供了th:attrappend和th:attrprepend属性,用于为属性之前或之后增加值

比如,你可能想在一个按钮的现有css类的基础上在新增一个css类,将会非常容易:

<input type="button" value="点击" class="btn" th:attrappend="class=${' ' + cssStyle}" />

如果你对cssStyle变量的值为warning,那么输出将为:

<input type="button" value="点击" class="btn warning" />

因为样式表使用的如此频繁,所以标准方言中还有两个附加属性,th:classappend和th:styleappend属性,用于追加一个class类或者一段样式表而不改变现有内容:

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

each为迭代属性,稍后介绍。

拥有固定值的布尔属性

在XHTML/HTML5中有些属性是特殊的,它们要么是一个固定值,要么根本就不存在,比如:

<input type="checkbox" name="option1" checked="checked" />
<input type="checkbox" name="option2" />

在严格模式下,checked不能有其他的值,类似的还有disabled,muliple,readonly,selected等。

在标准方言中,允许你通过一个条件值来设置这些属性,如果值为真,这些属性将设置为它的固定值,如果为假,则不会设置此属性。如:

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

在标准方言中,类似的属性如下:

th:asyncth:autofocusth:autoplayth:checked
th:controlsth:declareth:defaultth:defer
th:disabledth:formnovalidateth:hiddenth:ismap
th:loopth:multipleth:novalidateth:nowrap
th:openth:pubdateth:readonlyth:required
th:reversedth:scopedth:seamlessth:selected

设置自定义属性

除了刚刚看到的对于特定属性的处理外,Thymeleaf还在标准方言中提供了一个默认属性处理器,可以设置任意自定义属性的值,甚至可以没有具体的th:*处理器。
比如:

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

将返回

<span whatever="张三">...</span>

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-{前缀}-{名字}这种语法是一个html5的自定义属性的标准方式。这种方式下,开发者无需导入命名空间,如th:,Thymeleaf将自动提供执行(所有方言模式,不仅是标准方言)。

还有一种语法来指定自定义标签:{前缀}-{名字},这里遵循的是W3C标准的自定义标签。这也是可以使用,举个例子,比如th:block就可以使用th-block,这个将在稍后解释。

注意:这个语法是th:*命名空间的一个补充,并不是他的替换,在为了跟本没有弃用命名空间语法的想法。

迭代

到目前为止,这个网络商店已经有了一个主页,有了一个用户配置的页面,还有了一个让用户订阅我们的通讯页,但是,我们的产品呢?我们是不是首先应该有一个产品列表,让客户知道我们在卖什么东东么?嗯,显然是的,让我们现在就走吧!

迭代基础

在模板/WEB-INF/templates/product/list.html中要显示产品列表,需要一个table。然后每个产品在显示上是table的一行,所以在我们的模板中,就需要Thymeleaf来迭代每一个产品。

在标准方言中,提供了一个迭代属性,为th:each

使用 th:each

对于产品列表页,首先需要更改产品列表的控制器,让其从服务层中获取产品数据,然添加到模板的context中。

public void process(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext,
        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来迭代产品:

<h1>产品列表</h1>
<table>
    <tr>
        <th>产品名称</th>
        <th>产品价格</th>
        <th>有现货</th>
    </tr>
    <tr th:each="prod:${prods}">
        <td th:text="${prod.name}">土豆</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="@{/}">返回首页</a>
</p>

prod:${prods}属性值的意思是,迭代${prods}的每个元素并重复这个模板的这个片段。然后解释一下这两部分分别的意思:

  • ${prods}被称为迭代表达式或迭代变量
  • prod被称为重复变量或迭代值

注意:迭代值止只可以用在tr节点上面(包括迭代里边包含的td标签)。

可迭代的值

不是只有java.util.List对象可以使用Thymeleaf进行迭代。事实上,任何一个完整的对象集都可以使用th:each属性:

  • 所有实现了java.util.Iterable接口的对象
  • 所有实现了java.util.Map接口的对象(此时的迭代值是java.util.Map.Entry类)
  • 所有数组
  • 任何对象都视为一个只包含了它本身的单值的列表。

保持迭代状态

当使用th:each的时候,Thymeleaf会提供一个跟着迭代状态的机制:状态变量。

状态定义被封装在th:each的属性中。并包含以下数据:

  • 获取当前迭代的从0开始的下标,使用index属性
  • 获取当前迭代的从1开始的下标,使用count属性
  • 获取当前迭代元素的总量,使用size属性
  • 获取迭代变量中的迭代值,使用current属性
  • 当前迭代值是奇数还是偶数,使用even/odd的布尔值属性
  • 当前的迭代值是不是第一个元素,使用first布尔值属性
  • 当前迭代值是不是最后一个元素,使用last布尔值属性。

现在修改一下前一个例子:

<h1>产品列表</h1>
<table>
    <tr>
        <th>产品名称</th>
        <th>产品价格</th>
        <th>有现货</th>
    </tr>
    <tr th:each="prod,iterStat:${prods}" th:class="${iterStat.odd}?'odd'">
        <td th:text="${prod.name}">土豆</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="@{/}">返回首页</a>
</p>

可以看到,状态变量(即iterStat)的定义:将这个变量的名字作为属性写在迭代值之后,用逗号于迭代值隔开。产生了迭代值之后,他的状态值就可以也仅仅可以在th:each包含的代码段中使用。

这段代码的执行结果为:

201609202335.PNG

可以看到,这端代码执行的非常完美。并且只在奇数行添加了odd的css(行数从0开始)

如果没有显示的设置一个状态变量,Thymeleaf会默认的创建一个使用迭代值+Stat后缀的迭代变量,如:

<table>
  <tr>
    <th>产品名称</th>
    <th>产品价格</th>
    <th>有现货</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>

通过懒检索进行优化

有时我们需要对只有检索操作的数据集的检索(如从数据库查询)进行优化。

事实上,他可以应用于任何的数据块,而检索内存中可能要重复使用的集合可能是最常见的情况。

为了能够支持这一点,Thymeleaf提供了懒加载上下文变量的机制,上下文的变量内容通过时实现ILazyContextVariable接口(最有可可能使用它默认实现:LazyContextVariable),它将在模板解析的时候来执行,如:

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

这个变量就可以使用懒加载,,使用方式为:

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

但如果为下边的代码,条件为false的时候也将不会被初始化(它的loadValue()方法将不会被调用):

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

条件判断

if和unless

有时你可能想要,模板的某个片段只有在条件被满足的时候才出现在结果中。

比如,假设我们要在产品表中显示一个列,该列针对每个产品的评论,如果有评论,则链接到评论页面。

要实现这个功能,就要用到前面说到过的th:if属性:

<table>
    <tr>
        <th>产品名称</th>
        <th>产品价格</th>
        <th>有现货</th>
        <th>用户评价</th>
    </tr>
    <tr th:each="prod:${prods}">
        <td th:text="${prod.name}">土豆</td>
        <td th:text="${#numbers.formatDecimal(prod.price,0,2)}">2.41</td>
        <td th:text="${prod.isStock}?#{true}:#{false}">yes</td>
        <td>
            <span th:text=${#lists.size(prod.comments)}>2</span>个评价
            <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}"
            th:if="${not #lists.isEmpty(prod.comments)}">查看</a>
        </td>
    </tr>
</table>

重点注意下面这一行:

<a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}"
            th:if="${not #lists.isEmpty(prod.comments)}">查看</a>

事实上,这段代码没什么好解释的,如果产品有评论,那么我们就创建一个跳转到评论页面的超链接,并且使用产品ID作为参数。

查看生成结果:
20160908_2336.PNG

Perfect!这正是我们想要的!

th:if不光可以使用布尔值,一下规则都可以:

  • 如果值不为空:
    • 如果值为布尔型并且为true
    • 如果值为数值型并且不为0
    • 如果值为character并且不为0
    • 如果值为String,并且不为"false","off"和"no"
    • 如果值不为布尔型,数值型,character或String的任意类型
  • 如果值为null,th:if将为false

th:if还有一个互逆的表达式为th:unless,还继续用之前的例子作一个演示:

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

switch

条件选择还可以像java一样,使用switch来声明:在Thymeleaf中使用th:switch和th:case来实现。
他的工作方式会和你想的一样:

<div th:switch="${user.role}">
  <p th:case="'admin'">超级管理员用户</p>
  <p th:case="#{roles.manager}">管理员用户</p>
</div>

注意:一旦一个th:case被判断为真,那么其他的同等级的th:case都将被判断为假

default的写法为th:case="*"

<div th:switch="${user.role}">
  <p th:case="'admin'">超级管理员用户</p>
  <p th:case="#{roles.manager}">管理员用户</p>
  <p th:case="*">其他用户</p>
</div>

模板的布局

导入模板片段

定义和引用片段

我们经常会想让我们的模板包含一些其他模板,比较常见的用途如页眉,页脚,菜单等。

为了做到这一点,Thymeleaf需要我们定义一些可用片段,我们能通过th:fragment属性来实现这一点。

现在,我们要添加一个带标准版权声明的页脚文件(/WEB-INF/templates/footer.html):

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
  <body>
    <div th:fragment="copy">
      &copy; 网络商店
    </div>
  </body>
</html>

上面定义了一个叫copy的代码片段,我们能通过th:insert和th:replace属性(还有th:include,但Thymeleaf3.*版本不推荐使用),很容易的将他导入到首页中:

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

注意一点,th:insert使用的是一个片段表达式(~{...}),或片段中一个更具体的表达式。但在前者的情况下(简单片段表达式),比如就像上边的代码,~{...}是可选的,所以如下班的代码是等价的:

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

片段语法说明

判断表达式语法非常简单,有三种不同的格式:

  • 模板名::dom选择将导入模板名所指定的代码片段到dom选择器中。
    • 注意:dom选择器可以仅仅是一个片段的名字,所以可以指定一下非常简单的名字,如:footer:copy或更简单的。

dom选择器语法类似于XPath或css选择器,更多内容见附录C。

  • 直接使用模板名将此模板对应的完整的代码导入。

注意:此时由th:insert和th:replace标签导入的模板必须在当前的模板引擎下的模板解释器可以分辨。

  • 使用::dom选择器this:dom选择器导入与之相同的模板。

在上面的格式中,模板名dom选择器都可以使用任何表达式的结果来表示,比如:

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

片段中可以包含任何th:*属性。一点判断被包含到目标模板(即使用th:insert/th:replace的文件)中,这些属性将被执行。然后他们将能使用目标模板中的任何context变量。

这种方法有个很大的优势:你的任何片段的代码,完整的代码均可以显示在浏览器中,同时,仍保留了使用Thymeleaf把他导入到其他模板中的能力。

引用不包含th:fragment的片段

此外,由于强大的dom选择器,使我们可以导入不含有th:fragment属性的代码片段,他甚至可以标记所有来自Thymeleaf所不知道的应用,如:

<div id="copy-section">
  &copy; 2011 网络商店
</div>

这里可以就像css一样的使用它的id属性:

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

th:insert和th:replace的不同点(以及th:include)

好了,现在让我们看看这几个属性有什么区别(th:insert,th:replace和th:include(3.*版本不推荐使用)):

  • th:insert是将th:fragment标签的内容纳入宿主标签
  • th:replace是使用th:fragment标签替换宿主标签
  • th:include与th:insert类似,但是他插入的是片段的内容,而不是片段

不够直观?举个例子:

<div th:fragment="copy">
  &copy; 网络商店
</div>

导入到两个div标签中:

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

执行结果:

<body>
  ...
  <div>
    <footer>
        &copy; 网络商店
    </footer>
  </div>
  <footer>
    &copy; 网络商店
  </footer>
  <div>
      &copy; 网络商店
  </div>
</body>

片段的参数

为了像一个函数一样的使用一个模板片段,在片段定义的时候th:fragment可以定义一组参数:

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

th:insert和th:replace都用同一种语法来使用参数片段:

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

注意在第二种键值对的方式中,参数不关心顺序:

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

在没有参数签名的片段使用参数

即使一个片段没有定义参数,就像这样:

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

我们可以使用上面键值对的方式来赋予参数,并且也只能使用键值对的方法。

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

事实上,在目标也使用th:replace和th:with的组合属性来接收:

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

注意,在这里,无论参数是否有签名,都定义的是一个片段的局部变量,所以不会导致它覆盖或清空之前的context变量,片段仍然可以正常访问调用它的模板的每个context变量。

模板断言

th:assert属性可以定义一个用逗号分隔的表达式,用来为每一个条件做出评估,以判断是否产生异常。

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

这是一个在片段签名时就验证参数的方便方式:

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

更灵活的模板:超越单纯的插入

基于片段表达式,我们可以为片段指定一个非文本,数字,javabean的参数,以代替标记。

这样我们就能使用这样一种方式,它可以使用丰富的标记来调用模板,以实现一个非常灵活的模板布局机制。

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

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

  <title th:replace="${title}">这个超帅应用</title>

  <!-- 常用样式脚本 -->
  <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/myapp.css}">
  <link rel="shortcut icon" th:href="@{/images/favicon.ico}">
  <script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>

  <!--/* 每页占位符链接 */-->
  <th:block th:replace="${links}" />

</head>

我们使用的片段

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

  <title>超帅应用 - Main</title>

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

</head>
...

结果将我们的调用的模板的实际的

和标签作为标题和链接变量的值,从而导致我们的片段在插入时被定制:
...
<head>

  <title>超帅应用 - Main</title>

  <!-- 常用样式脚本 -->
  <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>超帅应用 - Main</title>
</head>

注意第二个参数设置为一个为空的片段,因此这时候没有任何内容写入块,结果为:

<head>

  <title>超帅应用 - Main</title>
  <!-- 常用样式脚本 -->
  <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>

使用无操作标记

如果我们只想让当前的片段使用它标记的默认值,可以使用一个无操作标记,还继续使用上边的例子:

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

注意标题片段(即common_header片段的第一个参数)就是一个无操作标记,所以这部分的片段不会被处理:

<title th:replace="${title}">这个超帅应用</title>

它的执行结果为:

<head>

  <title>The awesome application</title>

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

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

</head>

高级条件插入片段

空片段标记和无操作标记都支持我们使用一种非常简单和优雅的方式使用条件表达式来决定插入片段。

例如,我想之下管理员权限下插入common::adminhead片段,否则不插入:

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

同样,我们也可以使用不操作标记,来达到只有当条件满足时才插入片段,而不满足则不进行修改的操作:

...
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : _">
    欢迎 [[${user.name}]], 您 <a th:href="@{/support}">点击这里</a> 对我们支持.
</div>
...

此外,如果已经配置模板解析器检查模板资源是否存在(checkExistence ),我们甚至可以使用片段是否存在作为一个判断条件:

<!-- 如果片段common::salutation存在则DIV使用该片段填充  -->
<!-- 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>

删除模板片段

重温一下产品列表模板的最后一个版本:

<table>
    <tr>
        <th>产品名称</th>
        <th>产品价格</th>
        <th>有现货</th>
        <th>用户评价</th>
    </tr>
    <tr th:each="prod:${prods}"  th:class="${prodStat.odd}? 'odd'">
        <td th:text="${prod.name}">土豆</td>
        <td th:text="${#numbers.formatDecimal(prod.price,0,2)}">2.41</td>
        <td th:text="${prod.isStock}?#{true}:#{false}">yes</td>
        <td>
            <span th:text=${#lists.size(prod.comments)}>2</span>个评价
            <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}"
            th:if="${not #lists.isEmpty(prod.comments)}">查看</a>
        </td>
    </tr>
</table>

这是一个非常好的模板文件,但是作为一个静态页,它将不是一个很好的原型。

为什么呢?因为如果他直接在显示器总显示的话,只会显示出一个表头和一行数据,看起来不够现实,我作为一个原型,我们需要更多的行。

好的 添加一些:

<table>
    <tr>
        <th>产品名称</th>
        <th>产品价格</th>
        <th>有现货</th>
        <th>用户评价</th>
    </tr>
    <tr th:each="prod:${prods}"  th:class="${prodStat.odd}? 'odd'">
        <td th:text="${prod.name}">土豆</td>
        <td th:text="${#numbers.formatDecimal(prod.price,0,2)}">2.41</td>
        <td th:text="${prod.isStock}?#{true}:#{false}">yes</td>
        <td>
            <span th:text=${#lists.size(prod.comments)}>2</span>个评价
            <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}"
            th:if="${not #lists.isEmpty(prod.comments)}">查看</a>
        </td>
    </tr>
    <tr class="odd">
        <td>白菜测试</td>
        <td>1.50</td>
        <td>no</td>
        <td>
          <span>0</span>条评价
        </td>
      </tr>
      <tr>
        <td>洋葱测试</td>
        <td>1.99</td>
        <td>yes</td>
        <td>
          <span>3</span>条评价
          <a href="comments.html">查看</a>
       </td>
    </tr>
</table>

现在是3条,从原型的角度来看,肯定是更好了,但是Thymeleaf处理后会发生什么?

201609212349.PNG

最后两行模拟行同样也再生成的页面中!当然,Thymeleaf迭代第一行之后,没有任何理由来删除最后两行。

如果删除最后两行,可以通过th:remove属性来实现,将th:remove属性添加到tr标签内:

<table>
    <tr>
        <th>产品名称</th>
        <th>产品价格</th>
        <th>有现货</th>
        <th>用户评价</th>
    </tr>
    <tr th:each="prod:${prods}"  th:class="${prodStat.odd}? 'odd'">
        <td th:text="${prod.name}">土豆</td>
        <td th:text="${#numbers.formatDecimal(prod.price,0,2)}">2.41</td>
        <td th:text="${prod.isStock}?#{true}:#{false}">yes</td>
        <td>
            <span th:text=${#lists.size(prod.comments)}>2</span>个评价
            <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}"
            th:if="${not #lists.isEmpty(prod.comments)}">查看</a>
        </td>
    </tr>
    <tr class="odd"  th:remove="all">
        <td>白菜</td>
        <td>1.50</td>
        <td>no</td>
        <td>
          <span>0</span>条评价
        </td>
      </tr>
      <tr  th:remove="all">
        <td>洋葱</td>
        <td>1.99</td>
        <td>yes</td>
        <td>
          <span>3</span>条评价
          <a href="comments.html">查看</a>
       </td>
    </tr>
</table>

运行之后,可以看到输出结果:

%E6%8D%95%E8%8E%B7.PNG

那么,属性值中的all代表着什么呢?事实上,这个属性根据删除的方式可以有五种值:

  • all:删除包括标签和子标签在内的所有内容
  • body:不删除标签,但删除所有的子节点。
  • tag:删除标签,但不删除子节点。
  • all-but-first:删除包括标签和子节点在内的所有内容, 除了第一个。
  • none:什么都不做,只在动态执行时使用。

用上个例子演示一下all-but-first的用法:

<table>
    <thead>
        <tr>
            <th>产品名称</th>
            <th>产品价格</th>
            <th>有现货</th>
            <th>用户评价</th>
        </tr>
    <thead>
    <tbody th:remove="all-but-first">
        <tr th:each="prod:${prods}"  th:class="${prodStat.odd}? 'odd'">
            <td th:text="${prod.name}">土豆</td>
            <td th:text="${#numbers.formatDecimal(prod.price,0,2)}">2.41</td>
            <td th:text="${prod.isStock}?#{true}:#{false}">yes</td>
            <td>
                <span th:text=${#lists.size(prod.comments)}>2</span>个评价
                <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}"
                th:if="${not #lists.isEmpty(prod.comments)}">查看</a>
            </td>
        </tr>
        <tr class="odd">
            <td>白菜</td>
            <td>1.50</td>
            <td>no</td>
            <td>
              <span>0</span>条评价
            </td>
          </tr>
          <tr>
            <td>洋葱</td>
            <td>1.99</td>
            <td>yes</td>
            <td>
              <span>3</span>条评价
              <a href="comments.html">查看</a>
           </td>
        </tr>
    </tbody>
</table>

th:remove属性值可以使用任何Thymeleaf标准方言的表达式,只要返回的是一个允许的字符串的值(all, tag, body, all-but-first 或者 none)

这意味着,可以有条件的删除:

<a href="/something" th:remove="${condition}? tag : none">不删除链接文本</a>

还可以看到,在th:remove中,none和null是同义词,所有,这个例子还可以改为:

<a href="/something" th:remove="${condition}? tag">不删除链接文本</a>

这种情况下,如果条件为false,这回返回null,所以不会有任何删除动作。

局部变量

Thymeleaf将一个特定块内的变量称为局部变量,它只能在这个块内使用。

比如说,在产品列表页中的迭代值:

<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>
    第一个人的名字为:<span th:text="${firstPer.name}">张三</span>.
  </p>
</div>

当处理th:with的时候,firstPer变量将被创建为一个局部变量并且添加到context的变量map中,所以它可以开始象其它任何变量一样的使用,但是只能在div这个节点内部。

通常可以使用复合定义的语法来同时定义多个变量:

<div th:with="firstPer=${persons[0]},secondPer=${persons[1]}">
  <p>
    第一个人的名字为 <span th:text="${firstPer.name}">张三</span>.
  </p>
  <p>
    第二个人的名字为 
    <span th:text="${secondPer.name}">李四</span>.
  </p>
</div>

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

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

可以在网络商城中使用这个,还记得之前的格式化代码么:

<p>
  当前日期为: <span th:text="${#calendars.format(today,'yyyy-MM-dd')}">2016-9-5</span>
</p>

其实,我们想要的时间格式,如yyyy-MM-dd取决于区域,所以,我们可以将格式信息保存在home_zh_CH.properties:

date.format=yyyy-MM-dd

现在,我们使用th:with获取本地化的日期格式,并把它设置为局部变量,并在th:text表达式中使用它:

<p th:with="df=#{date.format}">
   当前日期为: <span th:text="${#calendars.format(today,df)}">2016-9-5</span>
</p>

很清晰和容易,事实上,考虑到th:with比th:text的优先级更高,所以我们还可以在span标签上解决这个问题:

<p >
   当前日期为: <span  th:with="df=#{date.format}" th:text="${#calendars.format(today,df)}">2016-9-5</span>
</p>

如果你想了解优先级,请看下一章的内容

属性优先级

想一下,当在一个标签上写了多个th:*属性的时候,会发生什么?例如:

<ul>
  <li th:each="item : ${items}" th:text="${item.description}">项目内容</li>
</ul>

当然,我们可以看到,th:each属性会在th:text之前执行,才能得到我们想要的结果,但是鉴于DOM标准对于是不关心属性书写的顺序,所以,优先级必须建立在th:*的属性本身,才能确保它们会如预期工作。

所以,所有的Thymeleaf属性都会有一个数值型的优先,来说明它的执行顺序,这个优先级列表为:

优先级特征属性
1包含块th:insert
th:replace
2迭代块th:each
3条件语句th:if
th:unless
th:switch
th:case
4局部变量声明th:object
th:with
5一般属性修改th:attr
th:attrprepend
th:attrappend
6特定属性修改th:value
th:href
th:src
...
7标签内容设置th:text
th:utext
8片段定义th:fragment
9片段删除th:remove

这个优先级意味着,调换th:*属性的位置,会得到相同的结果,如:

<ul>
  <li th:text="${item.description}" th:each="item : ${items}">项目内容</li>
</ul>

注释和说明

HTML/XML的标准注释

在HTML和XML的标准注释<!--...-->可以使用在Thymeleaf模板的任何地方。这些注释不会被Thymeleaf进行任何处理,并且会复制到输出结果中:


...

Thymeleaf可解释的注释块

可解释的注释块在Thymeleaf模板进行解析的时候会删除,他的格式是这样的:

<!--/* 这是一段Thymeleaf可解析的注释 */-->

Thymeleaf将删除一切处于<!--/**/-->之间的内容, 所以这些注释也可用用来当做静态文件的显示模板,当Thymeleaf执行的时候,他就会被删除,如:

<!--/*--> 
  <div>
     只能在Thymeleaf处理前看到
  </div>
<!--*/-->

这可以为table的原型显示提供很多方便,如:

<table>
   <tr th:each="x : ${xs}">
     ...
   </tr>
   <!--/*-->
   <tr>
     ...
   </tr>
   <tr>
     ...
   </tr>
   <!--*/-->
</table>

Thymeleaf的原型注释

Thymeleaf允许一种特殊的注释块,他在原型的时候是注释的,但执行的时候被认为是正常Thymeleaf标记。

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

因为Thymeleaf在执行的时候只会删除<!--/*//*/-->之间的注释标记而不是内容,所以在执行模板的时候,将还是可以看到:

<span>hello!</span>

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

正如看到的一样,注意此特性是独立于方言的。

th:block标签

Thymeleaf标准方言中,唯一的一个运行时标签(不是属性)就是th:black标签

th:block是一个纯粹的属性容器,可以让开发人员制定他们想要的任何属性。Thymeleaf将执行这些属性并同时使block消失。

它的用处是很多的,比如当创建一个table,但需要同时迭代两个以上的tr的时候:

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

尤其方便的是,它使用的是原型注释:

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

注意,该方式是一个标准的html模板,即不能再table内嵌套使用div,所以可以在静态页面的时候正常的打开显示。

内联表达式

表达式内联

虽然标准方言的标签属性几乎可以做任何事,但在有些情况下,我们可以直接使用HTML文本来书写表达式,例如,你可以这样写:

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

他和这样写是等价的:

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

[[...]][(...)]这种表达式在Thymeleaf中叫做内联表达式,你可以使用任何形式的表达式,包括th:text或th:utext属性

需要注意的是,[[...]]对应的是th:text,[(...)]对应的是th:utext,所以变量msg=这个<b>真棒</b>,加入使用片段:

<p>信息值为: "[(${msg})]"</p>

输出结果为:

<p>信息值为: 这个<b>真棒</b></p>

而如果这样:

<p>信息值为: "[[${msg}]]"</p>

则输出结果为:

<p>信息值为: 这个&lt;b&gt;真棒&lt;/b&gt;</p>

需要注意的是,文本内联在body内的每个标记的都默认激活,所以我们可以直接使用。

内联还是自然模板

那么现在你可能要问了:为什么不从一开始就使用内联表达式呢,他的代码量比属性少多了。

嗯,这是因为,你可能发现内联很好用,但你要永远记住,内联表达式在你的html中是完全按照文字打开显示的,所以,你就无法用他作为原型了。

比如,若果不适用内联,直接作为静态打开将显示:

Hello,张三

而使用内联后:

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

在设计方面的区别还是很明显。

禁用内联

这个机制还是可以禁用的,比如有时候[[...]]或[(...)]是作为内容输出的。对于这点,我们可以使用th:inline="none";

<p th:inline="none">一个二维数组: [[1, 2, 3], [4, 5]]!</p>

执行结果为:

<p>一个二维数组: [[1, 2, 3], [4, 5]]!</p>

内联文本

内联文本与刚刚我们看到的内联表达式非常的相似,但它实际上有更多的功能,并且必须显式启用:th:inline="text"

内联文本不仅允许我们使用与刚刚看到相同的表达式形式,但其实他的处理方式比较像文本模板处理模式,即可以在里边处理文本模板逻辑,而不仅仅是一个输出的表达式。

我们将看在下一章文本模板模式中看到更多内容。

内联JavaScript

内联JavaScript可以更好的在HTML模板模式下整合JavaScript的脚本块。

就像内联文本一样,这实际上是相当于它们在JavaScript模板模式下处理脚本内容,执行的是文本模板模式的功能(下一章介绍),这一章将着重介绍如何在JavaScript块中使用Thymeleaf表达式。

这种模式必须明确声明启用 th:inline="javascript":

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

将输出结果为:

<script th:inline="javascript">
    ...
    var username = "张\"老\"三";
    ...
</script>

这里有两点需要注意:

首先,内联JavaScrip不但能输出普通文字,还会队内容自动进行转义和JavaScript的编码,所以输出的结果一般会非常符合JavaScript的要求。

其次,如果我们不想进行转义,则使用[(...)],如:

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

返回结果为:

<script th:inline="javascript">
    ...
    var username = 张"老"三;
    ...
</script>

很明显,这是一段错误的代码,但有事我们就是需要一些非转义的东西,所以如果想通过内联来构筑脚本,手头最好有这个工具。

JavaScript的自然模板

上述的内联JavaScript机制要不仅仅运用JavaScript特性的表达式更为有效。

举例来说,我们可以将内联表达式使用JavaScript方式注释,如:

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

Thymeleaf将忽略注释和分号之前的一切("张三"),所以他的执行结果和不使用注释包装一模一样:

<script th:inline="javascript">
    ...
    var username = "张\"老\"三";
    ...
</script>

但是仔细看看这个模板代码:

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

它是一段完全正确的JavaScript代码,当在浏览器中直接打开的时候,它还是会正确的执行,并几乎和在服务器执行的结果一样。

所以这是一段JavaScript自然模板。

内联执行的高级方式和JavaScript序列化

Thymeleaf的内联执行是非常智能的,它不限于字符串,Thymeleaf会正确的创建JavaScript中以下的各种类型的对象:

  • 字符串
  • 数值
  • 布尔型
  • 数组
  • 集合
  • Map
  • JavaBean

比如说,下面的代码:

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

${session.user}是一个User对象,执行后的结果为

<script th:inline="javascript">
    ...
    var user = {'id':3,'age':27,'name':'张三'};
    ...
</script>

这个JavaScript完成序列化方式是通过org.thymeleaf.standard.serializer.IStandardJavaScriptSerializer接口实现,可以在当前模板引擎中配置标准方言使用的序列化方式实例。

这个JS的序列化机制的默认是查询classpath,如果有JackSon库则使用它完成序列化,如果没有,他还内置了一个序列化工具,可以涵盖大多数的情况,但不太灵活。

内联CSS

Thymeleaf也可以使用内联CSS标签,像这样:

<style th:inline="css">
  ...
</style>

举个例子,需要为两个变量需要设置为两个不同的字符串值:

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}]]输出的时候会自动转换,所以上文中的 classname='main elems'被转换为main elems

高级功能:CSS自然模板等

同JavaScript一样,内联CSS样式也可以静态或动态展示,即可以使用注释手段的方式实现自然模板。比如:

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

文本语言模板模式

文本语法

Thymeleaf有三种文本语言模板模式(Text,JavaScript,CSS),它们与标记语言模板模式有一些区别。

文本模板模式与标记语言模板模式的一个主要区别是,在文本模板中,没有标记,所以也就没有办法以属性形式插入逻辑标签,所以,我们必须依靠其它机制插入逻辑。

这些机制最基本的方式,就是我们前边已经详细介绍过的内联。内联语法是在文本语言模板模式中输出结果的最简单的表达式。用一个完整的电子邮件的模板文件作为例子:

[(${name})] 您好,

请在附件中查看您所要求的报告
名字为: "[(${report.name})]".

此致
    敬礼
A方项目经理 张三.

注意上面这个完整有效的Thymeleaf文本模板中,没有任何标记,但它完全可以正常执行。所以我们不应再标记语言模板模式中内联文本语言模板,而应直接在文本模板模式中直接执行。

而为了支持比单纯输出更复杂的逻辑,我们需要一个基于非标记的新语法形式:

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

它实际上是一个简写版:

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

注意这个新语法是基于元素(element,即加工后的标签)的,注意定义形式为[#element]而不是.元素和标签类似,使用[#element]和[/element]来确定一个封闭区间。还可以使用自关闭模式[#element.../]

标准方言的处理器实际上跟只可以使用一个元素,就是之前介绍过的th:block,虽然在自定义方言中可以扩展并创建新的元素。另外,th:block元素([#th:block ...]...[/th:block])可以直接缩写,([# ...]...[/]),所以上边的代码相当于:

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

当然[#th:utext="${item}"]相当于一个内联转移表达式,所以我们也可以在此使用,用来减少代码,所以,最终我们看到的代码为:

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

注意文本模式语法要求必须为全平衡元素(即没有非闭合标签)和属性,所以,它与html比,更像XML的模式。

下面一个更完整的文本模板模式例子,一个电子邮件的模板:

[(${customer.name})],您好:

这是我们的产品列表:

[# th:each="prod : ${products}"]
   - [(${prod.name})]  价格为: [(${prod.price})] 元/kg
[/]

谢谢,
  古泰商城

执行后输出的结果为:

张先生,您好:

这是我们的产品列表:

[# th:each="prod : ${products}"]
   - 土豆  价格为: 2.30元/kg
   - 白菜  价格为: 1.20元/kg
   - 西瓜  价格为: 1.60元/kg
   - 红薯  价格为: 3.50元/kg
[/]

谢谢,
  古泰虚拟商城

在举一个JavaScript模板模式的例子,在我们的HTML页面,需要调用一个greeter.js文件,它使用文本模板创建。注意,他不是的方式内嵌到HTML文件中,而是以一个单独的js文件作为模板处理:

var greeter = function() {

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

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

};

执行后的结果为:

var greeter = function() {
    var username = "张三";
    alert("Hello" + " " + username);
    alert("Ol\u00E1" + " " + username);
    alert("Hola" + " " + username);
};

元素属性转义

为了防止模板可能与其他的模板处理方式发生冲突(如文本模式内联在html模板中),Thymeleaf3.*的文本模式语法可以在元素的属性中使用转义

  • Text模板模式的属性将会使用HTML非转义字符
  • JavaScript模板模式的属性将会使用JavaScript非转义字符
  • CSS模板模式将会使用CSS非转义字符

所以,下边的代码将会正常的执行(注意其中的&lt)

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

当然,在一个真实的模板中,&lt是个错误的语法,但如果在执行一个html模板的内联text的时候,它就要起作用了,因为在上边的代码中,我们不希望浏览器认为<user.age是一个开放的静态标签。

可扩展性

此语法有一个有点就是它和标记一样有扩展性。用户仍然可以使用自定义元素或属性来定义方言,以前缀的方式应用(可选),然后在文本模板模式中使用它们。

[#myorg:dosomething myorg:importantattr="211"]一些内容[/myorg:dosomething]

使用文本原型注释:添加代码

如果要为JavaScript和CSS模板模式(文本模板模式不可以)添加额外的代码,则在内联JavaScript中使用一个特殊的语法/*[+...+]*/,Thymeleaf在模板执行的时候讲自动取消此注释:

var x = 23;
/*[+
var msg  = '这段代码为后台输出';
+]*/
var f = function() {
...

执行结果为:

var x = 23;
var msg  = '这段代码为后台输出';
var f = function() {
...

可以在这段注释中添加表达式,并执行:

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

使用文本原型注释:删除代码

同样的,也可以让Thymeleaf删除/*[-*//*-]*/之间的代码(三种文本模板模式均可):

var x = 23;
/*[- */
var msg  = '执行后就没有了';
/* -]*/
var f = function() {
...

或者在文本中使用:

...
/*[- 用户从会话中获得 -]*/
[(${session.user.name})] 您好!
...

自然JavaScript和Css模板

正如上一章看到的,JavaScript和CSS内联提供了在内敛表达式内使用JavaScript和CSS注释的可能,比如:

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

执行后为一个正常的JavaScript,结果为:

... 
var username = "李四";
...

这种欺骗内联表达式的语法实际上可以在全部的文本模式语法中使用:

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

这个alert语句当模板直接在浏览器中打开是会显示出来,因为是个完全正确的JavaScript代码,而当模板在运行后,当用户为管理员的时候alert才会弹出,因为它执行后的结果为:

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

实际上这是模板在分析过程中的初始转换后的代码。

但是注意,包住元素的注释在发现右边的逗号之前并不删除里边的内容。这种行为是只保留内联表达式的输出。

由此可见,Thymeleaf3.*可以开发出符合自然脚本形式的复杂的JavaScript或CSS。

为我们的商店制作更多的页面

现在我们已经了解了Thymeleaf很多的知识,我们可以添加更多的页面在我们的系统中,比如订单管理。

从现在开始假定你已经对controller很熟悉,而专注于html代码部分

订单列表

现在开始创建订单列表页,/WEB-INF/templates/order/list.html:

<body>
<h1>订单列表</h1>
<table>
    <tr>
        <th>创建日期</th>
        <th>客户</th>
        <th>总价</th>
        <th></th>
    </tr>
    <tr th:each="o:${orders}" th:class="${oStat.odd}?'odd'">
        <td th:text="${#dates.format(o.date,'yyyy-MM-dd')}">2016-9-9</td>
        <td th:text="${o.customer.name}">张三</td>
        <td th:text="${#numbers.formatDecimal(#aggregates.sum(o.orderLines.{purchasePrice*amount}),0,2)}">100</td>
        <td>
            <a href="details.html" th:href="@{/order/details(orderId=${o.id})}">查看</a>
        </td>
    </tr>
</table>
<p>
    <a href="../home.html" th:href="@{/}">回到首页</a>
</p>
</body>

这里应该没有什么可说明的,除了这一点点OGNL的魔术写法:

<td th:text="${#numbers.formatDecimal(#aggregates.sum(o.orderLines.{purchasePrice*amount}),0,2)}">100</td>

中的:

#aggregates.sum(o.orderLines.{purchasePrice*amount}

这句话的意思是:迭代order变量中的每一个OrderLine对象,获取它的purchasePrice和amount属性,返回他们相乘后的结果,然后把迭代的结果(一个价格列表)通过#aggregates.sum方法求和。获取订单总价。

这就是OGNL语法的强大力量。

订单详情

下面是一个订单详情页,这里使用了大量的*{...}语法

<body th:object="${order}">
<h1>订单详情</h1>
<div>
    <p><b>编号</b><span th:text="*{id}">99</span></p>
    <p><b>生成日期</b>
    <span th:text="*{#dates.format(date,'yyyy-MM-dd')}">2016-9-10</span></p>
</div>
<h2>顾客信息</h2>
<div th:object="*{customer}">
    <p><b>顾客姓名</b><span th:text="*{name}">张三</span></p>
    <p><b>生日</b>
    <span th:text="*{#dates.format(customerSince,'yyyy-MM-dd')}">1985-2-26</span>
    </p>
</div>
<h2>商品信息</h2>
<table>
    <tr>
        <th>产品名称</th>
        <th>数量</th>
        <th>购买价格</th>
    </tr>
    <tr th:each="ol,row:*{orderLines}" th:class="${row.odd}?'odd'">
        <td th:text="${ol.product.name}">土豆</td>
        <td th:text="${ol.amount}">3</td>
        <td th:text="${#numbers.formatDecimal(ol.purchasePrice,0,2)}">23.2</td>
    </tr>
</table>
<div>
<b>TOTAL:</b>
<span th:text="*{#numbers.formatDecimal(#aggregates.sum(orderLines.{purchasePrice*amount}),0,2)}"></span>
</div>
<p><a href="list.html" th:href="@{/order/list}">返回列表</a></p>
</body>

注意这里,这里是一个嵌套的*{...}

<body th:object="${order}">
...
<div th:object="*{customer}">
    <p><b>顾客姓名</b><span th:text="*{name}">张三</span></p>
    ...
</div>
...
</body>

之类的*{name}实际上相当于:

<p><b>顾客姓名</b><span th:text="${order.customer.name}">张三</span></p>

更多的配置

模板解释器

在样例的网络商城中,我们选择了一个实现了ITempResolver接口的ServletContextTemplateResolver实现来让我们从ServletContext获取模板资源。

实现ITempResolver除了这个创建自己的模板解析器之外,Thymeleaf还提供了其他三个实现:

  • org.thymeleaf.templateresolver.ClassLoaderTemplateResolver,通过一个资源文件来获取模板资源,如:

      return Thread.currentThread().getContextClassLoader().getResourceAsStream(templateName);
  • org.thymeleaf.templateresolver.FileTemplateResolver他从文件系统中解析文件为模板文件,比如:

      return new FileInputStream(new File(templateName));
  • org.thymeleaf.templateresolver.UrlTemplateResolver他从一个URL(甚至可以为非本地url)来解析模板文件,如:

      return (new URL(templateName)).openStream();
  • org.thymeleaf.templateresolverStringTemplateResolver它直接使用String作为模板(或者模板名称,在这种情况下,远不是一个单纯的名字):

      return new StringReader(templateName);

所有这些都实现了ITemplateResolver接口,所以可以配置相同的参数,包括:

  • 前缀和后缀:

      templateResolver.setPrefix("/WEB-INF/templates/");
      templateResolver.setSuffix(".html");
  • 模板别名可以使模板名称不直接对应文件名,如果别名,前缀后缀都存在,那么别名将在前后缀之前使用:

          templateResolver.addTemplateAlias("adminHome","profiles/admin/home");
          templateResolver.setTemplateAliases(aliasesMap);
  • 读取模板时要应用的编码:

      templateResolver.setEncoding("UTF-8");
  • 设置模板模式

      templateResolver.setTemplateMode("HTML5");
      templateResolver.getXhtmlTemplateModePatternSpec().addPattern("*.xhtml");
  • 缓存的默认模式和指定特定的模板进行缓存:

      templateResolver.setCacheable(false);
      templateResolver.getCacheablePatternSpec().addPattern("/users/*");
  • 设置缓存的生存时间,如果没有设置,则删除缓存的唯一途径是LRU算法(即超出缓存最大空间后删除最早的一条)

      templateResolver.setCacheTTLMs(60000L);

Thymeleaf+Spring集成包提供了一个SpringResourceTemplateResolver的实现,使用Spring来访问和应用资源。

链接模板解析器

同时,模板引擎可以指定多个模板解析器。在这种情况下,应该为模板解释器设置一个顺序,即如果第一个解析器不能解析模板,则使用第二个,以此类推。

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

ServletContextTemplateResolver servletContextTemplateResolver = new ServletContextTemplateResolver(servletContext);
servletContextTemplateResolver.setOrder(Integer.valueOf(2));

templateEngine.addTemplateResolver(classLoaderTemplateResolver);
templateEngine.addTemplateResolver(servletContextTemplateResolver);

当应用多个模板解释器的时候,建议明确为解释器匹配特定的模板,这样,Thymeleaf框架可以快速的剥离出无用的解释器和不相符的模板,以便提高性能,这并不是必须的,只是一个优化:

ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
// classloader将不会匹配任何非此两种模式的模板。
classLoaderTemplateResolver.getResolvablePatternSpec().addPattern("/layout/*.html");
classLoaderTemplateResolver.getResolvablePatternSpec().addPattern("/menu/*.html");

ServletContextTemplateResolver servletContextTemplateResolver = new ServletContextTemplateResolver();
servletContextTemplateResolver.setOrder(Integer.valueOf(2));

如果这些解释器没有明确的指定,我们将依靠每一个正在使用的ITemplateResolver的实现。注意并不能确定每一个模板真正的存在,所以可以考虑模板解析可以考虑解决后终端模板解析链,但这样可能会无法阅读到真正的资源。

另外,ITemplateResolver还实现了分布式,包括一种在资源解析之前检查是否真的存在这个资源的机制,是checkExistence标记,设置方式如下:

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

checkExistence标记强制解析器在解析阶段对资源执行检查,让下面的解析器如果存在,则返回false。听起来似乎在任何情况下都是一件好事,但需要注意,这意味着在大多数情况下,需要访问资源两次(检查一次,读取一次),在一些情况下,可能会发生一些性能问题,如基于远程的URL资源,但一般情况下性能问题可以通过缓存来解决(这时候模板只会解析它的第一次访问)

信息解释器

在示例应用中,我们并没有显示的指定一个信息解释器的实现,因为在之前解释过,这意味着我们使用的是org.thymeleaf.messageresolver.StandardMessageResolver这个类。

StandardMessageResolver类是IMessageResolver接口的标准实现,当然,也可以自己通过实现org.thymeleaf.messageresolver.IMessageResolver接口来实现自己的消息解释器。

在Thymeleaf+Spring集成包中提供了一个使用Spring标准信息检索功能的实现,叫MessageSource对象。

标准信息解释器

StandardMessageResolver将寻找与模板名称相同的(不包含本地化后缀)properties文件,比如模板名称为home,路径为/WEB-INF/template/home.html,本地区域为zh_CN,则解释器读取文件的顺序为:

  • /WEB-INF/template/home_zh_CN.properties
  • /WEB-INF/template/home.properties

关于StandardMessageResolver的更多细节请参考文档。

配置消息解释器

如果想添加一个或多个信息解释器在TemplateEngine中,可以这样:

// 设置一个
templateEngine.setMessageResolver(messageResolver);

// 设置多个
templateEngine.addMessageResolver(messageResolver);

为什么会有多个消息解释器呢?是与模板解释器同样的原因,消息解释器是有序的,如果第一个不能出来,则使用第二个,以此类推。

转换服务

转换操作可以使我们实现数据转换和格式化操作的一个双支撑的语法,实际上是标准方言的一个特性,而不是Thymeleaf模板引擎提供。

因此,配置是通过设置一个自定义的IStandardConversionService接口的实现注入到模板引擎正在配置的StandardDialect实例:

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

注意thymeleaf-spring3和thymeleaf-spring4中的SpringStandardDialect中已经预先配置好了基于Spring转换服务的一些IStandardConversionService的接口实现。

日志

Thymeleaf相当关心日志,总是试图通过日志接口提供最大数量的有用信息。
他的日志库使用的是slf4j,事实上他只是一个在你自己应用程序中使用日志的桥梁。

Thymeleaf根据需求,支持TRACE,DEBUG和INFO的日志级别,除了通用的日志,TemplateEngine类还根据配置的不同用途,支持三个特殊的日志相关功能:

  • org.thymeleaf.TemplateEngine.CONFIG在初始化过程中会输出配置的详细信息
  • org.thymeleaf.TemplateEngine.TIMER将输出在处理每个模板所花费的时间(非常有用的一个标准)
  • org.thymeleaf.TemplateEngine.cache是一个设置日志输出缓存特定信息的前缀。缓存记录的名称是用户配置并且个改变的,他的默认名称为:

    • org.thymeleaf.TemplateEngine.cache.TEMPLATE_CACHE
    • org.thymeleaf.TemplateEngine.cache.EXPRESSION_CACHE

比如配置一个log4j的日志,可以这样设置:

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

模板缓存

Thymeleaf的功能要归功于一套标记和文本的解析器,它将模板解析为事件序列(开标签,文本,闭标签,注释,等等)和一系列处理器,它们为了使数据与DOM树想结合而修改一系列的节点。

他还包括一个可以默认存储已解析模板的缓存,可以在读取和解析模板文件的事件序列之前处理这些缓存。这在处理WEB应用程序的时候,是非常有用的,因为:

  • 输入和输出在一个程序中,与内存中的的过程相比,通常都是最慢的。
  • 克隆一个内存中现有的事件序列总是比从硬盘中读取一个模板文件并解析并创建一个事件序列要快。
  • web应用程序的模板数量通常很少, 一般不超过十几个。
  • 模板文件的文件大小通常都不大,并且在运行的时候通常不会被修改。

以上这些原因,说明缓存最常用的模板在一个web应用程序中是可行的,它不会浪费很多内存,但会节约大量的,花在输入输出操作上的一小部分几乎不会改变的文件上的时间。

现在说明我们该如何控制这个缓存:首先,我们在之前就已经知道了可以在模板解析器中启用或禁用它,对全局或者特定的模板进设置:

//默认值为true
templateResolver.setCacheable(false);
templateResolver.getCacheablePatternSpec().addPattern("/users/*");

另外,还可以通过建立自己的缓存管理器对象并修改它的配置,这个缓存管理器对象可以是默认的StandardCacheManager的一个实例:

//默认值为50
StandardCacheManager cacheManager = new StandardCacheManager();
cacheManager.setTemplateCacheMaxSize(100);
...
templateEngine.setCacheManager(cacheManager);

更多配置请参阅org.thymeleaf.cache.StandardCacheManager的API文档.

手动清除缓存:

// 清除全部
templateEngine.clearTemplateCache();

// 清除特定模板
templateEngine.clearTemplateCacheFor("/users/userList");

解耦模板逻辑

解耦逻辑:概念

到目前为止,我们的网络虚拟商城的模板都使用了通常的方式,即将逻辑插入到模板属性的形式。

但Thymeleaf也可以让我们从模板标记中完全的解耦逻辑,允许为xml或html创建一个完全无逻辑标记模板。

主要方式是,模板逻辑被定义在一个单独的逻辑文件中(更精确的说是一个逻辑资源,因为它不要求是一个文件),默认情况下,逻辑被定义为一个与模板文件同目录下的文件,使用相同的名称,但扩展名为.th.xml

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

这时候html就可以完全不包含逻辑属性,它的html代码为:

<!DOCTYPE html>
<html>
  <body>
    <table id="usersTable">
      <tr>
        <td class="username">张三</td>
        <td class="usertype">普通用户</td>
      </tr>
      <tr>
        <td class="username">李四</td>
        <td class="usertype">管理员</td>
      </tr>
    </table>
  </body>
</html>

没有任何逻辑在里边,任何一个没有任何Thymeleaf知识的HTML设计人员都可以创建,编辑,理解和查看,并且可以提供给任何一个没有Thymeleaf环境的应用使用。

想让这个模板称为一个完美的Thymeleaf模板需要创建一个home.th.xml文件:

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

这里可以看到,很多的标签包在块中,这些标签通过sel选择节点,并执行将含有Thymeleaf选择器标记(AttoParser)的属性注入到原始模板的节点中的操作

应该注意到在附加选择器的时候,标记还可以嵌套.例如在sel="/tr[0]",将被处理为sel="#userTabe/tr[0]",并将username所在的td处理为sel="#userTable/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}">张三</td>
        <td class="usertype" th:text="#{|user.type.${user.type}|}">普通用户</td>
      </tr>
      <tr>
        <td class="username">李四</td>
        <td class="usertype">管理员</td>
      </tr>
    </table>
  </body>
</html>

虽然创建了两个文件确实减少了啰嗦,但感觉还是上边的代码显得更加清晰。其实使用解藕的好处是使我们的代码可以完全独立于Thymeleaf框架,更方便设计人员的开发和维护。

当然,开发和设计人员还是需要一些必要的沟通的,比如他们需要确定一个固定的id=usersTable,但无论如何,一个纯html还是会为开发人员和设计人员带来更好的沟通。

配置解藕模板

激活解藕模板

在默认的情况下,模板是不能使用解藕模板的。需要在模板解释器中进行配置来实现模板的逻辑解藕。

除了StringTemplateResolver(这个不能使用逻辑解藕),所有 其它解析器均可以直接使用userDecoupledLogic标记来设置全部或部分的模板使用一个单独的逻辑资源。

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

混合耦合和逻辑解藕

模板逻辑解藕在启用时,并不是必须的,它的启用只意味着引擎将去寻找一个包含解藕逻辑的资源,进行与原来模板的分析和合并,如果没有找到或不存在,将不会发生任何错误。

另外在同一个模板中可以同时使用这两种耦合和解藕的形式,比如在模板中有一些Thymeleaf属性,同时另一些属性在一个独立的解藕逻辑文件中,最常见的情况是使用3.0中新增的th:ref属性。

th:ref属性

th:ref只是一个标记属性,它从执行的角度来看,只是简单的消失,但它的实用性在意他作为一个引用的标记,即它可以让逻辑文件通过选择器找到它所代表的名词,就像是一个标签名或一个片段(th:fragment)

比如有个sel属性是这样:

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

那么将匹配:

  • 标签
  • 带有th:fragment="whatever"属性的标签
  • 带有th:ref="whatever"属性的标签

th:ref的优势是什么?比如和纯使用html的id相比.其实仅仅一点就够了,我们一般不想使用特别多的id和class属性仅仅作为一个逻辑锚,因为它实际对于最终的结果html很重要。

那么th:ref的缺点又是什么呢?也很显然,我们需要添加一点逻辑到html的文档中。

注意一点th:ref属性不仅适用于解藕的逻辑模板文件,它在其他场景,如fragment表达式中执行的原理相同。

解藕模板对性能的影响

解藕对性能的影响非常的小。当一个没有被缓存的使用逻辑解藕的模板进行解析时,模板的资源文件将首先被解析,并加工成一个顺序的指令存入内存中:基本上属性列表将均将被注入到选择器标记中。

但这一步是解藕模板唯一需要的额外步骤,因为在这之后,将解析真正模板,而它被解析的时候,通过AttoParser的高级功能,那些资源中的属性将被随手注入到模板中,因此,解析后解析两种模板模式在效率上实际没有什么区别。

这样做最大的优势是,缓存将直接记住已经注入属性的模板,所以一点被缓存之后,使用逻辑解藕的模板的额外开销为零。

分别解藕逻辑模板

Themeleaf解析器提供了一个用户为每一个模板配置解藕逻辑资源的方式,由org.thymeleaf.templateparser.markup.decouple.IDecoupledTemplatedLogicResover接口提供了一个扩展点,它有一个默认实现为StandardDecoupledTemplateLogicResover.

这个实现的主要工作有两点:

  • 首先,它会将一个前缀和后缀应用到模板资源的基础名字(通过ITemplateResource#getBaseName()方法获取)上,前缀和后缀都是可配置的,默认情况下,前缀为空,后缀为.th.xml.
  • 其次,它要求模板资源根据计算的名称解析一个相对资源,相对名称通过ITemplateResource#relative(相对位置的字符串)方法

对StandardEdcoupledTemplateLoginResover在TemplateEngin中配置很容易:

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

...
templateEngine.setDecoupledTemplateLogicResolver(decoupledResolver);

参考:
http://www.tuicool.com/articles/ayeQ3qn

转载于:https://www.cnblogs.com/jiangchao226/p/5900222.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值