《Spring 实战》笔记6:渲染 Web 视图

理解视图解析

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

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

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

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

View 接口的任务就是接受模型以及 Servletrequestresponse 对象,并将输出结果渲染到 response 中。

Spring 自带了 13 个视图解析器,能够将逻辑视图名转换为物理实现

  • 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 转换后的结果

创建 JSP 视图

Spring 提供了两种支持 JSP 视图的方式:

  • InternalResourceViewResolver 会将视图名解析为 JSP 文件。另外,如果在你的 JSP 页面中使用了 JSP 标准标签库(JavaServer Pages Standard Tag Library,JSTL)的话,InternalResourceViewResolver 能够将视图名解析为 JstlView 形式的 JSP 文件,从而将 JSTL 本地化和资源 bundle 变量暴露给 JSTL 的格式化(formatting)和信息(message)标签。
  • Spring 提供了两个 JSP 标签库,一个用于表单到模型的绑定,另一个提供了通用的工具类特性。

配置适用于 JSP 的视图解析器

InternalResourceViewResolver 遵循一种约定,会在视图名上添加前缀和后缀,进而确定一个 Web 应用中视图资源的物理路径。

@Bean
public ViewResolver viewResolver() {
  InternalResourceVIewResolver resolver = new InternalResourceVIewResolver();
  resolver.setPrefix("WEB-INF/view");
  resolver.setSuffix(".jsp")
  return resolver;
}

作为替代方法,如果你更喜欢基于 XML 的 Spring 配置,那么可以按照如下方式配置 InternalResourceVIewResolver

<bean id="viewResolver"
  class="org.springframework.web.servlet.view.InternalResourceVIewResolver"
  p:prefix="/WEB-INF/view"
  p:suffix=".jsp"

JSTL 的格式化标签需要一个 Locale 对象,以便于恰当地格式化地域相关的值,如日期和货币。信息标签可以借助 Spring 的信息资源和 Locale,从而选择适当的信息渲染到 HTML 之中。通过解析 JstlView,JSTL 能够获得 Locale 对象以及 Spring 中配置的信息资源。

如果想让 InternalResourceViewResolver 将视图解析为 JstlView,而不是 InternalResourceView 的话,那么我们只需设置它的 viewClass 属性即可:

@Bean
public ViewResolver viewResolver() {
  InternalResourceVIewResolver resolver = new InternalResourceVIewResolver();
  resolver.setPrefix("WEB-INF/view");
  resolver.setSuffix(".jsp")
  resolver.setViewClass("org.springframework.web.servlet.view.JstlView.class")
  return resolver;
}

同样,我们也可以在 XMl 完成这一任务

<bean id="viewResolver"
  class="org.springframework.web.servlet.view.InternalResourceVIewResolver"
  p:prefix="/WEB-INF/view"
  p:suffix=".jsp"
  p:viewClass="org.springframework.web.servlet.view.JstlView.class"

使用 Spring 的 JSP 库

用处不多。

将表单绑定到模型上

需要在JSP页面中对其进行声明

<%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %>
  • <sf:checkbox>
  • <sf:checkboxes>
  • <sf:errors>
  • <sf:form>
  • <sf:hidden>
  • <sf:input>
  • <sf:lable>
  • <sf:option>
  • <sf:password>
  • <sf:radiobutton>
  • <sf:radiobuttons>
  • <sf:select>
  • <sf:textarea>
  • <sf:radilbutton>
<sf:form method="POST" comandName="spitter">
  First Name: <sf:input path="firstName" /><br/>
  Last Name: <sf:input path="lastName" /><br/>
  Email: <sf:input path="email" /><br/>
  Username: <sf:input path="username" /><br/>
  Password: <sf:password path="password" /><br/>
  <sf:input type="submit" value="Register" />
</sf:form>

<sf:form> 会渲染一个 HTMl <form> 标签,但它也会通过 commandName 属性构建针对某个模型对象的上下文信息,在其他的表单绑定标签中,会引用这个模型对象的属性

我们将 commandName 属性设置为spitter。因此,在模型中必须要有一个 key 为 spitter 的对象,否则的话,表单不能正常渲染(会出现 JSP 错误)

从 Spring3.1 开始 <sf:input> 标签能够允许我们指定 type 属性,这样的话,除了其他可选的类型外,还能指定 HTML5 特定类型的文本域,如 date、range、email。我们可以按照如下方式指定 email 域:

Email: <sf:input path="email" type="email"/> <br/>

使用 <sf:errors>

<sf:form method="POST" commandName="spitter" >
    <sf:input path="firstName" /><br/>
    <sf:errors path="firstName" cssErrorClass="error" />
    ......
</sf:form>
@NotNull
@Size(min=5, max=16, message="{username.size}")
private String username;
.......
@NotNull
@Size(min=2, max=30, message="{lastName.size}")
private String lastName;

我们将其 @Size 注解的 message 设置为一个字符串,用大括号括起来的。没有没有大括号的话,message 中的值将会作为展现给用户的错误信息,使用了就用文件中某个一个属性,该属性包含了实际的信息。

创建一个 ValidationMessages.properties 的文件,并将其放到根路径之下。

firstName.size=First name must be between {min} and {max} characters long.
lastName.size=Last name must be between {min} and {max} characters long.

Spring 通用的标签库

需要在页面首页对其进行声明

<%@ taglib uri="http://www.springframework.org.tags" prefix='s'%>
  • <s:escapeBody> 将标签体中的内容进行 HTML 和 / 或 JavaScript 转义,包裹要转义内容,标签属性加上 htmlEscape="true" javaScriptEscape="true
  • <s:hasBindErrors> 根据指定模型对象(在请求属性中)是否有绑定错误,有条件地渲染内容
  • <s:htmlEscape> 为当前页面设置默认的 HTML 转义值
  • <s:message> 根据给定的编码获取信息,然后要么进行渲染(默认行为),要么将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用 var 和 scope 属性实现)
  • <s:theme> 根据给定的编码获取主题信息,然后要么进行渲染(默认行为),要么将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用 var 和 scope 属性实现)
  • <s:transform> 使用命令对象的属性编辑器转换命令对象中不包含的属性
  • <s:url> 创建相对于上下文的 URL,支持 URI 模板变量以及 HTML/XML/JavaScript 转义。可以渲染 URL(默认行为),也可以将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用 var 和 scope 属性实现)
  • <s:eval> 计算符合 Spring 表达式语言(Spring Expression Language,SpEL)语法的某个表达式的值,然后要么进行渲染(默认行为),要么将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用 var 和 scope 属性实现)

使用 Apache Tiles 视图定义布局

使用 Thymeleaf

Thymeleaf 模板是原生的,不依赖于标签库。它能在接受原始 HTML 的地方进行编辑和渲染。因为它没有与 Servlet 规范耦合,因此 Thymeleaf 模板能够进入 JSP 所无法涉足的领域。

配置 Thymeleaf 视图解析器

  • ThymeleafViewResolver:将逻辑视图名称解析为 Thymeleaf 模板视图;
  • SpringTemplateEngine:处理模板并渲染结果;
  • TemplateResolver:加载 Thymeleaf 模板。
// 声明这些 bean 的 Java 配置
@Bean
public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
    // Thymeleaf 视图解析器
    ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
    viewResolver.setTemplateEngine(templateEngine);
    return viewResolver;
}

@Bean
public TemplateEngine templateEngine(TemplateResolver templateResolver) {
    // 模板引擎
    SpringTemplateEngine templateEngine = new SpringTemplateEngine();
    templateEngine.setTemplateResolver(templateResolver);
    return templateEngine;
}

@Bean
public TemplateResolver templateResolver() {  // 模板解析器
    TemplateResolver templateResolver = new ServletContextTemplateResolver();
    templateResolver.setPrefix("/WEB-INF/templates");
    templateResolver.setSuffix(".html");
    templateResolver.setTemplateMode("THML5");
    return templateResolver;
}
<!-- 使用 XML 的方式,配置 Spring 对 Thymeleaf 的支持 -->
<bean id="viewResolver" class="org.thymeleaf.spring4.view.ThymeleafViewResolver"
        p:templateEngine-ref="templateEngine" />
<bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine"
        P:templateResolver-ref="templateResolver" />
<bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver"
        p:prefix="WEB-INF/templates/"
        p:suffix=".html"
        p:templateMode="HTML5" /> <!-- 预期要解析的模板会渲染成 HTML5 输出 -->

定义 Thymeleaf 模板

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

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">  <!--声明 Thymeleaf 命名空间-->
  <head>
    <title>Spitter</title>
    <link rel="stylesheet"
          type="text/css"
          th:href="@{/resources/style.css}"></link>  <!--到样式表的 th:href 链接-->
  </head>
  <body>
      <h1>Welcome to Spitter</h1>
      <a th:href="@{/spittles}">Spittles</a> |      <!--到页面的 th:herf 链接-->
      <a th:href="@{/spitter/register}">Register</a>
  </body>
</html>

使用了 th:href 属性,都用到了 @{} 表达式,用来计算相对于 URL 的路径

借助 Thymeleaf 实现表单绑定

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

th:class 属性会渲染为一个 class 属性,它的值是根据给定的表达式计算得到的。在上面的这两个 th:class 属性中,它会直接检查 firstName 域有没有校验错误。如果有的话,class 属性在渲染时的值为 error。如果这个域没有错误的话,将不会渲染 class 属性。

<input> 标签使用了 th:field 属性,用来引用后端对象的 firstName 域。这可能与你的预期有点差别。在 Thymeleaf 模板中,我们在很多情况下所使用的属性都对应于标准的 HTML 属性,因此貌似使用 th:value 属性来设置 <input> 标签的 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 属性和 *{} 表达式,为所有的表单域绑定后端对象。

<div> 元素使用 th:if 属性来检查是否有校验错误。如果有的话,会渲染 <div>,否则的话,它将不会渲染。

<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 属性。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Spring 实战》笔记6:渲染 Web 视图

文章字数:3.5k

本文作者:Bin

发布时间:2019-08-06, 20:31:36

最后更新:2019-12-11, 22:06:46

原始链接:http://coolview.github.io/2019/08/06/Spring/%E3%80%8ASpring%20%E5%AE%9E%E6%88%98%E3%80%8B%E7%AC%94%E8%AE%B06%EF%BC%9A%E6%B8%B2%E6%9F%93%20Web%20%E8%A7%86%E5%9B%BE/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录