Thymeleaf(第八章)模板布局

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Thymeleaf(第八章)模板布局相关的知识,希望对你有一定的参考价值。

8 模板布局

8.1 包括模板片段

定义和引用片段

使用th:fragment属性来定义片段,如下所示footer.html

1<!DOCTYPE html>
2<html xmlns:th="http://www.thymeleaf.org">
3  <body>
4    <div th:fragment="copy">
5      &copy; 2011 The Good Thymes Virtual Grocery
6    </div>
7  </body>
8</html>

使用th:insert和th:replace可以容易地包含片段(也可以使用th:include,但是从3.0后不再推荐)

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

注意,th:insert期望一个片段表达式(~{...}),它是一个表达式产生一个片段。~{}包围是完全可选的,所以下面是一样的:

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

片段规范语法

有三种不同的片段表达式语法:
~{templatename::selector},如~{footer :: copy}
~{templatename},没有指定片段名,那就把这个模板整体包含进来
~{::selector}"或~{this::selector}插入本模板的一个片段,如果没有找到,将沿着模板调用堆栈往上找,直到在某个级别匹配。
这里的选择器可以有多种语法,参考官方网站:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#appendix-c-markup-selector-syntax

上例中的templatename和selector可以是表达式,甚至是条件表达式

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

再次注意,在th:insert/th:replace中,使用~{...}包围是可选的。

片段能够包含任何th:*属性,这些属性将被计算一旦这个片段被包含进目标模板中,它能够引用任何定义在目标模板里的上下文变量。

引用没有th:fragment的片段

我们可以引入没有使用任何th:fragment属性的片段,这个片段可以来自于一个对thymeleaf一无所知的不同应用:

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

我们可以使用它的id属性来引用上面的那个片段,以和CSS选择器相似的方式:

1<body>
2  <div th:insert="~{footer :: #copy-section}"></div>
3</body>

th:insert和th:replace(和th:include)之间的不同:
th:insert是最简单的,它简单的插入指定的片段作为它的主人标签的body
th:replace实际使用指定的片段替换它的主人标签
th:include与th:insert相似,但不是插入片段本身,而是仅插入片段的内容

因此,一个像这样的HTML片段:

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

在标签中被包含三次,像这样:

1<body>
2  <div th:insert="footer :: copy"></div>
3
4  <div th:replace="footer :: copy"></div>
5
6  <div th:include="footer :: copy"></div>
7</body>

将产生:

 1<body>
 2  <div>
 3    <footer>
 4      &copy; 2011 The Good Thymes Virtual Grocery
 5    </footer>
 6  </div>
 7
 8  <footer>
 9    &copy; 2011 The Good Thymes Virtual Grocery
10  </footer>
11
12  <div>
13    &copy; 2011 The Good Thymes Virtual Grocery
14  </div>
15</body>

8.2 可参数化的片段签名

为了给模板片段创建一个更加像函数的机制,使用th:fragment定义的片段能指定若干参数:

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

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

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

注意,下面这样调用时,参数顺序不重要:

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

没有片段参数的片段本地变量

即使片段定义时没有参数,像这样:

1<div th:fragment="frag">
2    ...
3</div>

我们可以使用也只能使用上面的第二种语法来调用它们:

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

这将等于th:replace和th:with的一个结合:

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

注意,这个片段的本地变量的规范(无论它是否有一个参数签名),不会导致在它执行前上下文被清空。片段将仍然能够访问每一个正在被使用的上下文的变量当调用模板时。

th:assert用于模板内部断言
th:assert属性能够指定一个逗号分隔的表达式列表,当每次计算时都应该产生true,如果不是将引发异常。

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

这非常便于在一个片段签名处对参数进行验证:

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

8.3 灵活布局,超出了仅仅的片段插入

幸亏有片段表达式,我们能够为片段指定一些非文本、数字、布尔的参数,而是片段标记(即可以把片段本身当作参数传递)。

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

注意下面片段里的title和links变量的使用:

 1<head th:fragment="common_header(title,links)">
 2  <!-- 使用传进来的title进行替换 -->
 3  <title th:replace="${title}">The awesome application</title>
 4
 5  <!-- Common styles and scripts -->
 6  <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
 7  <link rel="shortcut icon" th:href="@{/images/favicon.ico}">
 8  <script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>
 9
10  <!-- 使用传进来的links进行替换 -->
11  <!--/* Per-page placeholder for additional links */-->
12  <th:block th:replace="${links}" />
13</head>

我们现在可以像这样调用这个模板:

1<!-- 用一个片段来替换自己,同时把自己的title和links传给它 -->
2<head th:replace="base :: common_header(~{::title},~{::link})">
3  <!-- 被当参数传递给common_header片段 -->
4  <title>Awesome - Main</title>
5  <!-- 被当参数传递给common_header片段 -->
6  <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
7  <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">
8</head>

结果将使用实际的来自于调用模板的<title>和<link>标签作为title和links变量的值,在插入期间能够使我们的片段被定制:

 1<head>
 2  <title>Awesome - Main</title>
 3
 4  <!-- Common styles and scripts -->
 5  <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
 6  <link rel="shortcut icon" href="/awe/images/favicon.ico">
 7  <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>
 8
 9  <link rel="stylesheet" href="/awe/css/bootstrap.min.css">
10  <link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css">
11</head>

使用空的片段

一个特别的片段表达式,空片段~{},可以用于不指定标记,使用前一个例子:

1<head th:replace="base :: common_header(~{::title},~{})">
2
3  <title>Awesome - Main</title>
4
5</head>

注意,片段的第二个参数links是如何被设置成空片段的,因此<th:block th:replace="${links}" />这个代码块什么都没有输出:

1<head>
2  <title>Awesome - Main</title>
3
4  <!-- Common styles and scripts -->
5  <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
6  <link rel="shortcut icon" href="/awe/images/favicon.ico">
7  <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>
8</head>

使用无操作token

无操作也可以作为片段的一个参数,如果我们想让片段使用它的当前标记作为默认值。我们再次使用这个示例:

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

看title参数(第一个参数)如何被设置成无操作,这将导致片段的这个部分不被执行而原样输出:

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

所以结果就是:

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

片段的高级有条件插入

空片段和无操作token的能力允许我们执行片段的有条件插入,且以一个非常容易和优雅的方式。
例如,只有用户是管理员时插入片段,不是时什么都不插入(其实插是入空片段):

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

如果指定条件满足时插入一个片段,相反条件不满足时,使标记原样输出而不改变,此时应该使用无操作token,示例:

1<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : _">
2    Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support.
3</div>

除此之外,如果我们已经配置了模板解析器来检查模板资源的存在性,依赖于checkExistence标志,我们可以使用片段自身的存在性作为条件,在一个默认操作里:

1<!-- The body of the <div> will be used if the "common :: salutation" fragment  -->
2<!-- does not exist (or is empty).                                              -->
3<div th:insert="~{common :: salutation} ?: _">
4    Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support.
5</div>

8.4 移除模板片段

让我们看下面这个产品列表模板:

 1<table>
 2  <tr>
 3    <th>NAME</th>
 4    <th>PRICE</th>
 5    <th>IN STOCK</th>
 6    <th>COMMENTS</th>
 7  </tr>
 8  <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? ‘odd‘">
 9    <td th:text="${prod.name}">Onions</td>
10    <td th:text="${prod.price}">2.41</td>
11    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
12    <td>
13      <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
14      <a href="comments.html" 
15         th:href="@{/product/comments(prodId=${prod.id})}" 
16         th:unless="${#lists.isEmpty(prod.comments)}">view</a>
17    </td>
18  </tr>
19</table>

这个代码作为模板是不错的,但是作为一个静态页(不经过thymeleaf的处理直接使用浏览器打开)将不会产生一个好的原型。
为什么?因为虽然可以使用浏览器很好地展示,但是那个表格只有一行,且这行是假数据。作为一个原型,它看起来不够足够的真实,我们有多个商品啊,我们需要多行数据。
所以让我们添加一些:

 1<table>
 2  <tr>
 3    <th>NAME</th>
 4    <th>PRICE</th>
 5    <th>IN STOCK</th>
 6    <th>COMMENTS</th>
 7  </tr>
 8  <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? ‘odd‘">
 9    <td th:text="${prod.name}">Onions</td>
10    <td th:text="${prod.price}">2.41</td>
11    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
12    <td>
13      <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
14      <a href="comments.html" 
15         th:href="@{/product/comments(prodId=${prod.id})}" 
16         th:unless="${#lists.isEmpty(prod.comments)}">view</a>
17    </td>
18  </tr>
19  <tr class="odd">
20    <td>Blue Lettuce</td>
21    <td>9.55</td>
22    <td>no</td>
23    <td>
24      <span>0</span> comment/s
25    </td>
26  </tr>
27  <tr>
28    <td>Mild Cinnamon</td>
29    <td>1.99</td>
30    <td>yes</td>
31    <td>
32      <span>3</span> comment/s
33      <a href="comments.html">view</a>
34    </td>
35  </tr>
36</table>

好,现在我们有三行,对于一个原型来说这样确实更好。但是,当使用thymeleaf处理它时会发生什么:

 1<table>
 2  <tr>
 3    <th>NAME</th>
 4    <th>PRICE</th>
 5    <th>IN STOCK</th>
 6    <th>COMMENTS</th>
 7  </tr>
 8  <tr>
 9    <td>Fresh Sweet Basil</td>
10    <td>4.99</td>
11    <td>yes</td>
12    <td>
13      <span>0</span> comment/s
14    </td>
15  </tr>
16  <tr class="odd">
17    <td>Italian Tomato</td>
18    <td>1.25</td>
19    <td>no</td>
20    <td>
21      <span>2</span> comment/s
22      <a href="/gtvg/product/comments?prodId=2">view</a>
23    </td>
24  </tr>
25  <tr>
26    <td>Yellow Bell Pepper</td>
27    <td>2.50</td>
28    <td>yes</td>
29    <td>
30      <span>0</span> comment/s
31    </td>
32  </tr>
33  <tr class="odd">
34    <td>Old Cheddar</td>
35    <td>18.75</td>
36    <td>yes</td>
37    <td>
38      <span>1</span> comment/s
39      <a href="/gtvg/product/comments?prodId=4">view</a>
40    </td>
41  </tr>
42  <tr class="odd">
43    <td>Blue Lettuce</td>
44    <td>9.55</td>
45    <td>no</td>
46    <td>
47      <span>0</span> comment/s
48    </td>
49  </tr>
50  <tr>
51    <td>Mild Cinnamon</td>
52    <td>1.99</td>
53    <td>yes</td>
54    <td>
55      <span>3</span> comment/s
56      <a href="comments.html">view</a>
57    </td>
58  </tr>
59</table>

最后两行是假数据行。迭代只应用到了第一行上,因此thymeleaf没有理由去删除其它两行。
我们需要一种方式来移除那两行数据在模板处理期间。让我们使用th:remove属性在第二和第三个<tr>标签上:

 1<table>
 2  <tr>
 3    <th>NAME</th>
 4    <th>PRICE</th>
 5    <th>IN STOCK</th>
 6    <th>COMMENTS</th>
 7  </tr>
 8  <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? ‘odd‘">
 9    <td th:text="${prod.name}">Onions</td>
10    <td th:text="${prod.price}">2.41</td>
11    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
12    <td>
13      <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
14      <a href="comments.html" 
15         th:href="@{/product/comments(prodId=${prod.id})}" 
16         th:unless="${#lists.isEmpty(prod.comments)}">view</a>
17    </td>
18  </tr>
19  <tr class="odd" th:remove="all">
20    <td>Blue Lettuce</td>
21    <td>9.55</td>
22    <td>no</td>
23    <td>
24      <span>0</span> comment/s
25    </td>
26  </tr>
27  <tr th:remove="all">
28    <td>Mild Cinnamon</td>
29    <td>1.99</td>
30    <td>yes</td>
31    <td>
32      <span>3</span> comment/s
33      <a href="comments.html">view</a>
34    </td>
35  </tr>
36</table>

一旦被处理,所有事情将再次看起来像它应该的样子:

 1<table>
 2  <tr>
 3    <th>NAME</th>
 4    <th>PRICE</th>
 5    <th>IN STOCK</th>
 6    <th>COMMENTS</th>
 7  </tr>
 8  <tr>
 9    <td>Fresh Sweet Basil</td>
10    <td>4.99</td>
11    <td>yes</td>
12    <td>
13      <span>0</span> comment/s
14    </td>
15  </tr>
16  <tr class="odd">
17    <td>Italian Tomato</td>
18    <td>1.25</td>
19    <td>no</td>
20    <td>
21      <span>2</span> comment/s
22      <a href="/gtvg/product/comments?prodId=2">view</a>
23    </td>
24  </tr>
25  <tr>
26    <td>Yellow Bell Pepper</td>
27    <td>2.50</td>
28    <td>yes</td>
29    <td>
30      <span>0</span> comment/s
31    </td>
32  </tr>
33  <tr class="odd">
34    <td>Old Cheddar</td>
35    <td>18.75</td>
36    <td>yes</td>
37    <td>
38      <span>1</span> comment/s
39      <a href="/gtvg/product/comments?prodId=4">view</a>
40    </td>
41  </tr>
42</table>

属性里面的all值是什么意思?th:remove可以有五种不同的行为,依赖于它的值:
all: 移除包含它的标签以及该标签的所有孩子
body: 不移除包含它的标签,但移除该标签的所有孩子
tag: 移除包含它的标签,但不移除该标签的孩子
all-but-first: 移除包含它的标签的第一个孩子之外的所有孩子
none : 什么都不做。这个值在动态计算时有用。

all-but-first值能够对什么是有用的呢?请看下面示例:

 1<table>
 2  <thead>
 3    <tr>
 4      <th>NAME</th>
 5      <th>PRICE</th>
 6      <th>IN STOCK</th>
 7      <th>COMMENTS</th>
 8    </tr>
 9  </thead>
10  <tbody th:remove="all-but-first">
11    <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? ‘odd‘">
12      <td th:text="${prod.name}">Onions</td>
13      <td th:text="${prod.price}">2.41</td>
14      <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
15      <td>
16        <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
17        <a href="comments.html" 
18           th:href="@{/product/comments(prodId=${prod.id})}" 
19           th:unless="${#lists.isEmpty(prod.comments)}">view</a>
20      </td>
21    </tr>
22    <tr class="odd">
23      <td>Blue Lettuce</td>
24      <td>9.55</td>
25      <td>no</td>
26      <td>
27        <span>0</span> comment/s
28      </td>
29    </tr>
30    <tr>
31      <td>Mild Cinnamon</td>
32      <td>1.99</td>
33      <td>yes</td>
34      <td>
35        <span>3</span> comment/s
36        <a href="comments.html">view</a>
37      </td>
38    </tr>
39  </tbody>
40</table>

th:remove属性能够使用任何thymeleaf标准表达式,只要它返回一个被允许的字符串值(all,tag,body,all-but-first或none)。
这意味着可以进行有条件地移除,像这样:

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

注意,th:remove将把null认为是none的同义词,所以下面这个示例可以和上面那个示例一样的工作:

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

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

8.5 布局继承

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

 1<!DOCTYPE html>
 2<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
 3<head>
 4    <title th:replace="${title}">Layout Title</title>
 5</head>
 6<body>
 7    <h1>Layout H1</h1>
 8    <div th:replace="${content}">
 9        <p>Layout content</p>
10    </div>
11    <footer>
12        Layout footer
13    </footer>
14</body>
15</html>

这个示例声明了一个片段叫做layout,title和content作为参数。在继承它的页面上,通过提供片段表达式,来替换这两个参数:

 1<!DOCTYPE html>
 2<html th:replace="~{layoutFile :: layout(~{::title}, ~{::section})}">
 3<head>
 4    <title>Page Title</title>
 5</head>
 6<body>
 7<section>
 8    <p>Page content</p>
 9    <div>Included on page</div>
10</section>
11</body>
12</html>

在这个文件里,html标签将被替换为layout,但是在layout里,title和content将已经被title和section块分别替换。

如果愿意的话,layout能够由若干个片段组成,如header和footer。

(完)

以上是关于Thymeleaf(第八章)模板布局的主要内容,如果未能解决你的问题,请参考以下文章

Thymeleaf 模板

第八章 android-布局

Spring boot:thymeleaf 没有正确渲染片段

第八章 Django的模板

thymeleaf 片段渲染后重新加载 javascript

第八章