1 理解视图解析

将控制器中请求处理的逻辑和视图中的渲染实现解耦是 Spring MVC 的一个重要特性。如果控制器中的方法直接负责产生 HTML 的话,就很难在不影响请求处理逻辑的前提下,维护和更新视图。

如果控制器只通过逻辑视图名来了解视图的话,那 Spring 该如何确定使用哪一个视图实现来渲染模型呢?这就是 Spring 视图解析器的任务了。

Spring MVC 定义了一个名为 ViewResolver 的接口,它大致如下所示:

public interface ViewResolver {
  View resolverViewName(String viewName, Locale locale) throws Exception;
}

当给 resolveViewName () 方法传入一个视图名和 Locale 对象时,它会返回一个 View 实例。View 是另外一个接口,如下所示:

public interface View {
  String getContentType();
  void render(Map<String, ?> model, HttpServletRequest request, HttpServlectResponse response) throws Exception;
}

View 接口的任务就是接受模型以及 Servlet 的 request 和 response 对象,并将输出结果渲染到 response 中。Spring 提供了多个内置的实现,如表 6.1 所示,它们能够适应大多数的场景。

视图解析器描述
BeanNameViewResolver将视图解析为 Spring 应用上下文中的 bean,其中 bean 的 ID 与视图的名字相同
ContentNegotiatingViewResolver通过考虑客户端需要的内容类型来解析视图,委托给另外一个能够产生对应内容类型的视图解析器
FreeMarkerViewResolver将视图解析为 FreeMarker 模板
InternalResourceViewResolver将视图解析为 Web 应用的内部资源(一般为 JSP)
JasperReportsViewResolver将视图解析为 JasperReports 定义
ResourceBundleViewResolver将视图解析为资源 bundle(一般为属性文件)
TilesViewResolver将视图解析为 Apache Tile 定义,其中 tile ID 与视图名称相同。注意有两个不同的 TilesViewResolver 实现,分别对应于 Tiles 2.0 和 Tiles 3.0
UrlBasedViewResolver直接根据视图的名称解析视图,视图的名称会匹配一个物理视图的定义
VelocityLayoutViewResolver将视图解析为 Velocity 布局,从不同的 Velocity 模板中组合页面
VelocityViewResolver将视图解析为 Velocity 模板
XmlViewResolver将视图解析为特定 XML 文件中的 bean 定义。类似于 BeanNameViewResolver
XsltViewResolver将视图解析为 XSLT 转换后的结果

因为大多数 Java Web 应用都会用到 JSP,我们首先将会介绍 InternalResource-ViewResolver,这个视图解析器一般会用来解析 JSP 视图。

2 创建 JSP 视图

3 控制 JSP 布局

4 创建 Thymeleaf 视图

4.1 配置 Thymeleaf 视图解析器

JSP 并不是真正的 HTML,并且与 Servlet 耦合严重,只能用于基于 Servlet 的 Web 应用中。

需要配置三个启用 Thymeleaf 与 Spring 集成的 bean:

  • ThymeleafViewResolver:将逻辑视图名称解析为 Thymeleaf 模板视图;
  • SpringTemplateEngine:处理模板并渲染结果;
  • TemplateResolver:加载 Thymeleaf 模板。
@Bean
public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
  ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
  viewResolver.setTemplateEngine(templateEngine);
  return viewResolver;
}
 
@Bean
public SpringTemplateEngine templateEngine(TemplateResolver templateResolver) {
  SpringTemplateEngine templateEngine = new SpringTemplateEngine();
  templateEngine.setTemplateResolver(templateResolver);
  return templateEngine;
}
 
@Bean
public TemplateResolver templateResolver() {
  TemplateResolver templateResolver = new ServletContextTemplateResolver();
  templateResolver.setPrefix("/WEB-INF/views/");
  templateResolver.setSuffix(".html");
  templateResolver.setTemplateMode("HTML5");
  return templateResolver;
}
<bean id="viewResolver" class="org.thymeleaf.spring3.view.ThymeleafViewResolver"
      p:templateEngine-ref="templateEngine" />
 
<bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine"
      p:templateResolver-ref="templateResolver" />
 
 
<bean id="templateResolver" class="org.thymeleaf.templateResolver.ServletContextTemplateResolver"
      p:prefix="/WEB-INF/templates/"
      p:suffix=".html"
      p:templateMode="HTML5" />

ThymeleafViewResolver 是 Spring MVC 中 ViewResolver 的一个实现类。像其他的视图解析器一样,它会接受一个逻辑视图名称,并将其解析为视图。不过在该场景下,视图会是一个 Thymeleaf 模板。

需要注意的是 ThymeleafViewResolver bean 中注入了一个对 Spring-TemplateEngine bean 的引用。SpringTemplateEngine 会在 Spring 中启用 Thymeleaf 引擎,用来解析模板,并基于这些模板渲染结果。可以看到,我们为其注入了一个 TemplateResolver bean 的引用。

TemplateResolver 会最终定位和查找模板。与之前配置 InternalResource-ViewResolver 类似,它使用了 prefix 和 suffix 属性。前缀和后缀将会与逻辑视图名组合使用,进而定位 Thymeleaf 引擎。它的 templateMode 属性被设置成了 HTML 5,这表明我们预期要解析的模板会渲染成 HTML 5 输出。

4.2 定义 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 属性的特殊之处在于它的值中可以包含 Thymeleaf 表达式,用来计算动态的值。它会渲染成一个标准的 href 属性,其中会包含在渲染时动态创建得到的值。

4.3 使用 Thymeleaf 完成表单绑定

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

在上面的这两个 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。

<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>

用了相同的 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 属性。

日后补充一下 Thymeleaf 的属性。

看情况,如果后续使用 ant design,似乎甚至不需要学习 Thymeleaf。毕竟自己做一整个前端还是有点费时费力的。