在编写login.html
,logout.html
还有error.html
的时候,其中的head
标签内部的样式都是相同的,有没有办法减少这一部分重复代码呢。
用Django
开发Web
应用的时候,模板可以比较方便的通过{% include %}
类似的标记语言来继承模板,达到减少编写重复代码的作用。Thymeleaf
是否也有类似功能呢?
Thymeleaf
的Template layout
功能
还真有这个功能,官网文档这里就有例子,相当不错。
定义fragment
这个功能就是在一个页面中,通过th:fragment
定义一个标签及内部的范围为一个fragment
,然后在另外的模板中,可以通过一个特殊的模板表达式 ~{模板名 :: fragment
名} 来引用这个fragment
,而具体插入fragment
的方法,又有三种。
就我们之前提到这三个模板而言,head
标签之内的部分,除了title
之外,全部都相同,那么我们可以创建一个fragment
,包含这些相同的部分,然后修改这三个模板来引入这些内容。
先来创建一个fragment
模板,在目录/templates/
下创建/shared/
目录用来存放这些共享的fragment
模板,在其中创建一个head.html
文件,如下:
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<div th:fragment="headForLoginandLogout">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link rel="shortcut icon" th:href="@{/img/favicon.ico}"/>
<style>
......
</style>
<style>
......
</style>
</div>
<title>错误</title>
</head>
</html>
很显然,这个html
文件的head
标签中的结构是不正确的,我们刻意用了一个div
标签包在相同的部分外边,并且给这个fragment
起了一个名称叫做headForLoginandLogout
。
下一步,就是在我们的标签中引入这一块内容。以login.html
为例,要如何来修改呢,这就涉及到Thymeleaf
引入fragment
的三种方式。
还可以不定义fragment
,而是通过id
属性来引用,会在下边一并介绍。
引入fragment
正常情况下,引入定义的fragment
有三种方式,分别是:
th:insert
:将fragment
所在的标签及其内容,放到th:insert
所在标签的内部th:replace
:将fragment
所在的标签直接替换掉th:insert
所在标签th:include
:将fragment
所在的标签的内部放入到th:insert
所在标签
除此之外,还有一种方式是不使用~{模板名::fragment名称}
,而是使用~{模板名::#id名称}
来将一个定义了id
属性的标签当成一个fragment
。
现在回到我们的login.html
,修改成这样:
<head>
<div th:replace="/shared/head::headForLoginandLogout"></div>
<title>登录</title>
</head>
,结果很尴尬的发现,这三种方法,都无法直接将fragment
引入到我们的页面中,由于head
中不允许出现div
标签,最终渲染的页面会出现一些结构上的问题。
带参数引用fragment
这个时候我们要重新考虑一下这个问题,既然不能这么引入,由于每个页面仅仅只有title
属性不同,Thymeleaf
提供了参数化的fragment
供我们使用。
先把fragment
修改一下,正常使用整个head
标签,但是引入一个参数叫做title
:
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="headforLoginandLogout(title)">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link rel="shortcut icon" th:href="@{/img/favicon.ico}"/>
<style>
......
</style>
<style>
......
</style>
<title th:text="${title}">财务管理系统</title>
</head>
<body>
</body>
</html>
参数用于渲染title
标签中的内容,现在我们先来尝试一下在login.html
中引入:
<!doctype html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head th:replace="/shared/head::headforLoginandLogout('登录')"></head>
<body class="text-center">
......
</body>
</html>
head
标签只有一行,就是用模板的fragment
替换,然后传入了参数,这里把参数写死了,就是字符串“登录”,到底行不行,运行一下项目,发现正常了。
这里还可以改进一下,目前是写死了传入的参数,实际上可以把参数动态传入,来修改一下/account/login
的控制器:
@RequestMapping("/login")
public String login(@RequestParam(required = false, name = "error") String failed, HttpServletRequest request, Model model) {
// 如果用户已经登录,直接重定向到首页
if (request.getUserPrincipal() != null) {
return "forward:/";
}
//URL中的error参数存在,则在model中放入一个"failed"键,在页面中通过if来判断。
if (failed != null) {
model.addAttribute("failed", "用户名或密码错误,或用户未激活,请联系管理员");
}
model.addAttribute("title", "登录");
return "/account/login";
}
通过model
添加了一个title
属性,值为登录,然后略微修改一下login.html
模板:
<head th:replace="/shared/head::headforLoginandLogout(${title})"></head>
之后重启项目,发现成功了。实际上就编写好了一个可以传入参数的fragment
。今后在编写控制器的时候,记得给model
加上title
属性即可。
不过先不用把logout.html
和error.html
都如法炮制,我们可以把不同的fragment
集中到一个文件中去。
编写标准的head
部分和script
部分
目前的这个head
,只是用于上边几个需要居中的页面。考虑到我们后边的页面除了bootstrap
,还有font-awesome
库,因此可以在一个模板中编写好这些内容,直接引入即可,我们把head.html
先删除,当然此时login.html
中引入的部分也会失效,不过我们来编一个包含原来head.html
的内容,以及标准的head
部分和script
部分的文件,叫做base.html
:
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="headforLoginandLogout(title)">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link rel="shortcut icon" th:href="@{/img/favicon.ico}"/>
<style>
......
</style>
<style>
......
</style>
<title th:text="${title}">财务管理系统</title>
</head>
<head th:fragment="standardHead(title)">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link href="/css/all.min.css" th:href="@{/css/all.min.css}" rel="stylesheet">
<link rel="shortcut icon" th:href="@{/img/favicon.ico}"/>
<title th:text="${title}">财务管理系统</title>
</head>
<body>
<script th:fragment="javascript" src="/js/bootstrap.bundle.min.js" th:src="@{/js/bootstrap.bundle.min.js}"></script>
</body>
</html>
这个模板中定义了三个fragment
,分别是用于登录页面和居中的headforLoginandLogout
,不含额外样式、用于标准界面的standardHead
,以及导入BootStrap
所需JS
文件的javascript
,之后就可以根据需要从这个文件中引入。
修改控制器和其他模板
此时刚刚提到的这三个模板,head
的部分都可以只保留一行即可:
<head th:replace="/shared/base::headforLoginandLogout(${title})"></head>
然后只需要修改每个页面对应的控制器,加上title
属性即可,error.html
由于没有控制器,直接把title
写死就行了,不再赘述。
然后我们再编写一个临时的根目录的控制器,编写一个对应的页面,放在/templates/homepage/index.html
,在其中导入一下标准页面和JS的fragment
,来检验我们做的对不对,页面如下:
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{/shared/base::standardHead(${title})}">
</head>
<body>
<h3 class="text-primary text-center">这是首页</h3>
<script th:replace="~{/shared/base::javascript}"></script>
</body>
</html>
这里可能会发现,刚才引入模板的语句都没有使用~{}
这个引入表达式,实际上对于模板名::
fragment名称
,也可以不在外边加上这个表达式。
控制器如下:
package cc.conyli.fms.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/")
public class HomePageController {
@RequestMapping("/")
public String homePage(Model model) {
model.addAttribute("title", "财务管理系统");
return "/homepage/index";
}
}
经过测试一切OK
,顺利搞定了Thymeleaf
的模板继承功能。