在编写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的模板继承功能。