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。毕竟自己做一整个前端还是有点费时费力的。