Spring Web MVC框架(十二) 使用Thymeleaf

Thymeleaf简介

前面的例子我们使用的视图技术主要是JSP。JSP的优点是它是Java EE容器的一部分,几乎所有Java EE服务器都支持JSP。缺点就是它在视图表现方面的功能很少,假如我们想迭代一个数组之类的,只能使用<% %>来包括Java语句进行。虽然有标准标签库(JSTL)的补足,但是使用仍然不太方便。另外JSP只能在Java EE容器中使用,如果我们希望渲染电子邮件之类的,JSP就无能为力了。

Java生态圈广泛,自然有很多视图框架,除了JSP之外,还有Freemarker、Velocity、Thymeleaf等很多框架。Thymeleaf的优点是它是基于HTML的,即使视图没有渲染成功,也是一个标准的HTML页面。因此它的可读性很不错,也可以作为设计原型来使用。而且它是完全独立于Java EE容器的,意味着我们可以在任何需要渲染HTML的地方使用Thymeleaf。

Thymeleaf也提供了Spring的支持,我们可以非常方便的在Spring配置文件中声明Thymeleaf Beans,然后用它们渲染视图。

引入Thymeleaf

Thymeleaf的官网上有详细教程,如果需要的话可以直接上官网查看。我们现在使用一个Spring项目来集成Thymeleaf。首先引入Thymeleaf的依赖项。thymeleaf-spring4会自动引入Thymeleaf核心包thymeleaf-core,因此我们只需要在Gradle项目中声明这一个依赖即可。

compile group: 'org.thymeleaf', name: 'thymeleaf-spring4', version: '3.0.3.RELEASE'

然后需要声明Thymeleaf的几个Bean,模板引擎、模板解析器和视图解析器。如果是开发过程中,可以关闭Thymeleaf页面缓存,这样可以让对页面的改动及时反映到视图中。

<bean id="templateResolver"
      class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
    <property name="prefix" value="/WEB-INF/templates/"/>
    <property name="suffix" value=".html"/>
    <property name="templateMode" value="HTML5"/>
    <!-- 缓存--><property name="cacheable" value="false"/>
</bean>

<bean id="templateEngine"
      class="org.thymeleaf.spring4.SpringTemplateEngine">
    <property name="templateResolver" ref="templateResolver"/>
</bean>
<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
    <property name="templateEngine" ref="templateEngine"/>
    <property name="order" value="1"/>
    <property name="characterEncoding" value="UTF-8"/>
</bean>

然后我们新建一个Thymeleaf页面,它和普通的HTML页面非常相似,只不过在html元素上需要添加xmlns:th="http://www.thymeleaf.org"属性。如果需要向页面传值的话,可以在控制器方法参数中添加Model对象,然后向其添加需要传递的对象。之后可以使用${var_name}在Thymeleaf中访问了。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>主页</title>
</head>
<body>
<h1>你好</h1>
<h2 th:text="呵呵"></h2>
</body>
</html>

然后我们用一个视图控制器将/和主页绑定。

<mvc:view-controller path="/" view-name="index"/>

然后运行程序,访问主页,即可看到Thymeleaf的结果。

基本使用

Thymeleaf使用的是OGNL语言,如果和Spring集成的话,会改为使用Spring EL。不过这两者之间大部分是相同的。因此这里讨论的大部分使用OGNL语言方法对Spring EL也适用。

文本、字面值和国际化文本

文本

文本需要th:text属性来设置。我们可以在文本元素中添加默认值,这样当Thymeleaf引擎处理失败的时候页面会显示默认值。${...}是变量表达式,将括号中的变量替换为其值。

<p th:text="${hello}">你好<p>

字面值

th:text中我们可以使用各种字面值,下面列举如下。

字符串字面值。如果需要连接多个字符串使用+即可。

<span th:text="'字符串字面值需要用单引号包括'">默认值</span>

数字字面量。我们可以使用运算符计算数字的值。

<span th:text="2012 + 5">2017</span>

布尔值字面量。

<p th:text="true==true"></p>

国际化文本

默认情况下,我们可以在与视图文件相同的目录下编写properties文件,然后使用消息语法#{home.welcome}来引用文件中的字符串。可以有多个属性文件,对应不同的区域,例如home_en.properties代表英文区域,home_es.properties代表西班牙区域。下面是一个示例属性文件。

home.welcome=你好

默认情况下Thymeleaf会在与视图相同的文件下寻找同名的属性文件来加载消息。我们也可以自定义消息解析器,用自己的策略从任何地方加载消息。

内插字符串

很多语言都支持内插字符串,可以方便的格式化字符串。不过Java不支持,这就比较蛋疼了。内插字符串可以将一个字符串中给定部分替换为实际字符串。内插字符串需要使用|包括,在|中只能包括${}表达式,不能包括其他表达式。

<span th:text="|你好, ${name}!|">

上面的内插字符串等效于下面的一段。

<span th:text="'你好, ' + '${name}' + '!'">

算术、比较和逻辑运算

在Thymeleaf中可以进行常见的各种算术运算。如果使用除/或者取余%运算符的话,还可以使用divmod代替。

<p th:text="(17%5)-2"></p>

也可以进行比较和逻辑运算。由于<这样的符号用在了HTML标签中。所以在Thymeleaf中需要使用gt等代替。比较和逻辑运算符有gt (>), lt (<), ge (>=), le (<=), not (!), eq (==), neq/ne (!=) 。

<p th:text="1 gt 2"></p>

条件表达式

这个和Java中的三元条件表达式?:一样。

<p th:text="true?'真':'假'"></p>

默认表达式

这也是一个非常方便的特性,在C#和一些语言中提供。默认表达式是val1 ?: val2,给定两个变量,当前面一个不是空的时候,前面的值会被使用,否则后面的值作为默认值被使用。

<p th:text="null?:'我不是空值'"></p>

它可以用三元条件表达式替换。

<p th:text="${val}!=null?${val}:'我不是空值'"></p>

无操作标记

无操作标记是下划线_,Thymeleaf遇到该标记的时候不会进行任何操作。

<p th:text="_">不进行任何操作</p>

生成链接

Thymeleaf也可以生成URL,类似JSTL中的<c:url>标签。这需要使用另外一种类型的表达式@{...},表示其中的是URL。支持的URL有绝对路径(完整的URL),Servlet环境相对路径(/showUsers这样的)、服务器相对路径(~/myapp/showUser这样的,允许我们调用同一个服务器下其他Servlet环境中的URL)、协议相对路径(//code.jquery.com/jquery-2.0.3.min.js)。

下面是简单的一个例子。

<p><a th:href="@{/}">返回主页</a></p>

假如需要在链接中包含查询参数,可以在@{}中使用括号,有多个查询参数使用逗号分隔开。下面这个例子最终会生成类似/hello?name=易天&age=23这样的链接。

<p><a th:href="@{/hello(name='易天',age=23)}">问候</a></p>

如果需要路径参数的话,也是类似的,只不过路径参数需要括号包括起来。

<p><a th:href="@{/hello/{name}(name='易天')}">问候</a></p>

这些链接会由org.thymeleaf.linkbuilder.ILinkBuilder接口处理,转换为实际的URL。Thymeleaf的默认实现org.thymeleaf.linkbuilder.StandardLinkBuilder可以用于非Web环境和Servlet环境。如果使用其他框架的话Thymeleaf可能不能正确生成URL,这时候就需要查阅相关文档了。

使用Thymeleaf表达式

前面用到的主要是${...},用来获取变量的值。除了美元表达式之外,Thymeleaf还有其他表达式,以下会逐一介绍。

表达式基本对象

Thymeleaf包含了一些基本对象,可以用于我们的视图中。这些基本对象使用#开头。

  • #ctx: 上下文对象.
  • #vars: 上下文变量.
  • #locale: 区域对象.
  • #request: (仅Web环境可用)HttpServletRequest对象.
  • #response: (仅Web环境可用)HttpServletResponse对象.
  • #session: (仅Web环境可用) HttpSession对象
  • #servletContext: (仅Web环境可用)ServletContext对象.

几个Web相关的对象会被Thymeleaf映射为Thymeleaf上下文中的对象,因此我们不需要也不能在它们前面添加#。例如如果我们要引用Session中的值,可以像这样使用。

<p th:text="${session.user}"></p>

表达式工具对象

这些对象同样以#开头,包含了各种工具方法,可以执行格式化、转换、生成URL等各种操作。由于这些工具对象比较多,所以我在这里就不介绍了。如果需要详细了解,可以查看表达式工具对象文档Thymeleaf文档附录B列出这些对象的使用方法。

选择表达式(星号语法)

星号表达式和美元表达式的作用都是求变量的值。不过它们之间有一些区别,星号表达式作用于被选择的对象。被选择的对象需要使用th:object指定。下面是Thymeleaf的一个例子。

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

当存在选择对象的时候,我们可以使用美元表达式${#object}来访问选择对象,也可以同时使用星号表达式和美元表达式。选择表达式的主要作用是在表达式比较长例如${session.users.address.street.fullname}的时候,简化表达式。

类型转换和格式化

当使用双括号包括的变量${{...}}或者*{{...}}时,Thymeleaf会使用它的IStandardConversionService来将变量转换为字符串。Thymeleaf默认的实现只是简单地调用了toString方法。如果使用了thymeleaf-spring4包和Spring集成的话,Thymeleaf会自动使用Spring的ConversionService来进行转换。

页面元素和布局

设置标签属性

有时候我们需要设置某些页面元素的属性(例如按钮)。在JSP中我们需要编写这样的代码<input type="button" value="${msg}">。在Thymeleaf中非常简单。

<input th:attr="type='button',value='${msg}'">

如果需要设定某个特定的属性。可以这样。

<input th:type="button" th:value="${msg}">

前面生成超链接的代码其实也是一样的,只不过属性是href、

<p><a th:href="@{/}">返回主页</a></p>

这些特定的属性有很多,详见Thymeleaf文档 设置特定属性的值

还有特殊的属性,可以同时设置两个值。它们分别是th:alt-titleth:lang-xmllang,在设置图片和语言属性的时候非常有用。

有时候需要向已有的属性中添加新的值(例如在Bootstrap中卫按钮设置不同的颜色)。这可以通过th:attrappendth:attrprepend向后或向前添加属性。

<input type="button" value="warn" class="btn" th:attrappend="class=${' ' + style}" />

条件选择块

在开发Web程序的时候, 很常见的需求就是根据某个值的真假,显示或隐藏某个HTML元素。这可以使用th:if="${exp}"实现。需要注意和JSTL中的<c:if>不同,Thymeleaf会根据条件显示或隐藏包含th:if的整个标签块,而不仅是它的子标签块。

<div th:if="true">....</div>

需要注意th:if的表达式不仅支持Java式的纯条件判断,还支持C语言式的真值判断,即当一个变量不为空或者整数不是0的时候,也认为是真值。另外还有一个th:unless执行和th:if相反的判断。

多重选择块

如果需要多重选择,可以使用th:switch。需要注意一旦有一个子条件匹配,Thymeleaf就不会继续检查其他条件了。如果需要提供默认值的话,可以这样写,th:case="*"

<div th:switch="${number}">
  <p th:case="1'">这是1</p>
  <p th:case="2">这是2</p>
  <p th:case="*">其他值</p>
</div>

迭代块

有时候我们需要遍历一个数组中的元素。这可以使用th:each实现。在th:each中我们要指定迭代元素和被迭代集合。被迭代集合可以是:数组,任何实现了java.util.Iterable的对象,任何实现了java.util.Enumeration的对象,任何实现了java.util.Iterator的对象,任何实现了java.util.Map的对象。需要注意的是,和JSTL的<c:forEach>标签不同,Thymeleaf会迭代包含<th:each>的整个标签块,而不仅仅是它的子标签块。

      <tr th:each="user : ${users}">
        <td th:text="${user.name}"></td>
        <td th:text="${user.age}"></td>
        <td th:text="${user.birthday}"></td>
      </tr>

Thymeleaf还提供了迭代状态变量来检测当前迭代的状态,只需要在th:each中声明iterStat即可。假如没有显式声明状态变量,Thymeleaf也会创建一个状态变量,名称是迭代元素添加后缀Stat。上面的代码中,Thymeleaf会创建一个名为userStat的状态变量。

      <tr th:each="user,iterStat : ${users}">
        <td th:text="${user.name}"></td>
        <td th:text="${user.age}"></td>
        <td th:text="${user.birthday}"></td>
      </tr>

状态变量包含以下属性;

  • index属性,以0开始的迭代元素索引。
  • count属性,以1开始的迭代元素索引。
  • size属性,迭代集合的总元素数。
  • iter属性,当前迭代元素。
  • even和odd两个布尔值,表明当前index的奇偶。
  • first和last两个布尔值,表明当前元素是否是第一个/最后一个。

代码段

定义代码段

首先我们来定义一个代码段。假设文件名叫做_header.html,该文件的内容如下。在文件中我们使用th:fragment定义了一个代码段。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="header">
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link th:href="@{/static/css/bootstrap.css}" rel="stylesheet"/>
    <link th:href="@{/static/css/bootstrap-theme.css}" rel="stylesheet"/>
    <script th:href="@{/static/js/jquery.js}"></script>
    <script th:href="@{/static/js/bootstrap.js}"></script>
    </th:block>

</head>
<body>

</body>
</html>

使用代码段

然后在其他地方,我们就可以使用th:insertth:replace来插入代码段了。这两者的区别是th:insert会将代码段插入该元素内部,而th:replace会将整个元素替换为要插入的代码块。在这里又出现了一种新语法~{file :: fragment},表示引用文件的某个代码块。

<head th:replace="~{_header :: head}">

另外~{..}是可选的,所以上面的代码还可以写为下面的形式。

<head th:replace="_header :: head">

代码段表达式的第一个参数可以省略(~{::selector})或者写为this(~{this::selector}),表示要引用的代码段就在当前文件中。表达式的第二个参数可以是代码段的名称,也可以是CSS选择器(~{file::#form}。如果不指定第二个参数的话,就会引入整个文件(~{templatename})。

参数化代码段

代码段可以有参数,折让它拥有类似函数的行为。下面是一个例子。

<head th:fragment="header(title)">
    <title th:text="${title}"></title>
</head>

要引入代码块的时候,同样指定参数即可。

<head th:replace="_header::header('问候')">

</head>

如果有多个参数,用逗号分隔即可。在使用代码块的时候,既可以按照位置传入参数,也可以按照名称传入参数。

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

块代码段

前面的代码段全部在某个页面元素中。如果代码块是一些平级的元素,又该怎么做呢?Thymeleaf提供了唯一的一个Thymeleaf块级元素<th:block>,可以帮助我们完成这一工作。首先创建一个_footer.html文件,在其中使用<th:block>创建一个代码块。

<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<th:block th:fragment="footer">
    <br>
    <p>这是页脚</p>
</th:block>

</body>
</html>

然后在其他地方一样使用即可。Thymeleaf模板引擎在处理<th:block>的时候会删掉它本身,而保留其内容。

<div th:replace="_footer::footer"></div>

灵活的代码段配置

Thymeleaf的这些特性让它可以配置非常灵活的代码段。来看看Thymeleaf的这个例子。首先我们定义一个公用代码段。注意这里的links,它是一个Thymeleaf块,会被实际传入的参数替代。

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

  <title th:replace="${title}">默认标题</title>

  <!-- 公用CSS和JS文件 -->

  <!--/* 各页面单独的文件 */-->
  <th:block th:replace="${links}" />

</head>

然后引用代码段。注意这里的两个代码块参数,会直接选择本页面的所有标题和link标签,然后将它们传入公用代码段中。最后生成的结果大家可以想象一下,是不是非常的灵活呢?

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

空代码段和无操作符

还是上面这个例子。如果我们传入空代码段,那么实际结果中相应的地方就会为空。

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

如果使用无操作符,那么实际生成的代码相应部分不会做任何Thymeleaf替换,也就是说会我们会得到默认标题。

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

删除代码段

以下是Thymeleaf的一个例子。我们可以使用th:remove来删除指定的部分,这在原型设计和调试的时候很有用。

  <tr th:remove="all">
    <td>Mild Cinnamon</td>
    <td>1.99</td>
    <td>yes</td>
    <td>
      <span>3</span> comment/s
      <a href="comments.html">view</a>
    </td>
  </tr>

th:remove可接受的值有5个:

  • all: 移除标签和所有子元素
  • body: 移除所有子元素,保留标签
  • tag: 移除标签,保留子元素
  • all-but-first: 保留第一个子元素,移除所有其他
  • none : 什么也不做。这个值在动态求值的时候会有作用

其他特性

由于Thymeleaf的用法很多,这里没办法完全写下,所以我就不写了。前面这些应该可以满足基本需求了。如果需要其他特性的话请看官方文档吧。这里简单的说明一下未详细介绍的其他特性。

本地变量

使用th:with声明一个本地变量,可以在某段代码中重用变量。

属性处理

Thymeleaf是如何处理这些th:*属性的呢?

注释和代码块

这里介绍了几种Thymeleaf注释。利用这些注释,我们可以让某些代码在原型设计的时候出现,某些代码在Thymeleaf引擎处理之后出现,等等。

内联

前面所有这些Thymeleaf属性都是使用属性方式写入的,能不能通过内联的方式直接在元素内部添加值?当然可以,只不过这样的话,当显示未处理的页面时,就不会显示预设的默认值,而是丑陋的表达式代码了。除此之外,还可以对CSS、JavaScript内联,让Thymeleaf引擎生成合适的代码。

模板模式

有HTML5、XML、TEXT等多种模式,可以用于生成各种类型的文档。

Thymeleaf配置

模板解析器、消息解析器、类型转换器、日志服务、缓存的配置方法。

附录

表达式基本对象、表达式工具对象和标记选择器语法的使用方法。

这些Thymeleaf的使用例子可以直接查看Thymeleaf官方文档,也可以看看我的代码

参考资料

Thymeleaf官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,728评论 1 92
  • 本章主要内容包括: 将model数据展现为HTML JSP视图的使用 在前面的章节中,我们主要关注点在于编写控制来...
    hoxis阅读 2,362评论 2 12
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • Thymeleaf 简易教程 本文源码可以在这里下载: https://github.com/codergege/...
    codergege阅读 50,653评论 4 30
  • e8a37405cb53阅读 309评论 0 0