1.Thymeleaf简介
1.1 Thymeleaf是什么
Thymeleaf是⾯向Web和独⽴环境的现代服务器端Java模板引擎,能够处理HTML,XML,JavaScript,CSS甚⾄纯⽂本。
Thymeleaf旨在提供⼀个优雅的、⾼度可维护的创建模板的⽅式。为了实现这⼀⽬标,Thymeleaf建⽴在⾃然模板的概念上,将其逻辑注⼊到模板⽂件中,不会影响模板设计原型。这改善了设计的沟通,弥合了设计和开发团队之间的差距。
Thymeleaf从设计之初就遵循Web标准——特别是HTML5标准,如果需要,Thymeleaf允许你创建完全符合HTML5验证标准的模板。
1.2 Thymeleaf能处理哪些模版
开箱即⽤,Thymeleaf可以处理六种类型的模板,每种类型的模板称为模板模式:
- HTML
- XML
- TEXT
- JAVASCRIPT
- CSS
- RAW
这六种模版模式包含两种标记模板模式(HTML和XML),三种⽂本模板模式(TEXT,JAVASCRIPT和CSS)和⼀个⽆操作模板模式(RAW)。
1.3 Thymeleaf标准方言
Thymeleaf是⼀个扩展性很强的模板引擎(实际上它可以称为模板引擎框架)。将⼀些逻辑应⽤于标记组件(标签,某些⽂本,注释或只有占位符)的⼀个对象被称为处理器,通常这些处理器的集合以及⼀些额外的组件就组成 了Thymeleaf⽅⾔。Thymeleaf的核⼼库提供了⼀种称为标准方言的方言,这对⼤多数⽤户来说应该是⾜够的。
当然,如果用户希望在使⽤标准⽅⾔库的⾼级功能的同时还想定义自己的处理逻辑,你也可以创建⾃⼰的方言(甚⾄扩展标准的方言)。你也可以将Thymeleaf配置为⼀次使用几种方言。
官⽅的thymeleaf-spring3和thymeleaf-spring4的整合包⾥都定义了 ⼀种称为“spring标准⽅⾔”的⽅⾔,该⽅⾔与“Thymeleaf标准⽅⾔”⼤致相同,但是对于Spring框架中的某些功能(例如,通过使⽤ SpringEL表达式代替OGNL表达式)做了⼀些简单的调整。所以如果你是⼀个SpringMVC⽤户,这份Thymeleaf使用手册并不会浪费你的时间,因为你在这⾥学到的所有东⻄都将可以应⽤到你的Spring应⽤程序中。
Thymeleaf标准方言中的大多数处理器都是属性处理器。这样,即使在模版未被处理之前,浏览器也可以正确地显示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}
的值代替value的真实值。这有助于你的设计师和开发⼈员处理相同的模板⽂件,并减少将静态原型转换为⼯作模板⽂件所需的工作量。具备这种能⼒的模版我们称为⾃然模板。
2.示例项目
2.1 一个实例网站
为了更好地解释使用Thymeleaf模板所涉及的概念,本文将通过⼀个示例程序来说明,该示列程序可以从站点下载。这个示例程序是⼀个简单的Thymeleaf功能展示项目,并将为我们提供许多场景来展示Thymeleaf的许多功能。
在Web层,我们的应⽤程序有⼀个过滤器,将根据请求URL将执行委托给启⽤Thymeleaf的命令:
private boolean process(HttpServletRequest request, HttpServletResponse response)
throws ServletException {
try {
// 防止触发资源url的引擎执行
if (request.getRequestURI().startsWith("/css") ||
request.getRequestURI().startsWith("/images") ||
request.getRequestURI().startsWith("/favicon")) {
return false;
}
/*
* 查询控制器/URL映射并获取将处理请求的控制器。
* 如果没有控制器可用,返回false并让其他过滤器/servlet处理请求。
*/
BaseController controller = this.application.resolveControllerForRequest(request);
if (controller == null) {
return false;
}
/*
* 获取TemplateEngine实例。
*/
ITemplateEngine templateEngine = this.application.getTemplateEngine();
/*
* 请求响应头。
*/
response.setContentType("text/html;charset=UTF-8");
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
/*
* 执行控制器和进程视图模板,并返回结果。
*/
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);
}
}
这是我们的BaseController接⼝:
public interface BaseController {
public void process(
HttpServletRequest request, HttpServletResponse response,
ServletContext servletContext, ITemplateEngine templateEngine)
throws Exception;
}
我们现在要做的就是创建BaseController接⼝的实现,并且从服务层中获取数据然后⽤ITemplateEngine对象处理模板并完成数据渲染。最后程序运⾏结果如下所示:
接下来我们先看⼀下模版引擎是如何初始化的。
2.2 创建和配置模版引擎
在过滤器的process
方法中包含下面一行代码:
ITemplateEngine templateEngine = this.application.getTemplateEngine();
这意味着Application
类负责创建和配置Thymeleaf应⽤程序中最重要的对象之⼀:TemplateEngine
实例(ITemplateEngine
接口的实现)。
我们的org.thymeleaf.TemplateEngine
对象初始化如下:
public class Application {
//...
private TemplateEngine templateEngine;
//...
public Application(final ServletContext servletContext) {
super();
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);
// HTML是默认模式,但是为了更好地理解代码,我们还是会设置它。
templateResolver.setTemplateMode(TemplateMode.HTML);
// This will convert "home" to "/WEB-INF/templates/home.html"
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
// 设置模板缓存TTL为1小时。如果没有设置,将留缓存中,直到LRU将其清除。
templateResolver.setCacheTTLMs(Long.valueOf(3600000L));
// 默认情况下,缓存设置为true。如果你希望模板在修改时被自动更新,则设置为false。
templateResolver.setCacheable(true);
this.templateEngine = new TemplateEngine();
this.templateEngine.setTemplateResolver(templateResolver);
//...
}
//...
}
这⾥有很多⽅法可以配置TemplateEngine
对象,但本示例程序中的这⼏⾏配置模版引擎代码将⾜够教给我们所需的步骤。
2.2.1 模版解析器
我们从模板解析器开始:
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);
模板解析器对象是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 templ ate, final Map<String, Object> templateResolutionAt tributes);
}
在我们的应⽤程序中,我们使用了org.thymeleaf.templateresolver.ServletContextTemplateResolver
,这意味着我们将从Servlet上下⽂中获取我们的模板⽂件资源:Java Web应⽤程序范围内都存在 javax.servlet.ServletContext
对象,并从 Web应⽤程序根⽬录中解析资源。
但这并不是关于模板解析器的全部内容,因为我们还在它上设置一些配置参数。
首先是模板模式:
templateResolver.setTemplateMode(TemplateMode.HTML);
HTML是ServletContextTemplateResolver
的默认模板模式,但⽆论如何,这么写是很好的做法,以便我们的代码清楚地说明了发⽣了什么。
设置模板的前缀和后缀:
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
使⽤此配置,模板名称"team/list”将对应于:
servletContext.getResourceAsStream("/WEB-INF/templates/team/list.html");
还可以通过cacheTTLMs
属性配置已解析的模板实例的缓存时间:
templateResolver.setCacheTTLMs(3600000L);
2.2.2 模版引擎
模板引擎对象是org.thymeleaf.ITemplateEngine
接⼝的实现。这些实现之⼀由Thymeleaf核⼼提供org.thymeleaf.TemplateEngine
,我们在此创建⼀个实例:
templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
我们所需要的就是创建⼀个实例并将Template Resolver
设置为它的属性。模板解析器是TemplateEngine
需要的唯⼀必需参数,尽管还有很多其他参数将被覆盖(消息解析器,缓存⼤⼩等)。现在,这就是我们所需要的。
我们的模板引擎现在已经准备就绪,可以开始使⽤Thymeleaf创建我们的⻚⾯。
4.标准表达式语法
Thymeleaf标准⽅⾔中最重要的部分之⼀就是Thymeleaf标准表达式语法。
我们已经看到了两种类型的属性值的表达⽅式:消息和变量表达式:
<p th:utext="#{home.welcome}">Welcome to our Thymeleaf Example!</p>
<p>Today is: <span th:text="${today}">2018-10-09</span></p>
Thymeleaf还有更多类型的表达式和更多有趣的细节需要我们去学习。⾸先,我们来看看标准表达式功能的快速总结:
- 简单表达式
- 变量表达式:
${ }
- 选择变量表达式:
*{ }
- 消息表达式:
#{ }
- URL表达式:
@{ }
- 片段(fragment )表达式:
~{ }
- 变量表达式:
- 字面值
- 文本字面值:
'one text'
,'Another one!'
,… - 数字字面值:
0
,34
,3.0
,12.3
,… - Boolean字面值:
true
,false
- Null字面值:
null
- 文本标记:
one
,sometext
,main
,…
- 文本字面值:
- 文本操作
- 字符串连接:
+
- 文本替换:
|The name is ${name}|
- 字符串连接:
- 算术运算符
- 二元运算符:
+
,-
,*
,/
,%
- 负号(一元运算符):
-
- 二元运算符:
- 布尔运算符
- 二元运算符:
and
,or
- 布尔取反(一元运算符):
!
,not
- 二元运算符:
- 比较和相等运算符
- 比较运算符:
>
,<
,>=
,<=
(gt
,lt
,ge
,le
) - 相等运算符:
==
,!=
(eq
,ne
)
- 比较运算符:
- 条件运算符
- If-then:
(if) ? (then)
- If-then-else:
(if) ? (then) : (else)
- Default:
(value) ?: (defaultvalue)
- If-then:
- 特殊符号
- 空操作符:
_
- 空操作符:
所有这些功能可以组合和嵌套:
'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))
4.1 消息
我们已经知道,#{ }
消息表达式允许我们这样引⽤消息字符串:
<p th:utext="#{home.welcome}">Welcome to our Thymeleaf Example!</p>
上⾯表达式的消息字符串如下:
home.welcome=Welcome to Thymeleaf Example!
但是我们可能会想到:如果消息⽂本不是完全静态的,会发⽣什么?例如,如果我们的应⽤程序想知道是哪个⽤户访问该⽹站,我们是否可以通过⽤户名字来问候呢?
这意味着我们需要在我们的消息中添加⼀个参数。就像这样:
home.welcome=Welcome to Thymeleaf Example, {0}!
为了指定我们参数的值,并给出⼀个名为user
的HTTP会话属性:
<p th:utext="#{home.welcome(${session.user.firstName})}">Welcome to our Thymeleaf Example, Sebastian!</p>
可以指定⼏个参数,⽤逗号分隔。实际上,消息键本身可以来⾃⼀个变量:
<p th:utext="#{${welcomeMsgKey}(${session.user.firstName})}">Welcome to our Thymeleaf Example, Sebastian!</p>
4.2 变量
我们已经提到${ }
表达式实际上是在上下⽂中包含的变量的映射上执⾏的**OGNL(Object-Graph Navigation Language)**对象。
有关OGNL语法和功能的详细信息,请阅读OGNL语⾔指南。在Spring MVC启⽤的应⽤程序中,OGNL将被替换为SpringEL,但其语法与OGNL⾮常相似(实际上,在⼤多数常⻅情况下完全相同)
从OGNL的语法,我们知道:
<p>Today is: <span th:text="${today}">2018-10-09</span></p>
实际上等同于:
ctx.getVariable("today");
但OGNL允许我们创建更强⼤的表达式:
<p th:utext="#{home.welcome(${session.user.firstName})}">Welcome to our Thymeleaf Example, Sebastian!</p>
通过执⾏如下代码获得⽤户名称:
((User) ctx.getVariable("session").get("user")).getFirstName();
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('-')}
4.2.1 基本表达式对象
当对上下⽂变量计算OGNL表达式时,为了获得更高的灵活性,表达式可以使用一些对象。这些对象以#
符号开头(按照OGNL标准):
#ctx
:上下⽂对象。#vars
:上下⽂变量。#locale
:上下⽂区域设置。#request
:(仅在Web Contexts中)HttpServletRequest对象。#response
:(仅在Web Contexts中)HttpServletResponse对象。#session
:(仅在Web Contexts中)HttpSession对象。#servletContext
:(仅在Web Contexts中)ServletContext对象。
因此,我们可以这样做:
Established locale country: <span th:text="${#locale.country}">US</span>.
你可以在官方文档附录A中阅读这些对象的完整参考。
4.2.2 工具表达式对象
除了这些基本的对象之外,Thymeleaf将为我们提供⼀组⼯具对象,这些对象将帮助我们在表达式中执⾏常⻅任务。
#execInfo
:有关正在处理的模板的信息。#messages
:⽤于在变量表达式中获取外部化消息的⽅法,与使⽤#{ }语法获得的⽅式相同。
#uris
:转义URL /URI部分的⽅法 。#conversions
:执⾏配置的转换服务(如果有的话)的⽅法。#dates
:java.util.Date
对象的⽅法:格式化,组件提取等。#calendars
:类似于#dates
,但对应于java.util.Calendar
对象。#numbers
:⽤于格式化数字对象的⽅法。#strings
:String
对象的⽅法:contains
,startsWith
,prepending
/appending
等 。#objects
:⼀般对象的⽅法。#bools
:布尔⽅法。#arrays
:数组的⽅法。#lists
:list
的⽅法。#sets
:set
的⽅法。#maps
:map
的⽅法。#aggregates
:在数组或集合上创建聚合的⽅法。#ids
:处理可能重复的id属性的⽅法(例如,作为迭代的结果)。
你可以在官方文档附录B中阅读每个实⽤程序对象提供的功能。
4.2.3 重新格式化首页的日期
4.3 选择表达式(星号语法)
我们不仅可以将变量表达式写为${ }
,还可以使用*{ }
。这两种⽅式有⼀个重要的区别:星号语法计算所选对象而不是整个上下文的表达式。也就是说,只要没有选定的对象,$
和*
语法就会完全相同。
什么是选定对象?使⽤th:object
属性的表达式的结果。假设我们在session
中存放了一个user
对象,如下:
<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链接
在Thymeleaf中,URL是Web应⽤程序模板中的⼀等公⺠,并且Thymeleaf标准⽅⾔中对于URL也有特殊的@语法:@{ }
。
Thymeleaf支持不同类型的URL:
- 绝对⽹址:http://www.thymeleaf.org
- 相对URL,可以是:
- 页面相对:
user/login.html
- 上下⽂相关:
/itemdetails?id=3
(服务器中的上下文名称将⾃动添加) - 服务器相对:
~/billing/processInvoice
(允许在同⼀服务器中的其他上下文(=应⽤程序)中调⽤URL) - 协议相关URL:
//code.jquery.com/jquery-2.0.3.min.js
- 页面相对:
下面我们来使⽤这个新的语法来认识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 '/(context name in server)/order/details?orderId=3' (plus rewriting) -->
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
<!-- Will produce '/(context name in server)/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未启⽤或尚未知道,则可能会在相对URL中添加“; jsessionid=…”后缀,以便会话被保留。这被称为URL重写, Thymeleaf允许你使⽤Servlet API中的每个URL的
response.encodeURL
机制来插⼊⾃⼰的重写过滤器。 th:href
属性允许我们(可选地)在我们的模板中有⼀个工作的静态href属性,这样当我们直接打开原型设计时,我们的模板链接可以被浏览器导航。
与消息语法(#{ }
)的情况⼀样,URL表达式也可以是另⼀个表达式计算的结果:
<a th:href="@{${url}(orderId=${o.id})}">view</a>
<a th:href="@{'/details/'+${user.login}(orderId=${o.id})}">view</a>
5.设置属性值
本章将介绍如何在标记中设置(或修改)属性的值的。
5.1 设置任何属性的值
当我们通过⽹站发布资讯后希望⽤户能够订阅我们的资讯,所以我们需要创建⼀个订阅消息的模板:
<form action="subscribe.html">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" />
</fieldset>
</form>
与Thymeleaf⼀样,这个模板相对于⼀个Web应⽤程序的模板来说更像是⼀个静态网页原型。首先,我们表单中的action属性静态地链接到模板文件本身,这样就没有可⽤的URL重写的地方。其次,提交按钮中的属性值以英文文本显示,但我们希望将其进行国际化。
使用th:attr
属性,以设置标签的属性值:
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
这个实现原理很简单:th:attr
只需要通过⼀个表达式将值赋给对应的属性。创建相应的控制器和消息文件后,此表单的执行后输出结果如下:
<form action="/thymeleaf3-example/subscribe">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="订 阅!"/>
</fieldset>
</form>
除了新的属性值之外,你还可以看到,应用上下文名称已经自动作为前缀添加到URL中。但是如果我们想要⼀次设置多个属性值呢? XML规则不允许你在标签中设置两次属性,因此th:att
将以逗号分隔的形式来设置多个属性值,如:
<img src="../../images/logo.png"
th:attr="src=@{/images/logo.png},title=#{logo},alt=#{logo}" />
5.2 设置指定属性的值
使用th:attr
在属性值内指定赋值可能⾮常实⽤,但如果你一直这么使用,则不是最优雅的创建模板的⽅式。Thymeleaf考虑了这一点,这就是为什么th:attr
几乎不用的原因。通常,你会使⽤其他th:*
属性来设置特定的标记属性(⽽不仅仅是像th:attr
这样的任何属性)。
例如,要设置value属性,请使⽤th:value
:
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
看起来好多了!我们尝试对表单标签中的action属性执行相同操作:
<form action="subscribe.html" th:action="@{/subscribe}">
还有很多类似的属性,每个都针对特定的HTML5属性:
th:abbr | th:accept | th:accept-charset |
th:accesskey | th:action | th:align |
th:alt | th:archive | th:audio |
th:autocomplete | th:axis | th:background |
th:bgcolor | th:border | th:cellpadding |
th:cellspacing | th:challenge | th:charset |
th:cite | th:class | th:classid |
th:codebase | th:codetype | th:cols |
th:colspan | th:compact | th:content |
th:contenteditable | th:contextmenu | th:data |
th:datetime | th:dir | th:draggable |
th:dropzone | th:enctype | th:for |
th:form | th:formaction | th:formenctype |
th:formmethod | th:formtarget | th:fragment |
th:frame | th:frameborder | th:headers |
th:height | th:high | th:href |
th:hreflang | th:hspace | th:http-equiv |
th:icon | th:id | th:inline |
th:keytype | th:kind | th:label |
th:lang | th:list | th:longdesc |
th:low | th:manifest | th:marginheight |
th:marginwidth | th:max | th:maxlength |
th:media | th:method | th:min |
th:name | th:onabort | th:onafterprint |
th:onbeforeprint | th:onbeforeunload | th:onblur |
th:oncanplay | th:oncanplaythrough | th:onchange |
th:onclick | th:oncontextmenu | th:ondblclick |
th:ondrag | th:ondragend | th:ondragenter |
th:ondragleave | th:ondragover | th:ondragstart |
th:ondrop | th:ondurationchange | th:onemptied |
th:onended | th:onerror | th:onfocus |
th:onformchange | th:onforminput | th:onhashchange |
th:oninput | th:oninvalid | th:onkeydown |
th:onkeypress | th:onkeyup | th:onload |
th:onloadeddata | th:onloadedmetadata | th:onloadstart |
th:onmessage | th:onmousedown | th:onmousemove |
th:onmouseout | th:onmouseover | th:onmouseup |
th:onmousewheel | th:onoffline | th:ononline |
th:onpause | th:onplay | th:onplaying |
th:onpopstate | th:onprogress | th:onratechange |
th:onreadystatechange | th:onredo | th:onreset |
th:onresize | th:onscroll | th:onseeked |
th:onseeking | th:onselect | th:onshow |
th:onstalled | th:onstorage | th:onsubmit |
th:onsuspend | th:ontimeupdate | th:onundo |
th:onunload | th:onvolumechange | th:onwaiting |
th:optimum | th:pattern | th:placeholder |
th:poster | th:preload | th:radiogroup |
th:rel | th:rev | th:rows |
th:rowspan | th:rules | th:sandbox |
th:scheme | th:scope | th:scrolling |
th:size | th:sizes | th:span |
th:spellcheck | th:src | th:srclang |
th:standby | th:start | th:step |
th:style | th:summary | th:tabindex |
th:target | th:title | th:type |
th:usemap | th:value | th:valuetype |
th:vspace | th:width | th:wrap |
th:xmlbase | th:xmllang | th:xmlspace |
5.3 一次设置多个属性的值
还有两个相当特殊的属性叫做th:alt-title
和th:lang-xmllang
,可⽤于同时将两个属性设置为相同的值:
th:alt-title
将设置alt
和title
。th:lang-xmllang
将设置lang
和xml:lang
。
例如,使用th:attr
:
<img src="../../images/logo.png"
th:attr="src=@{/images/logo.png},title=#{logo},alt=#{logo}" />
上述代码等价于:
<img src="../../images/logo.png"
th:src="@{/images/logo.png}" th:title="#{logo}" th:alt="#{logo}" />
也可以这样简写为:
<img src="../../images/logo.png"
th:src="@{/images/logo.png}" th:alt-title="#{logo}" />
5.4 后缀和前缀
Thymeleaf还提供th:attrappend
和th: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" />
Thymeleaf标准方言中还有两个特定的附加属性:th:classappend
和 th:styleappend
属性,用于将CSS类或样式片段添加到元素中,而不覆盖现有属性:
<tr th:each="prod : ${prods}" class="row" th:classappend="${prodStat.odd}? 'odd'">
(这里的th:each
属性是⼀个迭代属性,我们稍后会讨论)
5.5 固定值的布尔属性
HTML具有布尔属性的概念,这个布尔属性没有值,并且一旦这个布尔属性存在则意味着属性值为“true”。但是在XHTML中,这些属性只取⼀个值,这个属性值就是它本身。例如,checked
属性:
<input type="checkbox" name="option2" checked /> <!-- HTML -->
<input type="checkbox" name="option1" checked="checked" /> <!-- XHTML -->
Thymeleaf标准方言允许你通过计算条件表达式的结果来设置这些属性的值,如果条件表达式结果为true,则该属性将被设置为其固定值,如果结果为false,则不会设置该属性:
<input type="checkbox" name="active" th:checked="${user.active}" />
Thymeleaf标准方言还⽀持以下固定值布尔属性:
th:async | th:autofocus | th:autoplay |
th:checked | th:controls | th:declare |
th:default | th:defer | th:disabled |
th:formnovalidate | th:hidden | th:ismap |
th:loop | th:multiple | th:novalidate |
th:nowrap | th:open | th:pubdate |
th:readonly | th:required | th:reversed |
th:scoped | th:seamless | th:selected |
5.6 设置其他属性的值(默认属性处理器)
Thymeleaf提供了⼀个默认属性处理器,允许我们设置任何属性的值,即使在标准方言中没有为其定义特定的*th:处理器。
<span th:whatever="${user.name}">...</span>
将被输出为:
<span whatever="John Apricot">...</span>
5.7 HTML5友好属性和标签名的支持
Thymeleaf还⽀持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使这种语法自动提供给所有的方言(不仅仅是标准方言)。
重要提示(引用自Thymeleaf官方文档):
这个语法是命名空间(
th:*
)语法的⼀个补充,它不会替代命名空间语法的形式。而且在将来,也根本不会放弃命名空间的语法。
6.循环迭代
6.1 循环的基本语法
Thymeleaf标准方言为我们提供了⼀个用于循环迭代的属性:th:each
。
####使用th:each
:
对于我们的列表⻚⾯,我们将需要⼀个控制器(IterationController
)⽅法,从Service层获取列表数据,并将数据添加到模板上下⽂中:
@Override
public void process(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext, ITemplateEngine templateEngine) throws Exception {
TeamService teamService = new TeamService();
List<Team> teams = teamService.findAll();
WebContext ctx = new WebContext(request, response, servletContext);
ctx.setVariable("teams", teams);
templateEngine.process("example/iterationExample", ctx, response.getWriter());
}
然后我们将使⽤th:each
在我们的模板中遍历列表:
<table>
<thead>
<tr>
<th>CITY</th>
<th>NAME</th>
<th>PLAYERS COUNT</th>
</tr>
</thead>
<tbody>
<tr th:each="team : ${teams}">
<td th:text="${team.city}">洛杉矶</td>
<td th:text="${team.name}">湖人</td>
<td>
<span th:text="${#lists.size(team.players)}">12</span>
</td>
</tr>
</tbody>
</table>
在上⾯代码中,team : ${teams}
属性值表示对于${teams}
的结果中的每个元素,循环迭代当前模板片段,并使用名为”team“的变量作为当前迭代元素来填充模版数据。让我们给迭代过程中的每个部分赋予⼀个名字:
- 称
${teams}
迭代表达式或被迭代变量。 - 称
team
为迭代变量或简单的iter变量。
请注意,team
iter变量的作用域为<tr>
元素,这意味着它可用于内部标记,如<td>
。
被迭代变量的值类型:
java.util.List
类型不是可以在Thymeleaf中使⽤迭代的唯⼀值类型。下面这些类型的对象都是可以通过th:each
进⾏迭代的:
- Any object implementing
java.util.Iterable
- Any object implementing
java.util.Enumeration
. - Any object implementing
java.util.Iterator
, whose values will be used as they are returned by the iterator, without the need to cache all values in memory. - Any object implementing
java.util.Map
. When iterating maps, iter variables will be of classjava.util.Map.Entry
. - Any array.
- Any other object will be treated as if it were a single-valued list containing the object itself.
6.2 保存迭代状态
当使⽤th:each
时,Thymeleaf提供了一种用于跟踪迭代状态的机制:状态变量。状态变量在每个th:each
属性中定义,并包含以下数据:
- The current iteration index, starting with 0. This is the
index
property. - The current iteration index, starting with 1. This is the
count
property. - The total amount of elements in the iterated variable. This is the
size
property. - The iter variable for each iteration. This is the
current
property. - Whether the current iteration is even or odd. These are the
even/odd
boolean properties. - Whether the current iteration is the first one. This is the
first
boolean property. - Whether the current iteration is the last one. This is the
last
boolean property.
我们通过前⾯的例子来看看迭代状态变量的用法:
<table>
<thead>
<tr>
<th>CITY</th>
<th>NAME</th>
<th>PLAYERS COUNT</th>
</tr>
</thead>
<tbody>
<tr th:each="team,iterStat : ${teams}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${team.city}">洛杉矶</td>
<td th:text="${team.name}">湖人</td>
<td>
<span th:text="${#lists.size(team.players)}">12</span>
</td>
</tr>
</tbody>
</table>
迭代状态变量(本示例中的iterStat
)在th:each
属性中通过在iter变量本身之后直接写其名称来定义,用逗号分隔。就像iter变量⼀样,状态变量的作⽤范围也是th:each
属性的标签定义的代码⽚段中。
上面的模版代码执行结果如下:
<table>
<thead>
<tr>
<th>CITY</th>
<th>NAME</th>
<th>PLAYERS COUNT</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>金州</td>
<td>勇士</td>
<td>
<span>5</span>
</td>
</tr>
<tr>
<td>休斯顿</td>
<td>火箭</td>
<td>
<span>4</span>
</td>
</tr>
<tr class="odd">
<td>洛杉矶</td>
<td>湖人</td>
<td>
<span>3</span>
</td>
</tr>
</tbody>
</table>
我们的迭代状态变量已经完美运行,仅将奇数CSS类建立到奇数行。如果没有显式地设置状态变量,则Thymeleaf将始终为你创建⼀个默认的迭代变量,该状态迭代变量名称为:迭代变量名+“Stat”。
7.条件判断
7.1 简单条件判断:if 和 unless
有时,如果满⾜某个条件,才将⼀个模板片段显示在结果中。例如:
<tr th:each="team,iterStat : ${teams}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${team.city}">洛杉矶</td>
<td th:text="${team.name}">湖人</td>
<td>
<span th:text="${#lists.size(team.players)}">12</span>
<a href="iterationExample2.html"
th:href="@{/example/iterationExample(teamId=${team.id})}"
th:if="${#lists.size(team.players)}>3">view</a>
</td>
</tr>
对于第三个<td>
元素,只有当team.players
的长度**>3**时才显示<a>
标签。
th:if
属性不仅只以布尔值作为判断条件。它还将按照以下规则判定指定的表达式值为true:
- If value is not null:
- If value is a boolean and is
true
. - If value is a number and is non-zero
- If value is a character and is non-zero
- If value is a String and is not “false”, “off” or “no”
- If value is not a boolean, a number, a character or a String.
- If value is a boolean and is
- (If value is null,
th:if
will evaluate to false).
此外,th:if
还有⼀个反向属性:th:unless
。
7.2 switch语句
还有⼀种使⽤Java中等效的switch语句结构有条件地显示内容的⽅法:th:switch
/th:case
。
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
</div>
只要第⼀个th:case
的值为true,则同⼀个switch语句中的其他th:case
属性将被视为false。switch语句的default选项指定为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需要我们定义这些部分“片段(fragment)”,以供其他模版引用,可以使用th:fragment
属性来定义被包含的模版片段。
假设我们要向所有页面添加页脚,因此我们创建⼀个包含以下代码的/WEB-INF/templates/copy.html
⽂件:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="footer">
© 2018 XXX
</div>
</body>
</html>
上面的代码定义了⼀个名为footer的⽚段,我们可以使用th:insert
或th:replace
属性(以及th:include
,尽管Thymeleaf3.0不再推荐), 容易地包含在我们的主⻚中:
<body>
...
<div th:insert="~{copy :: footer}"></div>
</body>
th:insert
需要⼀个片段表达式(~{ }
)。在上⾯的例子中,使用的是⼀个简单⽚段表达式,(~{ }
)包围是完全可选的,所以上面的代码将等价于:
<body>
...
<div th:insert="copy :: footer"></div>
</body>
片段规范语法:
片段表达式的语法是非常简单的。有三种不同的格式:
-
“
~{templatename :: selector}
” 包含在名称为templatename的模板上应用指定的标签选择器匹配的片段。请注意,选择器可以只是⼀个片段名称,因此你可以像~{copy :: footer}
中的~{templatename :: fragmentname}
指定⼀些简单的东⻄。标记选择器语法由底层的
AttoParser
解析库定义,类似于XPath表达式或CSS选择器。有关详细信息,请参阅官方文档附录C. -
“
~{templatename}
” 包含名为templatename的整个模板。你在
th:insert
/th:replace
标签中使⽤的模板名称必须由TemplateEngine
当前正在使用的TemplateResolver
解析。 -
"
~{:: selector}
“或”~{this :: selector}
“包含在同⼀模板中的匹配指定选择器的片段。
上述示例中的模板名和选择器都可以是表达式(甚至是条件表达式!), 如:
<div th:insert="copy :: (${user.isAdmin}? #{footer.admin} : #{footer.normaluser})"></div>
再次注意,这里的th:insert
中,~{ }
是省略了的。
片段可以包含任何th:*
属性。⼀旦将⽚段包含在目标模板(具有th:insert
/th:replace
属性的模板)中,这些属性将被计算属性表达式的值,并且它们将能够引用此目标模板中定义的任何上下⽂变量。
这种分片方法的⼀⼤优点是,你可以将⻚⾯中的片段写⼊单独的文件,并且该文件具有完整而有效的标记结构,还能在浏览器中完美的显示该片段⻚⾯,同时允许Thymeleaf包含在其他模板中。
不用th:fragment
引用片段:
由于标签选择器的强大功能,我们可以包含不使⽤th:fragment
属性定义的片段。甚⾄可以使用没有Thymeleaf模版的应用程序的标记代码:
...
<div id="copy-section">
© 2018 XXX
</div>
...
我们可以通过ID属性来引用上面的片段,类似于CSS选择器:
<body>
...
<div th:insert="~{footer :: #copy-section}"></div>
</body>
th:insert
和th:replace
(th:include
)之间的区别:
th:insert
和th:replace
之间有什么区别?(th:include
在3.0之后不推荐使用了)
th:insert
是最简单的:它将简单地插⼊指定宿主标签的标签体中。th:replace
实际上用指定的片段替换其宿主标签。th:include
类似于th:insert
,而不是插入片段,它只插⼊此片段的内容。
所以这样⼀个HTML片段:
<footer th:fragment="footer">
© 2018 XXX
</footer>
在宿主<div>
标签中包含三次上述⽚段,如下所示:
<body>
...
<div th:insert="copy :: footer"></div>
<div th:replace="copy :: footer"></div>
<div th:include="copy :: footer"></div>
</body>
结果如下:
<body>
...
<!-- th:insert -->
<div>
<footer>
© 2018 XXX
</footer>
</div>
<!-- th:replace -->
<footer>
© 2018 XXX
</footer>
<!-- th:include -->
<div>
© 2018 XXX
</div>
</body>
8.2 可参数化的片段签名
为了使模板片段具有更多类似函数的功能,⽤th:fragment
定义的⽚段可以指定⼀组参数:
<div th:fragment="frag (onevar,twovar)">
<p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>
可以通过以下两种语法中的⼀种来引用上述模版片段,下⾯的th:replace
改成th:insert
也是⼀样的:
<div th:replace="::frag (${value1},${value2})">...</div>
<div th:replace="::frag (onevar=${value1},twovar=${value2})">...</div>
另外,在上述第二种语法中,可以改变参数顺序:
<div th:replace="::frag (twovar=${value2},onevar=${value1})">...</div>
不带片段参数的片段局部变量:
即使⽚段没有定义参数:
<div th:fragment="frag">
...
</div>
我们可以使⽤上⾯指定的第二个语法来调⽤它们(只有第二个语法才可以):
<div th:replace="::frag (onevar=${value1},twovar=${value2})">
上⾯的代码等价于th:replace
和th:with
的组合:
<div th:replace="::frag" th:with="onevar=${value1},twovar=${value2}">
注意,片段的局部变量规范–⽆论是否具有参数签名–都不会导致上下文在执行之前被清空。片段仍然能够访问调用模板中正在使用的每个上下文变量。
用th:assert
进行模版内部断言:
th:assert
属性可以指定逗号分隔的表达式列表,如果每个表达式的结果都为true,则正确执行,否则引发异常。
<div th:assert="${onevar},(${twovar} != 43)">...</div>
这有助于验证⽚段签名中的参数:
<header th:fragment="contentheader(title)" th:assert="${!#strings.isEmpty(title)}">...</header>
8.3 灵活的布局:不仅仅是片段插入
由于片段表达式的强大功能,我们不仅可以为片段指定文本类型,数字类型,bean对象类型的参数,还可以指定标记片段作为参数。这允许我们以⼀种⽅式创建我们的⽚段,使得它们可以调用模板的标记,从而产⽣⾮常灵活的模板布局机制。
请注意在下面的片段中使用title
和links
变量:
<head th:fragment="common_header(title,links)">
<title th:replace="${title}">The awesome application</title>
<!-- Common styles and scripts -->
<link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
<link rel="shortcut icon" th:href="@{/images/favicon.ico}">
<script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>
<!--/* Per-page placeholder for additional links */-->
<th:block th:replace="${links}" />
</head>
我们现在调用这个片段:
...
<head th:replace="base :: common_header(~{::title},~{::link})">
<title>Awesome - Main</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>
...
结果将使用调用模板中的实际<title>
和<link>
标签作为标题和链接变量的值,导致我们的片段在插入期间被定制化:
...
<head>
<title>Awesome - Main</title>
<!-- Common styles and scripts -->
<link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
<link rel="shortcut icon" href="/awe/images/favicon.ico">
<script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>
<link rel="stylesheet" href="/awe/css/bootstrap.min.css">
<link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css">
</head>
...
使用空片段:
⼀个特殊的片段表达式:空的片段(~{ }
)可以用于指定没有标记。使用前⾯的例子:
<head th:replace="base :: common_header(~{::title},~{})">
<title>Awesome - Main</title>
</head>
...
注意片段的第⼆个参数(links
)如何设置为空片段,因此没有为<th:block th:replace=“${links}”/>
块写⼊:
...
<head>
<title>Awesome - Main</title>
<!-- Common styles and scripts -->
<link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
<link rel="shortcut icon" href="/awe/images/favicon.ico">
<script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>
</head>
...
使用哑操作符:
如果我们只想让我们的片段将其当前标记用作默认值,那么no-op也可以用作片段的参数。继续使用common_header
示例:
...
<head th:replace="base :: common_header(_,~{::link})">
<title>Awesome - Main</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
</head>
...
看看title
参数(common_header
片段的第⼀个参数)如何设置为no-op(_
),这导致片段的这⼀部分不被执行(title=no-operation
)。
结果如下:
...
<head>
<title>The awesome application</title>
<!-- Common styles and scripts -->
<link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
<link rel="shortcut icon" href="/awe/images/favicon.ico">
<script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>
<link rel="stylesheet" href="/awe/css/bootstrap.min.css">
<link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css">
</head>
...
高级条件插入片段:
空片段和无操作片段允许我们以非常简单和优雅的方式执行片段的条件插入。例如,我们可以这样做,以便只有当用户是管理员时插入我们的common :: adminhead
片段,如果不是管理员插入空片段:
...
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : ~{}">...</div>
...
另外,只有满足指定的条件,我们才可以使用空操作符来插入片段,但是如果不满足条件,则不需要修改则保留标记:
...
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : _">
Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support.
</div>
...
8.4 删除模版片段
回到我们的示例应⽤程序,让我们回顾⼀下我们的列表模板的最后一个版本:
<table>
<thead>
<tr>
<th>CITY</th>
<th>NAME</th>
<th>PLAYERS COUNT</th>
</tr>
</thead>
<tbody>
<tr th:each="team,iterStat : ${teams}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${team.city}">洛杉矶</td>
<td th:text="${team.name}">湖人</td>
<td>
<span th:text="${#lists.size(team.players)}">12</span>
<a href="iterationExample2.html"
th:href="@{/example/iterationExample(teamId=${team.id})}"
th:if="${#lists.size(team.players)}>3">view</a>
</td>
</tr>
</tbody>
</table>
这个代码只是⼀个模板,但是作为⼀个静态页面(没有使用Thymeleaf,浏览器直接打开它时),它不会成为⼀个很好的原型。为什么?因为尽管浏览器完全可以显示,该表只有一行模拟数据。作为⼀个原型,它根本看起来不够真实,我们应该有多条数据, 我们需要更多的⾏。
所以我们来补充⼀下:
<table>
<thead>
<tr>
<th>CITY</th>
<th>NAME</th>
<th>PLAYERS COUNT</th>
</tr>
</thead>
<tbody>
<tr th:each="team,iterStat : ${teams}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${team.city}">洛杉矶</td>
<td th:text="${team.name}">湖人</td>
<td>
<span th:text="${#lists.size(team.players)}">12</span>
<a href="iterationExample2.html"
th:href="@{/example/iterationExample(teamId=${team.id})}"
th:if="${#lists.size(team.players)}>3">view</a>
</td>
</tr>
<tr>
<td>休斯顿</td>
<td>火箭</td>
<td>
<span>4</span>
<a href="/thymeleaf3-example/example/iterationExample?teamId=2">view</a>
</td>
</tr>
<tr class="odd">
<td>克利夫兰</td>
<td>骑士</td>
<td>
<span>3</span>
</td>
</tr>
</tbody>
</table>
好的,现在我们有三条数据,是⼀个比较好的原型了。但是,当我们用Thymeleaf处理它会发⽣什么?最后两行是模拟数据行!这是肯定的:迭代只适用于第一行,所以没有什么理由为什么Thymeleaf应该删除另外两个。
我们需要⼀种在模板处理过程中删除这两行的方法。我们使用第二和第三个<tr>
标签中的th:remove
属性:
<table>
<thead>
<tr>
<th>CITY</th>
<th>NAME</th>
<th>PLAYERS COUNT</th>
</tr>
</thead>
<tbody>
<tr th:each="team,iterStat : ${teams}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${team.city}">洛杉矶</td>
<td th:text="${team.name}">湖人</td>
<td>
<span th:text="${#lists.size(team.players)}">12</span>
<a href="iterationExample2.html"
th:href="@{/example/iterationExample(teamId=${team.id})}"
th:if="${#lists.size(team.players)}>3">view</a>
</td>
</tr>
<tr th:remove="all">
<td>休斯顿</td>
<td>火箭</td>
<td>
<span>4</span>
<a href="/thymeleaf3-example/example/iterationExample?teamId=2">view</a>
</td>
</tr>
<tr class="odd" th:remove="all" >
<td>克利夫兰</td>
<td>骑士</td>
<td>
<span>3</span>
</td>
</tr>
<tbody>
</table>
运行之后会发现,结果是我们想要的,而且还解决了原型页面的问题。
那么th:remove
属性中的all
值是什么意思?th:remove
可以有五种不同的删除方式,具体取决于它的值:
all
:删除包含标签及其所有⼦项。body
:不要删除包含的标签,但删除所有的⼦项。tag
:删除包含的标签,但不要删除其⼦项。all-but-first
:删除除第⼀个包含标签之外的所有⼦代。none
:什么都不做。该值对于动态计算是有⽤的。
all-but-first
有什么⽤呢?它可以实现保留删除原型设计:
<table>
<thead>
<tr>
<th>CITY</th>
<th>NAME</th>
<th>PLAYERS COUNT</th>
</tr>
</thead>
<tbody th:remove="all-but-first">
<tr th:each="team,iterStat : ${teams}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${team.city}">洛杉矶</td>
<td th:text="${team.name}">湖人</td>
<td>
<span th:text="${#lists.size(team.players)}">12</span>
<a href="iterationExample2.html"
th:href="@{/example/iterationExample(teamId=${team.id})}"
th:if="${#lists.size(team.players)}>3">view</a>
</td>
</tr>
<tr>
<td>休斯顿</td>
<td>火箭</td>
<td>
<span>4</span>
<a href="/thymeleaf3-example/example/iterationExample?teamId=2">view</a>
</td>
</tr>
<tr class="odd">
<td>克利夫兰</td>
<td>骑士</td>
<td>
<span>3</span>
</td>
</tr>
</tbody>
</table>
只要它返回⼀个允许的字符串值(all
,tag
,body
,all-but-first
或none
),则th:remove
属性可以使用任何Thymeleaf标准表达式。这意味着删除可能是有条件的,如:
<a href="/something" th:remove="${condition}? tag : none">Link text not to be removed</a>
另请注意,th:remove
将null
视为none
,因此以下实例与上述代码相同:
<a href="/something" th:remove="${condition}? tag">Link text not to be removed</a>
在这种情况下,如果${condition}
为false
,则返回null
,因此不会执行删除。
9.局部变量
Thymeleaf中的局部变量是指定义在模版片段中的变量,并且该变量的作用域为所在的模版片段。在我们的列表页面中的team
迭代变量就是⼀个局部变量:
<tr th:each="team : ${teams}">
...
</tr>
这里的team
变量仅在<tr>
标签的范围内可⽤。特别地:
- 它可用于在该标签中执行的优先级低于
th:each
的任何其他th:*
属性 (这意味着它们将在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>
10.属性优先级
当在同⼀个标签中写入多个th:*
属性时会发生什么?例如:
<ul>
<li th:each="item : ${items}" th:text="${item.description}">Item description here...</li>
</ul>
我们希望th:each
属性在th:text
之前执行,以便得到我们想要的结果,但是鉴于HTML/XML标准对于某个属性的渲染顺序没有任何定义,因此Thymeleaf如果要达到这种效果,必须在属性本身中建立优先机制,以确保这些属性按预期工作。因此,所有的Thymeleaf属性都定义⼀个数字优先级,以便确定在标签中执行它们的顺序。这个优先级清单如下:
Order | Feature | Attributes |
---|---|---|
1 | Fragment inclusion | th:insert th:replace |
2 | Fragment iteration | th:each |
3 | Conditional evaluation | th:if th:unless th:switch th:case |
4 | Local variable definition | th:object th:with |
5 | General attribute modification | th:attr th:attrprepend th:attrappend |
6 | Specific attribute modification | th:value th:href th:src ... |
7 | Text (tag body modification) | th:text th:utext |
8 | Fragment specification | th:fragment |
9 | Fragment removal | th:remove |
11.注释
12.内联
12.1 内联表达式
虽然通过Thymeleaf标准方言中的标签属性已经几乎满足了我们开发中的所有需求,但是有些情况下我们更喜欢将表达式直接写入我们的HTML文本。例如,我们喜欢这样写代码:
<p>Hello, [[${session.user.name}]]!</p>
而不喜欢这样写代码:
<p>Hello, <span th:text="${session.user.name}">Sebastian</span>!</p>
[[ ]]
或[( )]
表达式是Thymeleaf的内联表达式,任何在th:text
或th:utext
属性中使⽤的表达式都可以出现在[[ ]]
或[( )]
中。[[ ]]
等价于th:text
(即结果将被HTML转义),[( )]
等价于th:utext
,不会执行任何HTML转义。
内联与自然模板比较:
如果你使用过以常规方式输出文本的其他模板引擎,你可能会问:为什么我们从一开始不直接使用内联表达式输出文本呢?这比th:text
方式的代码少很多!请注意,虽然你可能会发现内联非常好用,但是你应该永远记住,当 你静态打开HTML文件时,内嵌的表达式将逐字显示在HTML⽂件中,因此你可能无法将其用作设计原型了!
不使用内联表达式时浏览器静态显示我们的代码段如下:
Hello, Sebastian!
而使用内联表达式时浏览器静态显示我们的代码如下:
Hello, [[${session.user.name}]]!
禁用内联:
内联机制是可以被禁用的,因为在实际应用中可能会出现我们想输出[[...]]
或 [(...)]
序列而不将其内容作为表达式处理的情况。为此,我们将使用th:inline="none"
来禁用内联:
<p th:inline="none">A double array looks like this: [[1, 2, 3], [4, 5]]!</p>
结果如下:
<p>A double array looks like this: [[1, 2, 3], [4, 5]]!</p>
12.2 内联文本
内联文本非常类似于我们刚刚看到的内联表达式功能,但实际上它的功能更加强大。内联文本必须显式地使用
th:inline="text"
。内联文本不仅允许我们使用内嵌表达式,而且还可以处理标签体,就像它们是以TEXT模板模式处理的模板⼀样,这允许我们执行基于文本的模板逻辑(不仅仅是输出表达式)。
- 我们将在下⼀章中看到关于文本模板模式的更多信息。
12.3 内联JavaScript
JavaScript内联允许在HTML模板模式中更好地集成JavaScript<script>
代码块。与文本内联⼀样,这实际上等同于处理脚本内容,就像它们是 JAVASCRIPT模板模式中的模板⼀样,因此⽂本模板模式的所有功能(见下⼀章)都可以在内联脚本中使⽤。 然而,在本节中,我们将重点介绍如何使用它将我们的Thymeleaf表达式的输出添加到我们的JavaScript块中。
首先必须使用th:inline="javascript"
显式启用此模式:
<script th:inline="javascript">
...
var username = [[${session.user.name}]];
...
</script>
结果如下:
<script th:inline="javascript">
...
var username = "Sebastian \"Fruity\" Applejuice";
...
</script>
以上代码中要两个重要的注意事项:
首先,JavaScript内联函数不仅会输出所需的文本,还可以使用引号和 JavaScript规范来包含其中的内容,以便将表达式结果作为⼀个格式良好的 JavaScript字符串输出。其次,因为我们输出${session.user.name}
表达式时进行了转义,即使用双括号表达式:[[${session.user.name}]]
。如果我们不进行转义,如:
<script th:inline="javascript">
...
var username = [(${session.user.name})];
...
</script>
结果将是这样的:
<script th:inline="javascript">
...
var username = Sebastian "Fruity" Applejuice;
...
</script>
这是格式错误的JavaScript代码。
JavaScript自然模版:
JavaScript的内联机制不仅能将JavaScript特定的字符串转义输出,而且在处理表达式时更加智能化。例如,我们可以在JavaScript注释中包含(转义)内联表达式,如:
<script th:inline="javascript">
...
var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
...
</script>
Thymeleaf将忽略在注释之后和分号之前写的所有内容(本例中 为“Gertrud Kiwifruit”),因此执行此操作的结果将与我们不使用包装注释时完全相同。注意这是有效的JavaScript代码。当你以静态方式打开模板文件(不在服务器上执行)时,它将完全执行。所以我们称这种方法为JavaScript自然模板。
高级内联表达式和JavaScript序列化:
关于JavaScript内联的⼀个重要的特性是,内联表达式的计算结果不限于字符串,它能自动地将以下对象序列化为javascript对象。Thymeleaf支持以下几种序列化对象:
- Strings
- Numbers
- Booleans
- Arrays
- Collections
- Maps
- Beans (objects with getter and setter methods)
例如,我们有以下代码:
<script th:inline="javascript">
...
var user = /*[[${session.user}]]*/ null;
...
</script>
${session.user}
表达式将计算为输出User
对象,Thymeleaf将正确地将其转换为JavaScript对象:
<script th:inline="javascript">
...
var user = {"firstName":"LeBron","lastName":"James"};
...
</script>
Thymeleaf的JavaScript序列化机制的默认实现在类路径中查找Jackson
库,如果存在,将使用它。如果没有,它将应用⼀个内置的序列化机制,内置的序列化机制涵盖大多数场景的需求,并和Jackson序列化机制产⽣类似的结果(但这种方法不太灵活)。
12.4 内联CSS
Thymeleaf还允许在CSS<style>
标签中使用内联,如:
<style th:inline="css">
...
</style>
例如,假设我们有两个变量设置为两个不同的String
值:
classname = 'main elems'
align = 'center'
我们可以这么做:
<style th:inline="css">
.[[${classname}]] {
text-align: [[${align}]];
}
</style>
输出结果如下:
<style th:inline="css">
.main\ elems {
text-align: center;
}
</style>
注意,CSS内联也具有像JavaScript内联⼀样的智能特性。例如,通过转义表达式输出的表达式,如[[${classname}]]
将被转义为CSS 标识符。这就是为什么我们的classname='main elems'
已经变成上⾯的代码⽚段中的main\ elems
。
高级功能:CSS自然模板:
与内联JavaScript⼀样,CSS内联也允许我们的<style>
标签可以静态和动态地工作,即通过在注释中包含内联表达式作为CSS⾃然模板:
<style th:inline="css">
.main\ elems {
text-align: /*[[${align}]]*/ left;
}
</style>
13.文本模板模式
Thymeleaf模板中的三个模板被认为是文本:TEXT,JAVASCRIPT和CSS。这将它们与标记模板模式区分开来:HTML和XML。文本模板模式和标记模式之间的关键区别在于,在文本模板中,没有标签以属性的形式插⼊逻辑,因此我们必须依赖其他机制。最基本的机制是内联。
内联语法是以文本模板模式输出表达式结果的最简单方法,因此邮件形式模板来说,内联是最好的机制。
Dear [(${name})],
Please find attached the results of the report you requested
with name "[(${report.name})]".
Sincerely,
The Reporter.
即使没有标签,上面的示例也是⼀个完整有效的Thymeleaf模板,可以在TEXT模板模式下执行。但是为了包含比单纯的输出表达式更复杂的逻辑,我们需要⼀个新的基于非标记的语法:
[# th:each="item : ${items}"]
- [(${item})]
[/]
其实上述代码是下面的简写形式:
[#th:block th:each="item : ${items}"]
- [#th:block th:utext="${item}" /]
[/th:block]
这种新语法是基于被声明为[#element]
而不是<element>
的元素(即可处理标签)。元素是像[#element]
这样打开并像[/element]
这样关闭,独立的标签可以通过使用/
以几乎相当于XML标签的方法最小化开放元素:[#element .../]
。此外,th:block
([#th:block ...]...[/th:block]
)被允许缩写为空字符串([#...]...[/]
),所以上面的块实际上等于:
[# th:each="item : ${items}"]
- [# th:utext="${item}" /]
[/]
文本语法需要完整的元素平衡(没有未封闭的标签)和引用的属性–它更像XML风格。我们来看看⼀个更完整的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.firstName}]];
[# th:each="salut : ${salutations}"]
alert([[${salut}]] + " " + username);
[/]
};
执行后,其结果是:
var greeter = function() {
var username = "LeBron";
alert("Hello" + " " + username);
alert("你好" + " " + username);
alert("Hola" + " " + username);
};