Skip to content

Thymeleaf

Use the Thymeleaf template engine

虽然JSP为我们带来了便捷,但是其缺点也是显而易见的,那么有没有一种既能实现模板,又能兼顾前后端分离的模板引擎呢?

Thymeleaf(百里香叶)是一个适用于Web和独立环境的现代化服务器端Java模板引擎,官方文档:https://www.thymeleaf.org/documentation.html。

Screenshot 2024-05-10 at 12.04.41

Screenshot 2024-05-10 at 12.05.33

那么它和JSP相比,好在哪里呢,我们来看官网给出的例子:

<table>
  <thead>
    <tr>
      <th th:text="#{msgs.headers.name}">Name</th>
      <th th:text="#{msgs.headers.price}">Price</th>
    </tr>
  </thead>
  <tbody>
    <tr th:each="prod: ${allProducts}">
      <td th:text="${prod.name}">Oranges</td>
      <td th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.99</td>
    </tr>
  </tbody>
</table>

我们可以在前端页面中填写占位符,而这些占位符的实际值则由后端进行提供,这样,我们就不用再像JSP那样前后端都写在一起了。

Screenshot 2024-05-10 at 12.11.01

那么我们来创建一个例子感受一下,首先还是新建一个项目,注意,在创建时,勾选Thymeleaf依赖。

首先编写一个前端页面,名称为test.html,注意,是放在resource目录下,在html标签内部添加xmlns:th="http://www.thymeleaf.org"引入Thymeleaf定义的标签属性:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div th:text="${title}"></div>
</body>
</html>

Screenshot 2024-05-10 at 12.17.23

接着我们编写一个Servlet作为默认页面:

@WebServlet("/index")
public class HelloServlet extends HttpServlet {

    TemplateEngine engine;
    @Override
    public void init() throws ServletException {
        engine = new TemplateEngine();
        ClassLoaderTemplateResolver r = new ClassLoaderTemplateResolver();
        engine.setTemplateResolver(r);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Context context = new Context();
        context.setVariable("title", "我是标题");
        engine.process("test.html", context, resp.getWriter());
    }
}

我们发现,浏览器得到的页面,就是已经经过模板引擎解析好的页面,而我们的代码依然是后端处理数据,前端展示数据,因此使用Thymeleaf就能够使得当前Web应用程序的前后端划分更加清晰。

虽然Thymeleaf在一定程度上分离了前后端,但是其依然是在后台渲染HTML页面并发送给前端,并不是真正意义上的前后端分离。

Screenshot 2024-05-10 at 12.27.24

Screenshot 2024-05-10 at 12.57.05

其他地方开了要记得关.

Screenshot 2024-05-10 at 13.07.52

Screenshot 2024-05-10 at 13.08.13

Thymeleaf语法基础

那么,如何使用Thymeleaf呢?

首先我们看看后端部分,我们需要通过TemplateEngine对象来将模板文件渲染为最终的HTML页面:

TemplateEngine engine;
@Override
public void init() throws ServletException {
    engine = new TemplateEngine();
    //设定模板解析器决定了从哪里获取模板文件,这里直接使用ClassLoaderTemplateResolver表示加载内部资源文件
    ClassLoaderTemplateResolver r = new ClassLoaderTemplateResolver();
    engine.setTemplateResolver(r);
}

由于此对象只需要创建一次,之后就可以一直使用了。接着我们来看如何使用模板引擎进行解析:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //创建上下文,上下文中包含了所有需要替换到模板中的内容
    Context context = new Context();
    context.setVariable("title", "<h1>我是标题</h1>");
    //通过此方法就可以直接解析模板并返回响应
    engine.process("test.html", context, resp.getWriter());
}

操作非常简单,只需要简单几步配置就可以实现模板的解析。接下来我们就可以在前端页面中通过上下文提供的内容,来将Java代码中的数据解析到前端页面。

接着我们来了解Thymeleaf如何为普通的标签添加内容,比如我们示例中编写的:

<div th:text="${title}"></div>

Screenshot 2024-05-10 at 13.22.55

我们使用了th:text来为当前标签指定内部文本,注意任何内容都会变成普通文本,即使传入了一个HTML代码,如果我希望向内部添加一个HTML文本呢?我们可以使用th:utext属性:

<div th:utext="${title}"></div>

并且,传入的title属性,不仅仅只是一个字符串的值,而是一个字符串的引用,我们可以直接通过此引用调用相关的方法:

<div th:text="${title.toLowerCase()}"></div>

这样看来,Thymeleaf既能保持JSP为我们带来的便捷,也能兼顾前后端代码的界限划分。

Screenshot 2024-05-10 at 13.25.04

Screenshot 2024-05-10 at 13.25.25

Screenshot 2024-05-10 at 13.26.10除了替换文本,它还支持替换一个元素的任意属性,我们发现,th:能够拼接几乎所有的属性,一旦使用th:属性名称,那么属性的值就可以通过后端提供了,比如我们现在想替换一个图片的链接:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Context context = new Context();
    context.setVariable("url", "http://n.sinaimg.cn/sinakd20121/600/w1920h1080/20210727/a700-adf8480ff24057e04527bdfea789e788.jpg");
    context.setVariable("alt", "图片就是加载不出来啊");
    engine.process("test.html", context, resp.getWriter());
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <img width="700" th:src="${url}" th:alt="${alt}">
</body>
</html>

现在访问我们的页面,就可以看到替换后的结果了。

Screenshot 2024-05-10 at 13.35.10

Screenshot 2024-05-10 at 13.35.25

Thymeleaf还可以进行一些算术运算,几乎Java中的运算它都可以支持:

<div th:text="${value % 2}"></div>

同样的,它还支持三元运算:

<div th:text="${value % 2 == 0 ? 'yyds' : 'lbwnb'}"></div>

多个属性也可以通过+进行拼接,就像Java中的字符串拼接一样,这里要注意一下,字符串不能直接写,要添加单引号:

<div th:text="${name}+' 我是文本 '+${value}"></div>

Screenshot 2024-05-10 at 14.22.11

Thymeleaf流程控制语法

除了一些基本的操作,我们还可以使用Thymeleaf来处理流程控制语句,当然,不是直接编写Java代码的形式,而是添加一个属性即可。

首先我们来看if判断语句,如果if条件满足,则此标签留下,若if条件不满足,则此标签自动被移除:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Context context = new Context();
    context.setVariable("eval", true);
    engine.process("test.html", context, resp.getWriter());
}
<div th:if="${eval}">我是判断条件标签</div>

Screenshot 2024-05-14 at 13.25.05

Screenshot 2024-05-14 at 13.25.13

th:if会根据其中传入的值或是条件表达式的结果进行判断,只有满足的情况下,才会显示此标签,具体的判断规则如下:

  • 如果值不是空的:
  • 如果值是布尔值并且为true
  • 如果值是一个数字,并且是非零
  • 如果值是一个字符,并且是非零
  • 如果值是一个字符串,而不是“错误”、“关闭”或“否”
  • 如果值不是布尔值、数字、字符或字符串。
  • 如果值为空,th:if将计算为false

th:if还有一个相反的属性th:unless,效果完全相反,这里就不演示了。

我们接着来看多分支条件判断,我们可以使用th:switch属性来实现:

<div th:switch="${eval}">
    <div th:case="1">我是1</div>
    <div th:case="2">我是2</div>
    <div th:case="3">我是3</div>
</div>

Screenshot 2024-05-14 at 13.31.46

Screenshot 2024-05-14 at 13.32.14

只不过没有default属性,但是我们可以使用th:case="*"来代替:

<div th:case="*">我是Default</div>

Screenshot 2024-05-14 at 13.35.52

Screenshot 2024-05-14 at 13.35.59

最后我们再来看看,它如何实现遍历,假如我们有一个存放书籍信息的List需要显示,那么如何快速生成一个列表呢?我们可以使用th:each来进行遍历操作:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Context context = new Context();
    context.setVariable("list", Arrays.asList("伞兵一号的故事", "倒一杯卡布奇诺", "玩游戏要啸着玩", "十七张牌前的电脑屏幕"));
    engine.process("test.html", context, resp.getWriter());
}
<ul>
    <li th:each="title : ${list}" th:text="'《'+${title}+'》'"></li>
</ul>

Screenshot 2024-05-14 at 13.37.41

Screenshot 2024-05-14 at 13.38.14

th:each中需要填写 "单个元素名称 : ${列表}",这样,所有的列表项都可以使用遍历的单个元素,只要使用了th:each,都会被循环添加。因此最后生成的结果为:

<ul>
        <li>《伞兵一号的故事》</li>
        <li>《倒一杯卡布奇诺》</li>
        <li>《玩游戏要啸着玩》</li>
        <li>《十七张牌前的电脑屏幕》</li>
    </ul>

我们还可以获取当前循环的迭代状态,只需要在最后添加iterStat即可,从中可以获取很多信息,比如当前的顺序:

<ul>
    <li th:each="title, iterStat : ${list}" th:text="${iterStat.index}+'.《'+${title}+'》'"></li>
</ul>

状态变量在th:each属性中定义,并包含以下数据:

  • 当前迭代索引,以0开头。这是index属性。
  • 当前迭代索引,以1开头。这是count属性。
  • 迭代变量中的元素总量。这是size属性。
  • 每个迭代的迭代变量。这是current属性。
  • 当前迭代是偶数还是奇数。这些是even/odd布尔属性。
  • 当前迭代是否是第一个迭代。这是first布尔属性。
  • 当前迭代是否是最后一个迭代。这是last布尔属性。

通过了解了流程控制语法,现在我们就可以很轻松地使用Thymeleaf来快速替换页面中的内容了。

Screenshot 2024-05-14 at 13.41.48

Screenshot 2024-05-14 at 13.42.05

Thymeleaf模板布局

在某些网页中,我们会发现,整个网站的页面,除了中间部分的内容会随着我们的页面跳转而变化外,有些部分是一直保持一个状态的,比如打开小破站,我们翻动评论或是切换视频分P的时候,变化的仅仅是对应区域的内容,实际上,其他地方的内容会无论内部页面如何跳转,都不会改变。

Thymeleaf就可以轻松实现这样的操作,我们只需要将不会改变的地方设定为模板布局,并在不同的页面中插入这些模板布局,就无需每个页面都去编写同样的内容了。现在我们来创建两个页面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div class="head">
        <div>
            <h1>我是标题内容,每个页面都有</h1>
        </div>
        <hr>
    </div>
    <div class="body">
        <ul>
            <li th:each="title, iterStat : ${list}" th:text="${iterStat.index}+'.《'+${title}+'》'"></li>
        </ul>
    </div>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div class="head">
        <div>
            <h1>我是标题内容,每个页面都有</h1>
        </div>
        <hr>
    </div>
    <div class="body">
        <div>这个页面的样子是这样的</div>
    </div>
</body>
</html>

接着将模板引擎写成工具类的形式:

public class ThymeleafUtil {

    private static final TemplateEngine engine;
    static  {
        engine = new TemplateEngine();
        ClassLoaderTemplateResolver r = new ClassLoaderTemplateResolver();
        engine.setTemplateResolver(r);
    }

    public static TemplateEngine getEngine() {
        return engine;
    }
}
@WebServlet("/index2")
public class HelloServlet2 extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Context context = new Context();
        ThymeleafUtil.getEngine().process("test2.html", context, resp.getWriter());
    }
}

Screenshot 2024-05-14 at 13.52.01

Screenshot 2024-05-14 at 13.52.31

Screenshot 2024-05-14 at 13.52.45

现在就有两个Servlet分别对应两个页面了,但是这两个页面实际上是存在重复内容的,我们要做的就是将这些重复内容提取出来。

我们单独编写一个head.html来存放重复部分:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<body>
    <div class="head" th:fragment="head-title">
        <div>
            <h1>我是标题内容,每个页面都有</h1>
        </div>
        <hr>
    </div>
</body>
</html>

现在,我们就可以直接将页面中的内容快速替换:

<div th:include="head.html::head-title"></div>
<div class="body">
    <ul>
        <li th:each="title, iterStat : ${list}" th:text="${iterStat.index}+'.《'+${title}+'》'"></li>
    </ul>
</div>

Screenshot 2024-05-14 at 13.56.06

我们可以使用th:insertth:replaceth:include这三种方法来进行页面内容替换,那么th:insertth:replace(和th:include,自3.0年以来不推荐)有什么区别?

  • th:insert最简单:它只会插入指定的片段作为标签的主体。
  • th:replace实际上将标签直接替换为指定的片段。
  • th:includeth:insert相似,但它没有插入片段,而是只插入此片段的内容

Screenshot 2024-05-14 at 13.58.26

Screenshot 2024-05-14 at 13.59.22

Screenshot 2024-05-14 at 14.00.29

你以为这样就完了吗?它还支持参数传递,比如我们现在希望插入二级标题,并且由我们的子页面决定:

<div class="head" th:fragment="head-title">
    <div>
        <h1>我是标题内容,每个页面都有</h1>
        <h2>我是二级标题</h2>
    </div>
    <hr>
</div>

稍加修改,就像JS那样添加一个参数名称:

<div class="head" th:fragment="head-title(sub)">
    <div>
        <h1>我是标题内容,每个页面都有</h1>
        <h2 th:text="${sub}"></h2>
    </div>
    <hr>
</div>

现在直接在替换位置添加一个参数即可:

<div th:include="head.html::head-title('这个是第1个页面的二级标题')"></div>
<div class="body">
    <ul>
        <li th:each="title, iterStat : ${list}" th:text="${iterStat.index}+'.《'+${title}+'》'"></li>
    </ul>
</div>

这样,不同的页面还有着各自的二级标题。

Screenshot 2024-05-14 at 14.03.56

Screenshot 2024-05-14 at 14.04.10