在狗厂,我所接触的项目里,Spring 的视图解析器采用最广泛的就是 Velocity。最近也一直在想前后端分离的事,略显古老的 Velocity 并不是前后端分离的好选择。还好,近几年 Java Web 诞生了一款新的视图解析器——“百里香叶” Thymeleaf,就像它的名字一样美妙。
和 Velocity 类似,Thymeleaf 支持通过 @Controller
注解的映射方法返回模板名称;模板支持 Spring Expression Language;支持在模板中创建表单,表单验证。(这就比较像 Jinja2 了)。
模板标准方言
引入
Thymeleaf 的模板标准语言中绝大多数 processors 都是 attribute processors,这就意味着浏览器可以正常地表现 XHTML/HTML5 模板文件,即使是在模板引擎没有加载的情况下,因为浏览器会忽略额外的 attribute。这就是 Thymeleaf 比前辈 JSP 厉害的地方之一。来看下面的 input 标签,JSP 里会加入浏览器无法直接识别的代码:
1 |
<form:inputText name="userName" value="${user.name}" /> |
而 Thymeleaf 模板标准语言会这样写:
1 |
<input type="text" name="userName" value="James Carrot" th:value="${user.name}" /> |
浏览器能直接识别上述 Thymeleaf 的 input 标签,而且还能在加载模板引擎后,由后端返回的数据渲染 value 值。也就是这一特性,可以让前后端工程师在同一个模板文件上协作开发,避免了从静态页面到模板页面的转换,前后端并行开发,这就是未来的趋势,也被称作 Natural Templating,页面即模板,模板即页面。
标准表达式语法
基本表达式
Thymeleaf 模板方言里最重要的就是它的标准表达式语法了。Thymeleaf 的表达式有:
简单表达式:
- 变量表达式:
${...}
- 选择变量表达式:
*{...}
- 消息表达式:
#{...}
- URL 表达式:
@{...}
- 变量表达式:
字面值表达式:
- 文本:’ABC’, ‘你好’
- 数字:0, 1, 2.0, 12.3
- 布尔值:
true
,false
- Null:
null
- 字面值 token:
one
,sometext
,main
算术操作:
- 二元:
+
,-
,*
,/
,%
- 一元:
-
- 二元:
布尔运算符:
- 二元:
and
,or
- 一元:
!
,not
- 二元:
比较运算符:
- 不等比较:
>
,<
,>=
,<=
(gt
,lt
,ge
,le
) - 相等比较:
==
,!=
(eq
,ne
)
- 不等比较:
条件运算符:
- If-then:
(if) ? (then)
- If-then-else:
(if) ? (then) : (else)
- Default:
(value) ?: (default_value)
- If-then:
参考如下模板代码:
1 |
'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown')) |
变量
变量表达式 ${...}
会把模板 context 内保存的变量解释出来。例如下面的表达式中
1 |
<p>Today is: <span th:text="${today}">13 August 2016</span>.</p> |
实际上是执行了这样的代码:
1 |
ctx.getVariables().get("today"); |
复合结构的变量:
1 |
<p th:utext="#{home.welcome(${session.user.name})}"> |
变量执行的代码是
1 |
((User) ctx.getVariables().get("session").get("user")).getName(); |
getter
方法只是其中一个功能,变量表达式就像 Python 一样极富表现力:
1 |
|
另外,*{...}
也是取变量的表达式,两者的区别是:*{...}
会从选定的对象中匹配,而 ${...}
是从整个 context 中去匹配。当未指定对象的前提下,二者的作用是一样的。参考下面的代码:
1 |
<div th:object="${session.user}"> |
就等同于
1 |
<div> |
URL
URL 是变量之外的另一个要点,由 @{...}
表达式解释,通过标签 th:href
或 th:src
指定。参见下面的示例代码:
1 |
<!-- 绝对路径 --> |
上述表达式中,orderId=${o.id} 是作为 URL 的参数,多个参数也是类似如此。
字面值
字面值很简单,就是做字面内容的替换,包括文本、数字、布尔值、空值 null。
1 |
<!-- 文本 --> |
字符串替换
有时候需要动态的改变文本的某一段内容,这就需要用到拼接和替换:
1 |
<span th:text="'The name of the user is ' + ${user.name}"> |
运算 / 比较操作符
基础的运算符包括加减乘除取余,比较操作符中由于存在 >
和 <
,所以需要转义处理:
1 |
<div th:with="isEven=(${prodStat.count} % 2 == 0)"> |
属性标签
前面提到了一些不属于 HTML 规范的属性标签,这都是 Thymeleaf 自定义的,通过这些属性标签来设置 HTML 标签的属性。
设置任意属性
Thymeleaf 的 th:attr
属性标签可以对 HTML 中任意属性值进行设置。例如 th:attr="action=abc"
是对 action
属性的设置;th:attr="value=xyz
是对 value
属性的设置。示例代码如下:
1 |
<form action="subscribe.html" th:attr="action=@{/subscribe}"> |
设置指定属性
除了上面的通用型属性标签外,Thymeleaf 还自定义了其他的特定属性标签。如 th:attr
标签指定 attr
属性;th:value
指定 value
标签。
1 |
<input type="submit" value="Subscribe me!" th:value="#{subscribe.submit}"/> |
还有许多其他的 th:*
自定义标签,都是和 HTML 标签一一对应,这里不逐个例举了。
同时设置多个属性
如果需要 Thymeleaf 动态设置多个属性,可以像上面一样依次指定,也可以同时指定,不同属性通过 -
连接:th:alt-title
同时设置 alt
和 title
属性; th:lang-xmllang
同时设置 lang
和 xml:lang
属性。
1 |
<!-- 依次指定 --> |
前置和后置
有些情况下,可能需要改变属性的一部分值,比如 DOM 节点样式属性中的某个样式,这就需要在已有属性的基础上前置或后置新的属性。对此,Thymeleaf 提供了 th:attrappend
和 th:attrprepend
属性标签。
1 |
<input type="button" value="Do it!" class="btn" th:attrappend="class=${' ' + cssStyle}" /> |
迭代
前端页面里经常会用到表格,像 Bootstrap 中就专门有 DataTable 来处理表格的渲染。Thymeleaf 的 th:each
属性标签实现了对表格 DOM 下各个 <tr>
元素的迭代渲染,类似 Python 中的 for in
用法。
1 |
<table> 大专栏 Spring MVC 集成 Thymeleaf; |
条件
Thymeleaf 的条件表达式包括 if
,unless
和 switch
。
1 |
<a href="comments.html" |
上面代码中,如果条件标签 th:if
的结果为真,则显示 a
标签的内容,否则不显示。th:if
判 true 的情况有:
- 布尔值 true
- 非零数字
- 非零字符
- 除 “false” “off” “no” 以外的字符串
- 布尔值、数字、字符、字符串之外的变量形式
th:unless
的用法正好和 th:if
相反,要实现和上面代码一样的作用,th:unless
会这样写:
1 |
<a href="comments.html" |
th:switch
和多数编程语言的 switch-case 用法是很类似的,其中 default 情况的表达式是 th:case="*"
:
1 |
<div th:switch="${user.role}"> |
块和引用
如果用过 Python 的优秀模板 Jinja2 的话,一定会对 Jinja2 的 block
印象深刻,因为这实现了模板的分块和复用,避免了大段代码的重复。Thymeleaf 同样也实现了这种优秀的特性,由标签 th:fragment
进行分块,th:include
进行引用。
下面是页面 footer.html 的代码:
1 |
<html> |
在另外一个页面中同样需要 footer 中的版权信息,所以把名为 copy 的块引入进来:
1 |
<body> |
th:include="templatename::domselector"
指定了引入模板块所在文件名称和模板块的名称。
引用模板块不仅仅只是支持 Thymeleaf 自定义的 th:fragment
,还支持 HTML 原生的 DOM 节点选择。
1 |
<div th:include="footer :: #copy"></div> |
如上引入的就是 footer.html 中 id=copy 的 DOM。
另一个和 th:include
标签类似的是 th:replace
,两者都能引入模板块,但区别是 th:include
是把模板块内的内容引入到 th:include
所在的 DOM 节点;th:replace
是把模板块整个引入到 th:replace
签所在 DOM 节点:
1 |
<body> |
最终渲染的结果如下:
1 |
<body> |
集成 Thymeleaf
大致梳理了下 Thymeleaf 的基础用法,接下来就把它集成到 Spring MVC 中。Thymeleaf 分别提供了 thymeleaf-spring3 和 thymeleaf-spring4 来支持 Spring 3.x 和 Spring 4.x,下面以最新的 4.x 版本为例。
设置模板引擎
首先需要把 Thymeleaf 配置为 Spring MVC 的模板引擎:
1 |
<bean id="templateResolver" |
配置内容很清晰,上述 xml 设置了模板文件所在路径,后缀名,因此 @Controller
下的方法只需要返回模板的名称,模板引擎就能通过路径和后缀名,拼接找到对应模板进行渲染。
View 和 View Resolver
Thymeleaf 实现了 org.thymeleaf.spring4.view.ThymeleafView
和 org.thymeleaf.spring4.view.ThymeleafViewResolver
来替换 Spring 内置的视图解析。这两个类负责在 Thymeleaf 模板中处理 @Controller
方法执行的结果。配置 ViewResolver 如下:
1 |
<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver"> |
其中,属性 templateEngine
引用了之前配置的模板引擎,order
属性设置了 ViewResolver 的处理优先级,这是可选的参数,在配置多视图解析器时会需要用到。这里对 prefix
和 suffix
属性进行设置,因为已经在 Template Resolver 中设置了。
如果需要定义一个 View 的 bean,并预置静态变量,也很简单:
1 |
<bean name="main" class="org.thymeleaf.spring4.view.ThymeleafView"> |
Spring MVC 配置
Spring MVC 的配置中除了指定 Template Engine 和 View Resolver 外,还需要指定标准的 Spring MVC artifacts,如静态文件的处理,注解的扫描:
1 |
<?xml version="1.0" encoding="UTF-8"?> |