6.4.2 定义 Thymeleaf 模板

Thymeleaf 在很大程度上就是 HTML 文件,与 JSP不同,它没有什么特 殊的标签或标签库。Thymeleaf 之所以能够发挥作用,是因为它通过自定义的命名空间,为标准的 HTML 标签集合添加 Thymeleaf 属性。如下的程序清单展现了 home.html,也就是使用 Thymeleaf 命名空间的首页模板。

程序清单 6.6 home.html:使用 Thymeleaf 命名空间的首页模板引擎
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>Spitter</title>
    <link rel="stylesheet" 
          type="text/css" 
          th:href="@{/resources/style.css}"></link>
  </head>
  <body>
    <div id="content">
      <h1>Welcome to Spitter</h1>
  
      <a th:href="@{/spittles}">Spittles</a> | 
      <a th:href="@{/spitter/register}">Register</a>
  </body>
</html>

首页模板相对来讲很简单,只使用了 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 提供了与之相匹敌的功能。

借助 Thymeleaf 实现表单绑定

表单绑定是 Spring MVC 的一项重要特性。它能够将表单提交的数据填充到命令对象中,并将其传递给控制器,而在展现表单的时候,表单中也会填充命令对象中的值。如果没有表单绑定功能的话,我们需要确保 HTML 表单域要映射后端命令对象中的属性,并且在校验失败后展现表单的时候,还要负责确保输入域中值要设置为命令对象的属性。

但是,如果有表单绑定的话,它就会负责这些事情了。为了复习一下表单绑定是如何运行的,下面展现了在 registration.jsp 中的 First Name 输入域:

<sf:label path="firstName" cssErrorClass="error">First Name</sf:label>
<sf:input path="firstName" cassErrorClass="error" /><br/> 

在这里,调用了 Spring 表单绑定标签库的 <sf:input> 标签,它会渲染出一个 HTML input 标签,并且其 value 属性设置为后端对象 firstName 属性的值。它还使用了 Spring 的 <sf:label> 标签及其 cssErrorClass 属性,如果出现校验错误的话,会将文本标记渲染为红色。

但是,我们本节讨论的并不是 JSP,而是使用 Thymeleaf 替换 JSP。因此,我们不能使用 Spring 的 JSP 标签实现表单绑定,而是使用 Thymeleaf 的 Spring 方言。

作为阐述的样例,请参考如下的 Thymeleaf 模板片段,它会渲染 First Name 输入域:

<label th:class="${#fields.hasErrors['firstName']}?'error'">First Name</label>
<input type="text" th:field="*{firstName}" th:class="${#fields.hasErrors['firstName']}?'error'" /><br/>

在这里,我们不再使用 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 将一个表单绑定到命令对象上
<form method="POST" th:object="${spitter}">
  <div class="errors" th:if="${#fields.hasErrors('*')}">
    <ul>
      <li th:each="err : ${#fields.errors('*')}" 
          th:text="${err}">Input is incorrect
      </li>
    </ul>
  </div>
  <label th:class="${#fields.hasErrors('firstName')}? 'error'">First Name</label>: 
  <input type="text" th:field="*{firstName}"  th:class="${#fields.hasErrors('firstName')}? 'error'" /><br/>
  
  <label th:class="${#fields.hasErrors('lastName')}? 'error'">Last Name</label>: 
  <input type="text" th:field="*{lastName}" th:class="${#fields.hasErrors('lastName')}? 'error'" /><br/>
  
  <label th:class="${#fields.hasErrors('email')}? 'error'">Email</label>: 
  <input type="text" th:field="*{email}" th:class="${#fields.hasErrors('email')}? 'error'" /><br/>
  
  <label th:class="${#fields.hasErrors('username')}? 'error'">Username</label>: 
  <input type="text" th:field="*{username}" th:class="${#fields.hasErrors('username')}? 'error'" /><br/>
  
  <label th:class="${#fields.hasErrors('password')}? 'error'">Password</label>: 
  <input type="password" th:field="*{password}" th:class="${#fields.hasErrors('password')}? 'error'" /><br/>

  <input type="submit" value="Register" />
</form>

程序清单 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 updated