Fms-Java版开发实录:06 Thymeleaf模板继承

Fms-Java版开发实录:06 Thymeleaf模板继承

用Django开发Web应用的时候,模板可以比较方便的通过{% include %}类似的标记语言来继承模板,达到减少编写重复代码的作用。Thymeleaf行不行呢?

在编写login.htmllogout.html还有error.html的时候,其中的head标签内部的样式都是相同的,有没有办法减少这一部分重复代码呢。

Django开发Web应用的时候,模板可以比较方便的通过{% include %}类似的标记语言来继承模板,达到减少编写重复代码的作用。Thymeleaf是否也有类似功能呢?

ThymeleafTemplate 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有三种方式,分别是:

  1. th:insert:将fragment所在的标签及其内容,放到th:insert所在标签的内部
  2. th:replace:将fragment所在的标签直接替换掉th:insert所在标签
  3. 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.htmlerror.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的模板继承功能。

LICENSED UNDER CC BY-NC-SA 4.0
Comment