第5章 集成 Thymeleaf 模版引擎

5.1、Thymeleaf

5.1.1、Thymeleaf 简介

  • 模板引擎:Thymeleaf 是一种用于Web和独立环境的现代服务器端的 Java 模板引擎。能够处理 HTML,XML,JavaScript,CSS 甚至纯文本。类似于 JSP、Freemarker。

  • 自然模板,原型即界面:Thymeleaf建立在自然模板的概念之上,以不影响模板作为设计原型的方式将其逻辑注入到模板文件中。 这改善了设计沟通,弥合了前端设计和开发人员之间的理解偏差。

  • 语法优雅易懂:支持 OGNLSpringEL 表达式

  • 遵循 Web 标准:支持 HTML5

5.1.2、Thymeleaf 标准方言

1、什么是标准方言?

定义了一组功能的 Thymeleaf 语法。

例如:包含以th前缀开头的属性,如<span th:text="..."><span data-th-text="...">

Thymeleaf 模板:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
  </head>
  <body>
    <p th:text="#{home.welcome}">Welcome to our grocery store!</p>
  </body>
</html>

在接下来的内容中,我们会学到关于 Thymeleaf 的一下内容:

Thymeleaf

2、标准表达式语法

Thymeleaf属性允许将它们的值设置为或包含表达式,由于它们使用的方言,我们将其称为标准表达式。这些表达式可以有五种类型:

  • ${...} : 变量表达式。
  • *{...} : 选择表达式。
  • #{...} : 消息 (i18n) 表达式。
  • @{...} : 链接 (URL) 表达式。
  • ~{...} : 片段表达式。

2.1、变量表达式

变量表达式是OGNL表达式 - 如果将ThymeleafSpring - 集成在上下文变量上(也称为Spring术语中的模型属性),则为Spring EL。 它们看起来像这样:

${session.user.name}

它们作为属性值或作为它们的一部分,取决于属性:

<span th:text="${book.author.name}">

2.2、选择表达式

选择表达式就像变量表达式一样,它们不是整个上下文变量映射上执行,而是在先前选择的对象。 它们看起来像这样:

*{customer.name}

它们所作用的对象由th:object属性指定:

<div th:object="${book}">
  ...
  <span th:text="*{title}">...</span>
  ...
</div>

2.3、消息(i18n)表达式

消息表达式(通常称为文本外部化,国际化或i18n)允许从外部源(如:.properties)文件中检索特定于语言环境的消息,通过键来引用这引用消息。

在Spring应用程序中,它将自动与Spring的MessageSource机制集成。如下:

#{main.title}
#{message.entrycreated(${entryId})}

以下是在模板中使用它们的方式:

<table>
  ...
  <th th:text="#{header.address.city}">...</th>
  <th th:text="#{header.address.country}">...</th>
  ...
</table>

2.4、链接(URL)表达式

链接表达式在构建URL并向其添加有用的上下文会话信息(通常称为URL重写的过程)
因此,对于部署在Web服务器的/myapp上下文中的Web应用程序,可以使用以下表达式:

<a th:href="@{/order/list}">...</a>

可以转换成如下的东西:

<a href="/myapp/order/list">...</a>

甚至,如果需要保持会话,并且cookie未启用(或者服务器还不知道),那么生成的格式为:

<a href="/myapp/order/list;jsessionid=s2ds3fa31abd241e2a01932">...</a>

网址也可以带参数,如下所示:

<a th:href="@{/order/details(id=${orderId},type=${orderType})}">...</a>

这将产生类似以下的结果 -

<!-- 注意&符号会在标签属性中进行HTML转义... -->
<a href="/myapp/order/details?id=23&type=online">...</a>

链接表达式可以是相对的,在这种情况下,应用程序上下文将不会被加到URL的前面:

<a th:href="@{../documents/report}">...</a>

也是服务器相对的(同样,没有应用程序上下文的前缀):

<a th:href="@{~/contents/main}">...</a>

和协议相关(就像绝对URL一样,但浏览器将使用与正在显示的页面相同的HTTP或HTTPS协议):

<a th:href="@{//static.mycompany.com/res/initial}">...</a>

当然,链接表达式也可以是绝对的:

<a th:href="@{http://www.mycompany.com/main}">...</a>

但是绝对(或协议相对)URL ,在 Thymeleaf 链接表达式中应该添加什么值? 很简单:由响应过滤器定义URL重写:在基于Servlet的Web应用程序中,对于每个输出的URL(上下文相对,相对,绝对…),在显示URL之前,Thymeleaf总是调用HttpServletResponse.encodeUrl(...)机制。 这意味着一个过滤器可以通过包装HttpServletResponse对象来为应用程序执行自定义的URL重写。

2.6、片段表达式

片段表达式是一种简单的方法用来表示标记的片段并将其移动到模板中。 由于这些表达式,片段可以被复制,传递给其他模板的参数等等。

最常见的是使用th:insertth:replace来插入片段:

<div th:insert="~{commons :: main}">...</div>

但是它们可以在任何地方使用,就像任何其他变量一样:

<div th:with="frag=~{footer :: #main/text()}">
  <p th:insert="${frag}">
</div>

2.5、 文字和操作

有很多类型的文字和操作可用,它们分别如下:

  • 文字
    • 文本文字,例如:'one text', 'Another one!',
    • 数字文字,例如:0,10, 314, 31.01, 112.83,
    • 布尔文字,例如:true,false
    • Null文字,例如:Null
    • 文字标记,例如:one, sometext, main,
  • 文本操作:
    • 字符串连接:+
    • 文字替换:|The name is ${name}|
  • 算术运算:
    • 二进制操作:+, -, *, /, %
    • 减号(一元运算符):-
  • 布尔运算:
    • 二进制运算符,and,or
    • 布尔否定(一元运算符):!,not
  • 比较和相等:
    • 比较运算符:>,<,>=,<=(gt,lt,ge,le)
    • 相等运算符:==, != (eq, ne)
  • 条件操作符:
    • If-then:(if) ? (then)
    • If-then-else:(if) ? (then) : (else)
    • Default: (value) ?: (defaultvalue)

5.1.3、设置属性值

1、设置任意属性值:th:att

假设我们的网站发布了一个时事通讯,我们希望我们的用户能够订阅它,所以我们创建一个带有表单的/WEB-INF/templates/subscribe.html模板:

<form action="subscribe.html">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" />
  </fieldset>
</form>

与 Thymeleaf 一样,此模板更像是静态原型,而不是 web application 的模板。首先,我们表单中的action属性静态链接到模板文件本身,因此没有地方可以进行有用的 URL 重写。其次,提交按钮中的value属性使其显示英文文本,但我们希望它能够国际化。

使用th:attr属性,以及更改其设置的标记属性的 value 的能力:

<form action="subscribe.html" th:attr="action=@{/subscribe}">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
  </fieldset>
</form>

这个概念非常简单:th:attr只需要一个为属性赋予 value 的表达式。创建了相应的控制器和消息 files 后,处理该文件的结果将是:

<form action="/gtvg/subscribe">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="¡Suscríbe!"/>
  </fieldset>
</form>

除了新的属性值之外,您还可以看到 applicacion context name 已自动添加到/gtvg/subscribe中的 URL 基础作为前缀。

1574089663756

2、将 value 设置为特定属性

到现在为止,你可能会想到以下内容:

<input type="submit" value="Subscribe!" th:attr="value=#{subscribe .submit}"/>

这是一个非常丑陋的标记。在属性的 value 中指定赋值可能非常实用,但如果你必须在 time 中完成,那么它不是创建模板的最优雅方式。

Thymeleaf 同意你的意见,这就是为什么在模板中几乎不使用th:attr的原因。通常,您将使用其任务设置特定标记属性的其他th:*属性(而不仅仅是th:attr之类的任何属性)。

对于 example,要设置value属性,请使用th:value

<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>

这看起来好多了!让我们尝试对form标签中的action属性执行相同的操作:

<form action="subscribe.html" th:action="@{/subscribe}">

简化如下图所示:

1574173606463

有很多这样的属性,每个属性都针对特定的 HTML5 属性,更多特定属性可以参考官方文档:

th:abbrth:acceptth:accept-charset
th:accesskeyth:actionth:align
th:altth:archiveth:audio
th:autocompleteth:axisth:background
.........

3、固定值布尔属性Fixed-value boolean 属性

HTML 具有 boolean 属性的概念,没有 value 的属性和 1 的 precence 意味着 value 是“true”。在 XHTML 中,这些属性只占用 1 value,这本身也是如此。

对于 example,checked

<input type="checkbox" name="option2" checked /> <!-- HTML -->
<input type="checkbox" name="option1" checked="checked" /> <!-- XHTML -->

标准方言包含允许您通过评估条件来设置这些属性的属性,因此如果计算为 true,则属性将设置为其固定 value,如果计算为 false,则不会设置该属性:

<input type="checkbox" name="active" th:checked="${user.active}" />

标准方言中存在以下 fixed-value boolean 属性,更多属性可以参考官方文档:

th:asyncth:autofocusth:autoplay
th:checkedth:controlsth:declare
th:defaultth:deferth:disabled
th:formnovalidateth:hiddenth:ismap
th:loopth:multipleth:novalidate
th:nowrapth:openth:pubdate
th:readonlyth:requiredth:reversed
th:scopedth:seamlessth:selected

5.1.4、迭代器

1、标准方言为我们提供了一个属性:th:each 来遍历属性值

<tr th:each="prod : ${prods}">
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

在上面看到的prod : ${prods}属性 value 意味着“对于评估${prods}的结果中的每个元素,使用名为 prod 的变量中的当前元素重复此模板片段。

  • 我们将${prods}称为迭代表达式或迭代变量。
  • 我们将prod称为迭代变量或简称为变量。

2、当使用th:each时,Thymeleaf 提供了一种用于跟踪迭代状态的机制:状态变量。

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

  • 当前迭代索引,从 0 开始。这是index property。
  • 当前迭代索引,从 1 开始。这是count property。
  • 迭代变量中元素的总量。这是size property。
  • 每次迭代的 iter 变量。这是current property。
  • 当前迭代是偶数还是奇数。这些是even/odd boolean properties。
  • 当前迭代是否是第一个。这是first boolean property。
  • 当前迭代是否是最后一次。这是last boolean property。

具体使用如下图中的例子所示:

1574174649832

5.1.5、条件语句

1、简单条件:“if”和“除非”

th:if属性:

1574174856433

此外,th:if有一个逆属性th:unless

<a href="comments.html"
   th:href="@{/comments(prodId=${prod.id})}" 
   th:unless="${#lists.isEmpty(prod.comments)}">view</a>

2、切换语句

还有一种方法可以使用 Java 中的等效开关结构有条件地显示内容:th:switch/th:case属性集。

只要有一个th:case属性计算为true,同一个 switch context 中的其他th:case属性都将被计算为false。默认选项指定为th:case="*"

<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#{roles.manager}">User is a manager</p>
  <p th:case="*">User is some other thing</p>
</div>

5.1.6、模板布局

1、定义和引用片段

在我们的模板中,我们通常希望包含来自其他模板的部分,例如页脚,页眉,菜单…

为了做到这一点,Thymeleaf 需要我们定义这些部分,“片段”,以便包含,这可以使用th:fragment属性来完成。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <body>
    <div th:fragment="copy">
      &copy; 2011 The Good Thymes Virtual Grocery
    </div>
  </body>
</html>

上面的 code 定义了一个名为copy的片段,我们可以使用th:insertth:replace属性中的一个轻松地在我们的主页中包含它(以及th:include,但是自 Thymeleaf 3.0 以来不再推荐使用它):

<body>
  ...
  <div th:insert="~{footer :: copy}"></div>
</body>

请注意,th:insert需要一个片段表达式(~{...}),它是一个导致片段的表达式。在上面的 example 中,这是一个 non-complex 片段表达式,(~{})封闭是完全可选的,所以上面的 code 等同于:

<body>
  ...
  <div th:insert="footer :: copy"></div> 
</body>

2、在没有 th:fragment 的情况下引用片段

由于 Markup Selector 的强大功能,我们可以包含不使用任何th:fragment属性的片段。它甚至可以是来自不同的 application 的标记 code,完全不了解 Thymeleaf:

...
<div id="copy-section">
  &copy; 2011 The Good Thymes Virtual Grocery
</div>
...

我们可以使用上面的片段简单地通过其id属性引用它,方式与 CSS 选择器类似:

<body>
  ...
  <div th:insert="~{footer :: #copy-section}"></div> 
</body>

3、th:insert 、 th:replace 和 th:include之间的区别

th:insertth:replaceth:include(自 3.0 以来不推荐)之间有什么区别:

  • th:insert是最简单的:它只是将指定的片段作为其 host 标记的主体插入。
  • th:replace实际上用指定的片段替换了它的 host 标记。
  • th:include类似于th:insert,但它不是插入片段,而是仅插入此片段的内容。

举例:

定义一个像这样的 HTML 片段:

<footer th:fragment="copy">
  &copy; 2011 The Good Thymes Virtual Grocery
</footer>

使用th:insert 、 th:replace、 th:include中进行片段引用,如下所示:

<body>
  ...
  <div th:insert="footer :: copy"></div>

  <div th:replace="footer :: copy"></div>

  <div th:include="footer :: copy"></div>
  
</body>

最终效果:

<body>
  ...
  <div>
    <footer>
      &copy; 2011 The Good Thymes Virtual Grocery
    </footer>
  </div>

  <footer>
    &copy; 2011 The Good Thymes Virtual Grocery
  </footer>

  <div>
    &copy; 2011 The Good Thymes Virtual Grocery
  </div>
  
</body>

更多内容读者可以自行参考官文档,这里只做简单入门介绍。

5.1.7、属性优先

在同一个标签中写入多个th:*属性会发生什么?例如:

<ul>
  <li th:each="item : ${items}" th:text="${item.description}">Item description here...</li>
</ul>

我们希望在th:text之前执行th:each属性,以便我们得到我们想要的结果,但是考虑到 HTML/XML 标准没有给写入标签中的属性的 order 赋予任何意义,优先级必须在 order 中的属性本身中建立机制,以确保它将按预期工作。

因此,所有 Thymeleaf 属性都定义了一个数字优先级,它建立了在标记中执行它们的顺序。这个顺序是:

优先级特征属性
1片段包含th:insert th:replace
2片段迭代th:each
3有条件的 evaluationth:if th:unless th:switch th:case
4局部变量定义th:object th:with
5一般属性修改th:attr th:attrprepend th:attrappend
6具体属性修改th:value th:href th:src ...
7文字(标签正文修改)th:text th:utext
8片段规范th:fragment
9片段删除th:remove

5.1.8、Comments(注释)

1、标准 HTML/XML comments

标准 HTML/XML comments <!-- ... -->可以在 Thymeleaf 模板中的任何位置使用。这些 comments 中的任何内容都不会由 Thymeleaf 处理,并将逐字复制到结果中:

<!-- User info follows -->
<div th:text="${...}">
  ...
</div>

2、Thymeleaf 解析器级注释块

Thymeleaf 将删除<!--/**/-->之间的所有内容,因此当模板静态打开时,这些 comment 块也可用于显示 code,知道在 Thymeleaf 处理它时它将被删除:

<!--/*--> 
  <div>
     you can see me only before Thymeleaf processes me!
  </div>
<!--*/-->

3、原型注释块

Thymeleaf prototype-only :当模板静态打开(比如作为原型)时,Thymeleaf 允许定义标记为 原型注释的特殊注释块,但在执行模板时 Thymeleaf 认为是正常标记。

<span>hello!</span>
<!--/*/
  <div th:text="${...}">
    ...
  </div>
/*/-->
<span>goodbye!</span>

Thymeleaf 的解析系统将简单地删除<!--/*//*/-->标记,但不删除其内容,因此将取消注释。因此,在执行模板时,Thymeleaf 实际上会看到:

<span>hello!</span>
 
  <div th:text="${...}">
    ...
  </div>
 
<span>goodbye!</span>

5.1.9、内联

1、表达内联式

[[...]][(...)]之间的表达式被称为是 Thymeleaf 中的内联表达式,在其中我们可以使用任何在th:textth:utext属性中也有效的表达式。

虽然标准方言允许我们使用标记属性来完成几乎所有操作,但在某些情况下我们可能更喜欢将表达式直接编写到 HTML 文本中。例如,我们更喜欢写这个:

<p>Hello, [[${session.user.name}]]!</p>

而不是这个:

<p>Hello, <span th:text="${session.user.name}">Sebastian</span>!</p>

请注意,虽然[[...]]对应于th:text(i.e.结果将是 HTML 转义),但[(...)]对应于th:utext并且不会执行任何 HTML转义。所以对于一个变量如msg = 'This is <b>great!</b>',给定这个片段:

<p>The message is "[(${msg})]"</p>

结果将使那些<b>标签未转义,因此:

<p>The message is "This is <b>great!</b>"</p>

而如果像以下一样转义:

<p>The message is "[[${msg}]]"</p>

结果将是 HTML 转义后的效果:

<p>The message is "This is &lt;b&gt;great!&lt;/b&gt;"</p>

2、禁用内联

我们也可以禁用此机制,因为实际上可能存在我们想要输出[[...]][(...)] 内容文本而不将其内容作为表达式处理的情况。为此,我们将使用th:inline="none"

<p th:inline="none">A double array looks like this: [[1, 2, 3], [4, 5]]!</p>

这将输出:

<p>A double array looks like this: [[1, 2, 3], [4, 5]]!</p>

3、JavaScript 内联

JavaScript 内联允许在HTML模板模式下处理的模板中更好地整合 JavaScript <script>块。

与文本内联一样,这实际上相当于处理脚本内容,就好像它们是JAVASCRIPT模板模式中的模板一样,因此文本模板模式的所有功能都将在眼前。但是,在本节中,我们将重点介绍如何使用它将 Thymeleaf 表达式的输出添加到 JavaScript 块中。

必须使用th:inline="javascript"显式启用此模式:

<script th:inline="javascript">
    ...
    var username = [[${session.user.name}]];
    ...
</script>

这将导致:

<script th:inline="javascript">
    ...
    var username = "Sebastian \"Fruity\" Applejuice";
    ...
</script>

4、CSS 内联

Thymeleaf 还允许在 CSS <style>标签中使用内联,例如:

<style th:inline="css">
  ...
</style>

对于 example,假设我们将两个变量设置为两个不同的String值:

classname = 'main elems'
align = 'center'

我们可以像以下一样使用它们:

<style th:inline="css">
    .[[${classname}]] {
      text-align: [[${align}]];
    }
</style>

执行结果将是:

<style th:inline="css">
    .main\ elems {
      text-align: center;
    }
</style>

5.1.10、表达式基本对象 Expression Basic Objects

一些 objects 和变量 maps 始终可以调用。

1、基础 objects

  • #ctx:上下文对象 context object。是 org.thymeleaf.context.IContextorg.thymeleaf.context.IWebContext的实现。
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.context.IContext
 * ======================================================================
 */

${#ctx.locale}
${#ctx.variableNames}

/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.context.IWebContext
 * ======================================================================
 */

${#ctx.request}
${#ctx.response}
${#ctx.session}
${#ctx.servletContext}

注意#vars#root是同一 object 的同义词,但建议使用#ctx

  • # locale:直接访问与当前请求关联的java.util.Locale
${#locale}

2、命名空间 request/session 属性

在 web 环境中使用 Thymeleaf 时,我们可以使用一系列快捷方式来访问请求参数,session 属性和 application 属性:

请注意,这些不是 context objects,但 maps 作为变量添加到 context,因此我们在没有#的情况下访问它们。在某种程度上,它们充当命名空间。

  • param:用于检索请求参数。 ${param.foo}是带有foo请求参数值的String[],因此${param.foo[0]}通常用于获取第一个 value。
/*
 * ============================================================================
 * See javadoc API for class org.thymeleaf.context.WebRequestParamsVariablesMap
 * ============================================================================
 */

${param.foo}              // Retrieves a String[] with the values of request parameter 'foo'
${param.size()}
${param.isEmpty()}
${param.containsKey('foo')}
...
  • session:用于检索 session 属性。
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.context.WebSessionVariablesMap
 * ======================================================================
 */

${session.foo}                 // Retrieves the session atttribute 'foo'
${session.size()}
${session.isEmpty()}
${session.containsKey('foo')}
...
  • application:用于检索 application/servlet context 属性
/*
 * =============================================================================
 * See javadoc API for class org.thymeleaf.context.WebServletContextVariablesMap
 * =============================================================================
 */

${application.foo}              // Retrieves the ServletContext atttribute 'foo'
${application.size()}
${application.isEmpty()}
${application.containsKey('foo')}
...

3、Web上下文对象 Web context objects

在 web 环境中,还可以直接访问以下 objects(注意这些是 objects,而不是 maps/namespaces):

  • #request:直接访问与当前请求关联的javax.servlet.http.HttpServletRequest object。
${#request.getAttribute('foo')}
${#request.getParameter('foo')}
${#request.getContextPath()}
${#request.getRequestName()}
...
  • # session:直接访问与当前请求关联的javax.servlet.http.HttpSession object。
${#session.getAttribute('foo')}
${#session.id}
${#session.lastAccessedTime}
...
  • #servletContext:直接访问与当前请求关联的javax.servlet.ServletContext object。
${#servletContext.getAttribute('foo')}
${#servletContext.contextPath}
...

至此,我们关于 Thymeleaf 的入门介绍就结束了,更多的相关内容可以参考相关文档:

5.2、Thymeleaf 与 Spring Boot 集成

上一节我们快速介绍了 Thymeleaf 的一些语法和理论知识,这一节我们来进入实战,学习一下Thymeleaf 如何与 Spring Boot 集成。

新建一个项目 thymeleaf-in-action ,引入lombok 和 web 依赖:

thymeleaf-in-action

将之前 hello-world 项目中的 controller 拷贝过来,引入 thymeleaf 依赖:

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

完整 pom 文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springboot.blog</groupId>
    <artifactId>thymeleaf-in-action</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>thymeleaf-in-action</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

至此,我们的 thymeleaf 集成就完成了,启动项目在浏览器中访问 http://localhost:8080/hello 可以看到返回 Hello World!

5.3、Thymeleaf 实战

上一节我们在 Spring Boot 中成功集成了 Thymeleaf ,这一节我们继续在 thymeleaf-in-action 项目中来学习和完善。

首先我们修改 application.properties 文件:

# THYMELEAF
spring.thymeleaf.encoding=UTF-8
# 热部署静态文件
spring.thymeleaf.cache=false
# 使用HTML5标准,这个如果提示 warring 可以改用 HTML 标准
spring.thymeleaf.mode=HTML5

5.31、设计API

  • GET /users : 返回用于展现用户列表的 list.heml 页面
  • GET /users/{id} :返回用于展现用户信息的 view.html 页面
  • GET /users/form :返回用于新增或修改用户的 form.html 页面
  • POST /users:新增或修改用户,成功后重定向到 list.html 页面
  • GET /users/delete/{id} :根据 id 删除用户数据,成功后重定向到 list.html 页面
  • GET /users/modify/{id} :根据 id 获取用户数据,并返回 form.html 页面用来执行修改

此处只做演示,未使用遵循 rest 风格的接口

5.3.2、后台编码

  • 实体 User
  • 资源库 UserRepository :用户存储操作
  • 控制器 UserController :处理用户请求

1、在src\main\java\com\springboot\blog\domain\包下新建 User.java

1574295231366

User 代码如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    /** 用户的唯一标识 */
    private Long id;
    private String name;
    private Integer age;
}

2、在 src\main\java\com\springboot\blog\repository\ 包下新建 UserRepository.java接口:

public interface UserRepository {

    /**
     * 新增或者修改用户
     *
     * @param user
     * @return
     */
    User saveOrUpdateUser(User user);

    /**
     * 删除用户
     *
     * @param id
     */
    void deleteUser(Long id);

    /**
     * 根据用户id获取用户
     *
     * @param id
     * @return
     */
    User getUserById(Long id);

    /**
     * 获取所有用户的列表
     *
     * @return
     */
    List<User> listUser();
}

src\main\java\com\springboot\blog\repository\impl\ 包下新建 UserRepositoryImpl.java 类来实现 UserRepository 接口。

因为我们现在还没有使用数据库,所以我们可以先将用户的数据存储在一个 MAP 中,我们这里使用ConcurrentHashMap 来存储。

因为用户 Id 唯一,我们可以使用 AtomicLong 来每一次递增一个数生成用户 id。

@Repository // 别忘了注入bean
public class UserRepositoryImpl implements UserRepository {

    /**
     * 因为用户 Id 唯一,我们可以使用 AtomicLong 来每一次递增一个数生成用户 id。
     */
    private static AtomicLong counter = new AtomicLong();
    /**
     * 因为我们现在还没有使用数据库,所以我们可以先将用户的数据存储在一个 MAP中,我们这里使用ConcurrentHashMap来存储。
     */
    private final ConcurrentMap<Long, User> userMap = new ConcurrentHashMap<>();

    @Override
    public User saveOrUpdateUser(User user) {
        Long id = user.getId();
        // 新建
        if (id == null) {
            // id自增
            id = counter.incrementAndGet();
            user.setId(id);
        }
        this.userMap.put(id, user);
        return user;
    }

    @Override
    public void deleteUser(Long id) {
        this.userMap.remove(id);
    }

    @Override
    public User getUserById(Long id) {
        return this.userMap.get(id);
    }

    @Override
    public List<User> listUser() {
        return (ArrayList<User>) this.userMap.values();
    }
}

3、在 src\main\java\com\springboot\blog\controller\ 包下新建 UserController.java 类,并实现之前设计的API接口。

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    /**
     * 从 用户存储库 获取用户列表
     *
     * @return
     */
    private List<User> getUserList() {
        return userRepository.listUser();
    }

    /**
     * 查询所用用户
     *
     * @param model
     * @return
     */
    @GetMapping
    public ModelAndView list(Model model) {
        model.addAttribute("userList", this.getUserList());
        model.addAttribute("title", "用户管理");
        return new ModelAndView("users/list", "userModel", model);
    }

    /**
     * 根据id查询用户
     *
     * @param id
     * @param model
     * @return
     */
    @GetMapping("{id}")
    public ModelAndView view(@PathVariable("id") Long id, Model model) {
        User user = userRepository.getUserById(id);
        model.addAttribute("user", user);
        model.addAttribute("title", "查看用户");
        return new ModelAndView("users/view", "userModel", model);
    }

    /**
     * 获取 form 表单页面
     *
     * @param model
     * @return
     */
    @GetMapping("/form")
    public ModelAndView createForm(Model model) {
        model.addAttribute("user", new User());
        model.addAttribute("title", "创建用户");
        return new ModelAndView("users/form", "userModel", model);
    }

    /**
     * 新建用户
     *
     * @param user
     * @return
     */
    @PostMapping
    public ModelAndView create(User user) {
        user = userRepository.saveOrUpdateUser(user);
        return new ModelAndView("redirect:/users");
    }

    /**
     * 删除用户
     *
     * @param id
     * @param model
     * @return
     */
    @GetMapping(value = "delete/{id}")
    public ModelAndView delete(@PathVariable("id") Long id, Model model) {
        userRepository.deleteUser(id);
        model.addAttribute("userList", this.getUserList());
        model.addAttribute("title", "删除用户");
        return new ModelAndView("users/list", "userModel", model);
    }

    /**
     * 修改用户
     *
     * @param id
     * @param model
     * @return
     */
    @GetMapping(value = "modify/{id}")
    public ModelAndView modifyForm(@PathVariable("id") Long id, Model model) {
        User user = userRepository.getUserById(id);
        model.addAttribute("user", user);
        model.addAttribute("title", "修改用户");
        return new ModelAndView("users/form", "userModel", model);
    }

}

5.3.3、前端编码

  • list.html 展示用户列表

  • form.html 新增或修改用户资料

  • view.html 查看用户资料

  • header.html 公用的头部页面

  • footer.html 公用的底部页面

header 和 footer 我们使用 thymeleaf 中的 th:fragment 特性来完成

1、在 src\main\resources\templates 目录下中新建 fragments 目录,用来存在模板文件。

footer.html

footer.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf in action</title>
</head>
<body>
<div data-th-fragment="footer">
    <a href="https://blog.csdn.net/runewbie">Welcome to blog.csdn.net</a>
</div>
</body>
</html>

header.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf in action</title>
</head>
<body>
<div data-th-fragment="header">
    <h1>Thymeleaf in action</h1>
    <a href="/users">首页</a>
</div>
</body>
</html>

2、在 src\main\resources\templates 目录下中新建users 目录,存放业务相关的页面文件。

list.html:展示用户列表

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <title th:text="${userModel.title}">welcome</title>
</head>
<body>
<div th:replace="~{fragments/header :: header}">...</div>
<h3 th:text="${userModel.title}">Welcome to blog.csdn.net</h3>
<div>
    <a href="/users/form.html">创建用户</a>
</div>
<table border="1">
    <thead>
    <tr>
        <td>ID</td>
        <td>Age</td>
        <td>Name</td>
    </tr>
    </thead>
    <tbody>
    <tr th:if="${userModel.userList.size()} eq 0">
        <td colspan="3">没有用户信息!!</td>
    </tr>
    <tr th:each="user : ${userModel.userList}">
        <td th:text="${user.id}">1</td>
        <td th:text="${user.age}">11</td>
        <td><a href="view.html" th:href="@{'/users/' + ${user.id}}"
               th:text="${user.name}">waylau</a></td>
    </tr>
    </tbody>
</table>
<div th:replace="~{fragments/footer :: footer}">...</div>
</body>
</html>

form.html:提交用户表单

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <title th:text="${userModel.title}">users : View</title>
</head>
<body>
<div th:replace="~{fragments/header :: header}">...</div>
<h3 th:text="${userModel.title}">Welcome to blog.csdn.net</h3>
<div>
    <a href="/users">返回主页</a>
</div>
<form action="/users" method="POST" th:object="${userModel.user}">
    <input type="hidden" name="id" th:value="*{id}">
    名称:<br>
    <input type="text" name="name" th:value="*{name}">
    <br>
    年龄:<br>
    <input type="text" name="age" th:value="*{age}">
    <input type="submit" value="提交">
</form>
<div th:replace="~{fragments/footer :: footer}">...</div>
</body>
</html>

view.html:查看用户信息

<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <title th:text="${userModel.title}">users : View</title>
</head>
<body>
<div th:replace="~{fragments/header :: header}">...</div>
<h3 th:text="${userModel.title}">Welcome to blog.csdn.net</h3>
<div>
    <a href="/users">返回主页</a>
</div>
<div>
    <p><strong>ID:</strong><span id="id" th:text="${userModel.user.id}">123</span></p>
    <p><strong>Name:</strong><span id="name" th:text="${userModel.user.name}">waylau</span></p>
    <p><strong>Age:</strong><span id="age" th:text="${userModel.user.age}">30</span></p>
</div>

<div>
    <a th:href="@{'/users/delete/' + ${userModel.user.id}}">删除 </a>
    | <a th:href="@{'/users/modify/' + ${userModel.user.id}}">修改</a>
</div>
<div th:replace="~{fragments/footer :: footer}">...</div>
</body>
</html>

3、启动测试项目

在完成之前12的操作之后,我们可以启动项目来测试一下,启动后如果系统正常,可以看到下面的界面:

image-20191124220740131

我们点击创建用户新建几条数据:

image-20191124221202984

插入数据之后的效果,可以看到 ID 是自增的:

image-20191124221241097

点击某一条 name 可以数据的修改和删除操作:

image-20191124221442274

至此,我们关于 Thymeleaf 的入门实战就介绍完毕了。可能有人注意到了,我的页签带有一个自定义的图标:

image-20191124221639460

这个图标是怎么添加的呢?其实很简单,就是在 src\main\resources\static 目录下添加一个自己喜欢的 favicon.ico 文件即可。ico 格式文件可以照一张图片修改名称即可获取。然后重新启动服务就可以看到图标,如果看不到,是因为浏览器存在缓存,可以关闭浏览器重新打开对应页面即可。

以上就是我们的 Thymeleaf 学习,更对内容可以查看源代码获取。

源代码

thymeleaf-in-action

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值