6.4.2 定义 Thymeleaf 模板
Thymeleaf 在很大程度上就是 HTML 文件,与 JSP不同,它没有什么特 殊的标签或标签库。Thymeleaf 之所以能够发挥作用,是因为它通过自定义的命名空间,为标准的 HTML 标签集合添加 Thymeleaf 属性。如下的程序清单展现了 home.html,也就是使用 Thymeleaf 命名空间的首页模板。
程序清单 6.6 home.html:使用 Thymeleaf 命名空间的首页模板引擎
1
<html xmlns="http://www.w3.org/1999/xhtml"
2
xmlns:th="http://www.thymeleaf.org">
3
<head>
4
<title>Spitter</title>
5
<link rel="stylesheet"
6
type="text/css"
7
th:href="@{/resources/style.css}"></link>
8
</head>
9
<body>
10
<div id="content">
11
<h1>Welcome to Spitter</h1>
12
13
<a th:href="@{/spittles}">Spittles</a> |
14
<a th:href="@{/spitter/register}">Register</a>
15
</body>
16
</html>
Copied!
首页模板相对来讲很简单,只使用了 th:href 属性。这个属性与对应的原生 HTML 属性很类似,也就是 href 属性,并且可以按照相同的方式来使用。th:href 属性的特殊之处在于它的值中可以包含 Thymeleaf 表达式,用来计算动态的值。它会渲染成一个标准的 href 属性,其中会包含在渲染时动态创建得到的值。这是 Thymeleaf 命名空间中很多属性的运行方式:它们对应标准的 HTML 属性,并且具有相同的名称,但是会渲染一些计算后得到的值。在本例中,使用 th:href 属性的三个地方都用到了 @{} 表达式,用来计算相对于 URL 的路径(就像在 JSP 页面中,我们可能会使用的 JSTL <c:url> 标签或 Spring 标签类似)。
尽管 home.html 是一个相当简单的 Thymeleaf 模板,但是它依然很有价值,这在于它与纯 HTML 模板非常接近。唯一的区别之处在于 th:href 属性,否则的话,它就是基础且功能丰富的 HTML 文件。
这意味着 Thymeleaf 模板与 JSP 不同,它能够按照原始的方式进行编辑甚至渲染,而不必经过任何类型的处理器。当然,我们需要 Thymeleaf 来处理模板并渲染得到最终期望的输出。即便如此,如果没有任何特殊的处理,home.html 也能够加载到 Web 浏览器中,并且看上去与完整渲染的效果很类似。为了更加清晰地阐述这一点,图 6.6 对比了 home.jsp(上方)和 home.html(下方)在 Web 浏览器中的显示效果。
可以看到,在 Web 浏览器中,JSP 模板的渲染效果很糟糕。尽管我们可以看到一些熟悉的元素,但是 JSP 标签库的声明也显示了出来。在链接前出现了一些令人费解的未闭合标记,这是 Web 浏览器没有正常解析 <s:url> 标签的结果。
与之相反,Thymeleaf 模板的渲染效果基本上没有任何错误。稍微有点问题的是链接部分,Web 浏览器并不会像处理 href 属性那样处理 th:href,所以链接并没有渲染为链接的样子。除了这些细微的问题,模板的渲染效果与我们的预期完全符合。
像 home.jsp 这样的模板作为 Thymeleaf 入门是很合适的。但是 Spring 的 JSP 标签所擅长的是表单绑定。如果我们抛弃 JSP 的话,那是不是也要抛弃表单绑定呢?不必担心。Thymeleaf 提供了与之相匹敌的功能。
图 6.6 Thymeleaf 模板与 JSP 不同,它是 HTML,可以像 HTML 那样进行渲染和编辑
借助 Thymeleaf 实现表单绑定
表单绑定是 Spring MVC 的一项重要特性。它能够将表单提交的数据填充到命令对象中,并将其传递给控制器,而在展现表单的时候,表单中也会填充命令对象中的值。如果没有表单绑定功能的话,我们需要确保 HTML 表单域要映射后端命令对象中的属性,并且在校验失败后展现表单的时候,还要负责确保输入域中值要设置为命令对象的属性。
但是,如果有表单绑定的话,它就会负责这些事情了。为了复习一下表单绑定是如何运行的,下面展现了在 registration.jsp 中的 First Name 输入域:
1
<sf:label path="firstName" cssErrorClass="error">First Name</sf:label>
2
<sf:input path="firstName" cassErrorClass="error" /><br/>
Copied!
在这里,调用了 Spring 表单绑定标签库的 <sf:input> 标签,它会渲染出一个 HTML input 标签,并且其 value 属性设置为后端对象 firstName 属性的值。它还使用了 Spring 的 <sf:label> 标签及其 cssErrorClass 属性,如果出现校验错误的话,会将文本标记渲染为红色。
但是,我们本节讨论的并不是 JSP,而是使用 Thymeleaf 替换 JSP。因此,我们不能使用 Spring 的 JSP 标签实现表单绑定,而是使用 Thymeleaf 的 Spring 方言。
作为阐述的样例,请参考如下的 Thymeleaf 模板片段,它会渲染 First Name 输入域:
1
<label th:class="${#fields.hasErrors['firstName']}?'error'">First Name</label>
2
<input type="text" th:field="*{firstName}" th:class="${#fields.hasErrors['firstName']}?'error'" /><br/>
Copied!
在这里,我们不再使用 Spring JSP 标签中的 cssClassName 属性,而是在标准的 HTML 标签上使用 th:class 属性。th:class 属性会渲染为一个 class 属性,它的值是根据给定的表达式计算得到的。在上面的这两个 th:class 属性中,它会直接检查 firstName 域有没有校验错误。如果有的话,class 属性在渲染时的值为 error。如果这个域没有错误的话,将不会渲染 class 属性。
<input> 标签使用了 th:field 属性,用来引用后端对象的 firstName 域。这可能与你的预期有点差别。在 Thymeleaf 模板中, 我们在很多情况下所使用的属性都对应于标准的 HTML 属性,因此貌似使用 th:value 属性来设置标签的 value 属性才是合理的。
其实不然,因为我们是在将这个输入域绑定到后端对象的 firstName 属性上,因此使用 th:field 属性引用 firstName 域。通过使用 th:field,我们将 value 属性设置为 firstName 的值, 同时还会将 name 属性设置为 firstName。
为了阐述 Thymeleaf 是如何实际运行的,如下的程序清单展示了完整的注册表单模板。
程序清单 6.7 注册页面,使用 Thymeleaf 将一个表单绑定到命令对象上
1
<form method="POST" th:object="${spitter}">
2
<div class="errors" th:if="${#fields.hasErrors('*')}">
3
<ul>
4
<li th:each="err : ${#fields.errors('*')}"
5
th:text="${err}">Input is incorrect
6
</li>
7
</ul>
8
</div>
9
<label th:class="${#fields.hasErrors('firstName')}? 'error'">First Name</label>:
10
<input type="text" th:field="*{firstName}" th:class="${#fields.hasErrors('firstName')}? 'error'" /><br/>
11
12
<label th:class="${#fields.hasErrors('lastName')}? 'error'">Last Name</label>:
13
<input type="text" th:field="*{lastName}" th:class="${#fields.hasErrors('lastName')}? 'error'" /><br/>
14
15
<label th:class="${#fields.hasErrors('email')}? 'error'">Email</label>:
16
<input type="text" th:field="*{email}" th:class="${#fields.hasErrors('email')}? 'error'" /><br/>
17
18
<label th:class="${#fields.hasErrors('username')}? 'error'">Username</label>:
19
<input type="text" th:field="*{username}" th:class="${#fields.hasErrors('username')}? 'error'" /><br/>
20
21
<label th:class="${#fields.hasErrors('password')}? 'error'">Password</label>:
22
<input type="password" th:field="*{password}" th:class="${#fields.hasErrors('password')}? 'error'" /><br/>
23
24
<input type="submit" value="Register" />
25
</form>
Copied!
程序清单 6.7 使用了相同的 Thymeleaf 属性和 *{} 表达式,为所有的表单域绑定后端对象。这其实重复了我们在 First Name 域中所做的事情。
但是,需要注意我们在表单的顶部了也使用了 Thymeleaf,它会用来渲染所有的错误。元素使用 th:if 属性来检查是否有校验错误。如果有的话,会渲染,否则的话,它将不会渲染。
在 <div> 中,会使用一个无顺序的列表来展现每项错误。<li> 标签上的 th:each 属性将会通知 Thymeleaf 为每项错误都渲染一个 <li>,在每次迭代中会将当前错误设置到一个名为 err 的变量中。
<li> 标签还有一个 th:text 属性。这个命令会通知 Thymeleaf 计算某一个表达式(在本例中,也就是 err 变量)并将它的值渲染为 <li> 标签的内容体。实际上的效果就是每项错误对应一个 <li> 元素,并展现错误的文本。
你可能会想知道 ${}*{} 括起来的表达式到底有什么区别。${} 表达式(如 ${spitter})是变量表达式(variable expression)。一般来讲,它们会是对象图导航语言(Object-Graph Navigation Language,OGNL)表达式 。但在使用 Spring 的时候,它们是 SpEL 表达式。在 ${spitter} 这个例子中,它会解析为 key 为 spitter 的 model 属性。
而对于 *{} 表达式,它们是选择表达式(selection expression)。变量表达式是基于整个 SpEL 上下文计算的,而选择表达式是基于某一个选中对象计算的。在本例的表单中,选中对象就是 <form> 标签中 th:object 属性所设置的对象:模型中的 Spitter 对象。因此,*{firstName} 表达式就会计算为 Spitter 对象的 firstName 属性。
Last modified 2yr ago
Copy link