Thymeleaf特点(6)- 模块化

本文最后更新于:2 年前

这次我们学习灵活的布局,或者说提高代码的复用性。

无论你浏览什么网页,都会(基本上)看到它——没错,它就是导航栏。

bilibili导航栏

W3school导航栏

gitee导航栏

ProcessOn导航栏

以上列举了 4 个网站的导航栏 ,在网站内部跳转时,它们基本上不变,或只是变更少数内容。

要为网站的每个页面都添上导航栏,必定会造成重复的代码,一个好的模板怎么会容忍这种事情发生?!于是, Thymeleaf 马上就提供了 th:fragment 属性来避免这种事的发生,现在我们来试试

假如有一个简易(过于简陋了,将来会解决这个问题)的导航栏:用来在网站内跳转不同的页面。

简易菜单

我们要在网站的每个页面都添加上(例如:主页面2、订阅点这里、主页面 的跳转链接)

先单独创建一个 menu.html 文件,专门写导航栏的代码:(也可以直接写在某一个页面内)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Good Thymes Virtual Grocery</title>
</head>
<body>
<menu th:fragment="menu-copy">
<p>menu</p>
<a href="/home2.html"
th:href="@{/home2}">主页面2</a>
<a href="/subscribe"
th:href="@{/subscribe}">订阅点这里</a>
<a href="/"
th:href="@{/}">主页面</a>
</menu>
</body>
</html>

并给主要标签加上 th:fragment 属性,值随便取一个,就叫 menu-copy

接着在 home.html(主页 html)、home2.html(主页2 html)、subscribe.html(订阅界面 html),文件中加入:

1
<div th:insert="~{menu :: menu-copy}"></div>

或:

1
<div th:replace="~{menu :: menu-copy}"></div>

或:

1
<div th:include="~{menu :: menu-copy}"></div>

也可以把 ~{} 去掉,但是 th:include 属性在 Thymeleaf 3.0之后就不推荐使用了,那它们的区别是什么?

th:insert 是最简单的,它将目标标签作为自己本地标签的子标签插入进来,带目标标签的属性

th:replace 直接把本地标签换成了目标标签,带属性

th:include 只是将目标标签的内容(例如它的子标签)插入到本地标签来,不带属性

所以上述代码结果分别为:

1
2
3
4
5
6
7
8
9
10
11
<div>
<menu th:fragment="menu-copy">
<p>menu</p>
<a href="/home2.html"
th:href="@{/home2}">主页面2</a>
<a href="/subscribe"
th:href="@{/subscribe}">订阅点这里</a>
<a href="/"
th:href="@{/}">主页面</a>
</menu>
</div>

and

1
2
3
4
5
6
7
8
9
<menu th:fragment="menu-copy">
<p>menu</p>
<a href="/home2.html"
th:href="@{/home2}">主页面2</a>
<a href="/subscribe"
th:href="@{/subscribe}">订阅点这里</a>
<a href="/"
th:href="@{/}">主页面</a>
</menu>

and

1
2
3
4
5
6
7
8
9
<div>
<p>menu</p>
<a href="/home2.html"
th:href="@{/home2}">主页面2</a>
<a href="/subscribe"
th:href="@{/subscribe}">订阅点这里</a>
<a href="/"
th:href="@{/}">主页面</a>
</div>

它们也有共同的部分:片段表达式

· ~{templatename::selector} :templatenamehtml 文件名,selector 是文件中目标片段的 th:fragment 属性值。

· ~{templatename}:表示复制整个模板。

· ~{::selector} or ~{this::selector} :表示在当前模板中用 selector 匹配目标片段。

注意到 templatename 必须要能够被当前模板引擎使用的模板解析器正确解析 ,否则如果 selector 没有匹配到片段,会报异常,~{} 依然可以被省略。

Both templatename and selector can be fully-featured expressions(even conditionals!):

1
<div th:insert="menu :: (${page.home}? #{menu.home}:#{menu.common})"></div>

在上面的例子中,主页面会加载一个特有的导航栏,前提是 menu 文件中有该目标片段。

(没有主页面跳转链接的导航栏,本来就不需要φ(* ̄0 ̄))

🙋‍♀️:直接在主页面的模板中指定用主页面导航栏不就行了?需要判断吗?

🤦‍♂️:好像也是φ(゜▽゜*)♪,咳咳~这就是一个说明可以用条件式的例子罢了(🤷‍♂️我不管~~)

id 属性可以代替 th:fragment ,片段被复制到新的模板中之后也可以引用模板中的变量,there is a example

1
2
3
4
5
6
7
8
9
10
@GetMapping("/")
public String home(Model model){
String enname = "TCJ";
String cnname = "田超杰";
String nname = null;
boolean admin = true;
model.addAttribute("user",new users(enname,cnname,nname,admin));
model.addAttribute("webs",webs);
return "home";
}

首先,在主页控制器往模型中添加 user 数据

然后,特制 导航栏

1
2
3
4
<menu id="houtai-copy" th:if="${user.admin}">
<a href="/houtai.html"
th:href="@{/hhhhhhhoutai}">管理员后台</a>
</menu>

最后,在主页模板中进行复制

1
<div th:insert="menu :: #houtai-copy"></div>

只有管理员访问网站时才会看到 管理员后台 的跳转链接。

admin后台

为了进一步提高代码的复用性,Thymeleaf 提供了可参数化的片段签名,例如

Java 的方法、CPython 的函数等,那 Thymeleaf 的 “函数” 的语法是什么呢?

构造语法
1
2
3
4
<!--属性值(Var)-->
<div th:fragment="frag(onevar,twovar)">
<p th:text="${onevar} + 'and' + ${twovar}">...</p>
</div>

和编程语言中的构造语法基本一致。

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

第二个语法中变量赋值语句可以乱序,例如

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

即使在构造的时候没有声明参数,也可以在调用的时候加入
1
2
<!--声明-->
<div th:fragment="frag"> ... </div>
1
2
<!--调用-->
<div th:replace="::frag(onevar=${value1},towvar=${value2})">...</div>
1
2
<!--调用时添加参数的第二种方法-->
<div th:replace="::frag" th:with="onevar=${value1},twovar=${value2}">...</div>

和编程语言不同的是,被调用的片段可以使用该模板的上下文变量,而 Java 只允许使用同一个类中的全局变量和传进来的参数。


现在,扩展一下语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--构造,注意 title 和 links 变量的使用-->
<head th:fragment="common_header(title,links)">

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

<!-- Common styles and scripts -->
<link rel="stylesheet" type="text/css" media="all" th:href="@{/css/shoppingapp.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>
1
2
3
4
5
6
7
8
9
10
11
...
<!--调用,注意参数的语法-->
<head th:replace="base :: common_header(~{::title},~{::link})">

<title>Shopping - Main</title>

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

</head>
...

运行它并且结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
<head>

<title>Shopping - Main</title>

<!-- Common styles and scripts -->
<link rel="stylesheet" type="text/css" media="all" href="/sho/css/shoppingapp.css">
<link rel="shortcut icon" href="/sho/images/favicon.ico">
<script type="text/javascript" src="/sho/sh/scripts/codebase.js"></script>
<link rel="stylesheet" href="/sho/css/bootstrap.min.css">
<link rel="stylesheet" href="/sho/themes/smoothness/jquery-ui.css">

</head>
...

再稍改一下

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

结果是:

1
2
3
4
5
6
7
8
9
10
11
12
...
<head>

<title>Shopping - Main</title>

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

</head>
...

使用默认值的语法 _

1
2
3
4
5
6
7
8
9
10
...
<head th:replace="base :: common_header(_,~{::link})">

<title>Shopping - Main</title>

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

</head>
...

'_' results in current part of the fragment not being executed at all( title = no-operation),so the result is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
<head>

<title>The shopping application</title>

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

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

</head>
...

th:insert 的表达式也可以是conditionals:

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

又或者:

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

之前学习的

1
<div th:insert="~{templatename :: selector}"></div>

templatename or selector 未被正确解析或匹配到目标片段时会抛出异常,th:assert 属性与此类似,它通过创建一个表达式列表,并且当列表中的值均为 true 或等效与 true 时才会执行代码,否则会抛出异常:

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

可以用它来检验参数。

除此之外,我们可以通过模板解析器来检查模板资源是否存在,即通过它们的 checkExistence 标志。我们也可以把片段是否存在作为一个默认的条件:

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

简言之就是若该模板和目标片段都存在,该语法和 th:insert="~{templatename :: selector}" 基本一样;如果不存在,返回 null ,它不会报异常。

模板继承

th:fragment 属性移到 <html> 标签中完成构造,then 在另一个模板的 <html> 标签中加入 th:replace 属性完成调用,切不可用 th:insert 否则会造成双重 <html> 标签。