本文首发于 欧雷流。因为我会时不时对文章进行补充、修正和润色,为了保证所看到的是最新版本,请阅读 原文。
本文并不是 Thymeleaf 使用教程,而是讲述如何以尽量小的改动将页面从 Velocity 迁移到 Thymeleaf。若想了解 Thymeleaf 的用法,请看官方文档。javascript
谨以此文献给那些将要从 Velocity 跳到 Thymeleaf 这个坑的人。
提到 Thymeleaf,想必你们对这个名字比较陌生,若是是在几天前我也是闻所未闻。然而,大佬忽然一声令下:「咱们要把仓储管理系统分离出去,用 Spring Boot 进行开发。」相伴而来的就是后端模板引擎的变动——再也不支持 Velocity 了!css
在接到这个消息后,第一时间到官网看下这首次听到的东西长个啥样。乍一看,以为咋那么眼熟呢?哦~原来是跟 Vue 有点像!html
先来瞅一瞅 Vue 的模板语法——java
<!-- 对属性动态赋值 --> <div v-bind:id="dynamicId"></div> <!-- 条件渲染 --> <div v-if="condition">在符合条件时才显示该元素</div> <!-- 列表渲染 --> <ul> <li v-for="(item, index) in items">{{ index }} - {{ item.message }}</li> </ul>
再看看 Thymeleaf——后端
<!-- 对属性动态赋值 --> <div th:id="${dynamicId}"></div> <!-- 条件渲染 --> <div th:if="${condition}">在符合条件时才显示该元素</div> <!-- 列表渲染 --> <ul> <li th:each="item : ${items}" th:text="${item.message}">此处文本会被覆盖</li> </ul>
我去!难道它们是失散多年的双胞胎?!ide
目前大部分项目是 Spring MVC + Velocity,但之后的新项目极可能都是 Spring Boot + Thymeleaf。无论怎么说,仍是先看下 Velocity 中的模板用法吧。布局
在我所参与的项目中,layout 的模板代码大概是这样的——post
#set($timestamp = $dateTool.get("yyyyMMddHH")) <!DOCTYPE html> <html lang="zh-CN" dir="ltr" data-page="$!{primaryPage}-$!{secondaryPage}"> <head> <meta charset="UTF-8"> <!-- 页面标题 --> <title>#if($!pageTitle)$!{pageTitle} - #end后台系统</title> <!-- 网站图标 --> <link rel="icon" href="/bower_components/handle/dist/images/favicon.png"> <!-- 全局样式 --> <link rel="stylesheet" href="/template/assets/admin/reset.css?t=$!timestamp"> <!-- 各页面样式 --> $!headAssets <!-- 全局脚本 --> <script src="/template/assets/admin/global.js?t=$!timestamp"></script> </head> <body class="Page"> <header class="Page-header Header"> <div class="Header-brand"> <a href="/"><img src="/bower_components/handle/dist/images/logo.png" srcset="/bower_components/handle/dist/images/logo-2x.png 2x" alt="卖好车"><span>后台</span></a> </div> <div class="Header-extra"> <div class="Header-operations"> <!-- 页头中的操做 --> $!headerActions <!-- 新增数据按钮 --> #if($!modal)<div class="Header-action Action"><button class="Action-trigger fa fa-plus js-add--header" type="button" data-toggle="modal" data-target=".js-addNewData" title="新增"><span class="sr-only">新增</span></button></div>#end <!-- 用户信息 --> #if($!user) #if($!user.realName.length() > 2) #set($startPos = $!user.realName.length() - 2) #set($displayName = $!user.realName.substring($startPos, $!user.realName.length())) #else #set($displayName = $!user.realName) #end <div class="Header-action Action Action--avatar"><a class="Action-trigger" href="javascript:void(0);"><span>$!displayName</span></a> <div class="Action-content Card"> <div class="Card-content"> <ul> <li>$!user.mobile</li> <li>$!user.email</li> </ul> </div> <div class="Card-footer"> <a href="/logout.htm" class="btn btn-default btn-xs">退出</a> </div> </div> </div> #end </div> </div> </header> <main class="Page-content"> <div class="Page-sidebar Sidebar"> <nav class="Sidebar-navs Navs"> <ul> ... </ul> </nav> </div> <div class="Page-main"> <div class="Content container-fluid"> <div class="Content-header"> <!-- 面包屑 --> <div class="Breadcrumb"><i class="fa fa-map-marker"></i>$!breadcrumb</div> <!-- 页面标题 --> <h1>$!pageTitle</h1> </div> <!-- 页面内容片断 --> $screen_content <!-- 条件筛选区域 --> $!queryArea <!-- 数据表格区域 --> <div class="Area Area--table"> #if($!dataTableList) $dataTableList #else <table class="js-showDataTable"></table> #end </div> </div> <!-- 新增/修改数据对话框 --> $!modal </div> </main> <!-- 各页面脚本 --> $!bodyAssets </body> </html>
其中所使用的变量都是具体页面中定义的,有的是用 #set()
定义的简单的值:网站
变量名 | 含义 | 是否必须 |
---|---|---|
primaryPage |
一级页面标记 | 是 |
secondaryPage |
二级页面标记 | 是 |
pageTitle |
当前页面标题 | 是 |
有的是用 #define()
定义的代码片断:ui
变量名 | 含义 | 是否必须 |
---|---|---|
headAssets |
各页面样式 | 否 |
headerActions |
页头中的操做 | 否 |
breadcrumb |
面包屑 | 是 |
queryArea |
条件筛选区域 | 否 |
dataTableList |
数据表格 | 否 |
modal |
新增/修改数据对话框 | 否 |
bodyAssets |
各页面脚本 | 否 |
每一个具体页面的模板中所写的代码,除了在 layout 中指定位置引用的 #define()
定义的片断会显示在相应的位置,其余的不在 #define()
中的代码都会被渲染到 $screen_content
的位置——
#set($primaryPage = "example") #set($secondaryPage = "demo") #set($pageTitle = "示例页面") #define($headAssets) <link rel="stylesheet" href="/template/views/admin/example/demo.css?t=$!timestamp"> #end #define($bodyAssets) <script src="/template/views/admin/example/demo.js?t=$!timestamp"></script> #end #define($breadcrumb) <ul> <li>使用案例</li> <li>$pageTitle</li> </ul> #end #define($queryArea) <div class="Area Area--query"> <form class="Card"> <div class="Card-content"> <div class="row"> <div class="form-group col-xs-6 col-sm-4 col-lg-3"> <label>查询条件</label> <select name="selectDemo" class="form-control input-sm" multiple data-placeholder="请选择"> #foreach($o in $opts) <option value="${o.value}">${o.text}</option> #end </select> </div> </div> </div> <div class="Card-footer"> <button type="submit" class="btn btn-primary btn-sm"><i class="fa fa-filter"></i><span>筛选</span></button><button type="reset" class="btn btn-default btn-sm"><i class="fa fa-refresh"></i><span>重置</span></button> </div> </form> </div> #end #define($modal) <div class="modal fade js-addNewData"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal"><span>×</span></button> <h4 class="modal-title">填写信息</h4> </div> <div class="modal-body"> <form> ... </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button> <button type="button" class="btn btn-primary js-saveNewData">提交</button> </div> </div> </div> </div> #end <section><p>这是一个示例页面</p></section>
虽然 Themeleaf 的语法比较「友好」,但彻底靠本身去把原来用 Velocity 写的页面彻底成功迁移过来,至少得用半天到一天的时间去踩坑探索。但有了这篇文章就不同了,看完以后基本不用去看官方文档就可以完成!
无论怎么说,Thymeleaf 的模板语法仍是要先叨咕叨咕的。
虽说的时候只说「Thymeleaf」,但在实际使用时倒是 Thymeleaf 和 Thymeleaf Layout Dialect。前者提供核心功能,其语法为 th:*
;后者专解决布局及模板继承问题,语法是 layout:*
。本文中所用示例是基于 Thymeleaf 2.x 和 Thymeleaf Layout Dialog 1.x 实现,有的用法在新版本中可能已不被支持。
在迁移的过程当中,主要用到的语法以下:
语法 | 做用 |
---|---|
layout:decorator |
指定所继承的布局模板 |
layout:fragment |
定义用于布局的代码片断 |
th:fragment |
定义通用的非布局代码片断 |
th:replace |
用指定片段替换当前元素 |
th:with |
向代码片断中传入参数 |
th:if |
条件判断 |
th:each |
遍历 |
th:text |
覆盖文本 |
在访问变量时要用 ${variable}
形式,文件路径用 @{/path/to/your/file}
的形式。另外,Thymeleaf 中提供了一个不被渲染的可用做占位符的虚拟元素——<th:block>
。
在了解了这些语法以后,就能够开展迁移工做了!
用上面所介绍的语法,将 Velocity 的 layout 改造为——
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" lang="zh-CN" dir="ltr" th:attr="data-page=(${primaryPage} and ${secondaryPage} ? (${primaryPage} + '-' + ${secondaryPage}) : '')"> <head th:with="timestamp=${#dates.format(#dates.createNow(),'yyyyMMddHH')}"> <meta charset="UTF-8" /> <!-- 页面标题 --> <title th:text="${pageTitle} + '- 后台系统'"></title> <!-- 网站图标 --> <link rel="icon" th:href="@{/bower_components/handle/dist/handle/images/favicon.png}" /> <!-- 全局样式 --> <link rel="stylesheet" th:href="@{/assets/admin/reset.css(t=${timestamp})}" /> <!-- 全局脚本 --> <script th:src="@{/assets/admin/global.js(t=${timestamp})}"></script> </head> <body class="Page" th:with="timestamp=${#dates.format(#dates.createNow(),'yyyyMMddHH')}"> <header class="Page-header Header"> <div class="Header-brand"> <a href="/"><img th:src="@{/bower_components/handle/dist/handle/images/logo.png(t=${timestamp})}" th:attr="srcset=(@{/bower_components/handle/dist/handle/images/logo-2x.png(t=${timestamp})} + ' 2x')" alt="卖好车" /><span>后台</span></a> </div> <div class="Header-extra"> <div class="Header-operations"> <!-- 页头中的操做 --> <th:block layout:fragment="headerActions"></th:block> <!-- 新增数据按钮 --> <div class="Header-action Action" th:if="${creatable}"><button class="Action-trigger fa fa-plus js-add--header" type="button" data-toggle="modal" data-target=".js-addNewData" title="新增"><span class="sr-only">新增</span></button></div> <!-- 用户信息 --> <th:block th:if="${user != null}"> <div class="Header-action Action Action--avatar"><a class="Action-trigger" href="javascript:void(0);"><span th:text="${user.realName.substring((user.realName.length() - 2), user.realName.length())}"></span></a> <div class="Action-content Card"> <div class="Card-content"> <ul th:object="${user}"> <li th:if="*{mobile}" th:text="*{mobile}"></li> <li th:if="*{email}" th:text="*{email}"></li> </ul> </div> <div class="Card-footer"> <a th:href="@{/logout.htm}" class="btn btn-default btn-xs">退出</a> </div> </div> </div> </th:block> </div> </div> </header> <main class="Page-content"> <div class="Page-sidebar Sidebar"> <nav class="Sidebar-navs Navs"> <ul> ... </ul> </nav> </div> <div class="Page-main"> <div class="Content container-fluid"> <div class="Content-header"> <!-- 面包屑 --> <div class="Breadcrumb"><i class="fa fa-map-marker"></i><th:block layout:fragment="breadcrumb"></th:block></div> <!-- 页面标题 --> <h1 th:text="${pageTitle}"></h1> </div> <!-- 页面内容片断 --> <th:block layout:fragment="content"></th:block> <!-- 条件筛选区域 --> <th:block layout:fragment="query"></th:block> <!-- 数据表格区域 --> <div class="Area Area--table"> <table class="js-showDataTable"></table> </div> </div> <!-- 新增/修改数据对话框 --> <th:block layout:fragment="modal"></th:block> </div> </main> <!-- 各页面脚本 --> <th:block layout:fragment="bodyAssets"></th:block> </body> </html>
若是细心观察就会发现,迁移后与迁移前相比,少了 headAssets 变量并多了个 creatable 变量。
去掉了 headAssets
是由于 Thymeleaf Layout Dialect 提供了一种机制,能够将具体页面模板的 <head>
标签中的 <link>
和 <script>
自动插入到布局模板的 <head>
标签的底部,即闭合标签 </head>
前。
增长了 creatable
则是由于 Thymeleaf 中没法对某个代码片断判断是否存在。(也许是我不会……)
只要布局模板的继承及排列逻辑搞定了,具体页面模板的迁移就小菜一碟儿了~
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layouts/admin" th:with="pageTitle='示例页面', primaryPage='example', secondaryPage='demo', creatable=true"> <head> <link rel="stylesheet" th:href="@{/template/views/admin/example/demo.css(t=${timestamp})}"> </head> <body> <th:block layout:fragment="content"> <section><p>这是一个示例页面</p></section> </th:block> <th:block layout:fragment="query"> <div class="Area Area--query"> <form class="Card"> <div class="Card-content"> <div class="row"> <div class="form-group col-xs-6 col-sm-4 col-lg-3"> <label>查询条件</label> <select name="selectDemo" class="form-control input-sm" multiple data-placeholder="请选择"> <option th:each="o : $opts" th:value="${o.value}" th:text="${o.text}"></option> </select> </div> </div> </div> <div class="Card-footer"> <button type="submit" class="btn btn-primary btn-sm"><i class="fa fa-filter"></i><span>筛选</span></button><button type="reset" class="btn btn-default btn-sm"><i class="fa fa-refresh"></i><span>重置</span></button> </div> </form> </div> </th:block> <th:block layout:fragment="modal"> <div class="modal fade js-addNewData"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal"><span>×</span></button> <h4 class="modal-title">填写信息</h4> </div> <div class="modal-body"> <form> ... </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button> <button type="button" class="btn btn-primary js-saveNewData">提交</button> </div> </div> </div> </div> </th:block> <th:block layout:fragment="bodyAssets"> <script th:src="@{/template/views/admin/example/demo.js(t=${timestamp})}"></script> </th:block> <ul layout:fragment="breadcrumb"> <li>使用案例</li> <li th:text="${pageTitle}"></li> </ul> </body> </html>
重要的部分都已经说完了,但在迁移过程当中有几点须要注意的,不然 Thymeleaf 在解析时会报错:
<img>
、<input>
这类单标签须要有斜杠关闭标签:<img />
、<input />
;required
、multiple
等属性须要有值:required="required"
、multiple="multiple"
。至此,本文已接近尾声,若是你在看过以后茅塞顿开,那我这就是一篇成功的文章!