

从 Velocity 到 Thymeleaf:浅谈模板迁移
source link: https://ourai.ws/posts/from-velocity-to-thymeleaf/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

从 Velocity 到 Thymeleaf:浅谈模板迁移
欧雷 发表于 4 年之前
谨以此文献给那些将要从 Velocity 跳到 Thymeleaf 这个坑的人。
欧雷
提到 Thymeleaf,想必大家对这个名字比较陌生,如果是在几天前我也是闻所未闻。然而,大佬突然一声令下:「我们要把仓储管理系统分离出去,用 Spring Boot 进行开发。」相伴而来的就是后端模板引擎的变更——不再支持 Velocity 了!
在接到这个消息后,第一时间到官网看下这首次听到的东西长个啥样。乍一看,觉得咋那么眼熟呢?哦~原来是跟 Vue 有点像!
先来瞅一瞅 Vue 的模板语法——
<!-- 对属性动态赋值 -->
<div v-bind:id="dynamicId"></div>
<!-- 条件渲染 -->
<div v-if="condition">在符合条件时才显示该元素</div>
<!-- 列表渲染 -->
<ul>
<li v-for="(item, index) in items"> - </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>
我去!难道它们是失散多年的双胞胎?!
本文并非 Thymeleaf 使用教程,而是讲述如何以尽可能小的改动将页面从 Velocity 迁移到 Thymeleaf。若想了解 Thymeleaf 的用法,请看官方文档。
Spring MVC + Velocity
目前大部分项目是 Spring MVC + Velocity,但以后的新项目很可能都是 Spring Boot + Thymeleaf。不管怎么说,还是先看下 Velocity 中的模板用法吧。
在我所参与的项目中,layout 的模板代码大概是这样的——
#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()
定义的代码片段:
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>
Spring Boot + Thymeleaf
虽然 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"
。
至此,本文已接近尾声,如果你在看过之后茅塞顿开,那我这就是一篇成功的文章!
Recommend
-
81
thymeleaf的th:href传递多个参数格式
-
48
README.md
-
62
README.md
-
43
README.md Hope-Boot
-
50
虽然现在慢慢在流行前后端分离开发,但是据松哥所了解到的,还是有一些公司在做前后端不分的开发,而在前后端不分的开发中,我们就会需要后端页面模板(实际上,即使前后端分离,也会在一些场景下需要使用页面模板,例如邮件发送模板)。
-
65
#set指令 #set指令用于向一个变量或者对象赋值。 格式: #set($var = value) LHS是一个变量,不要使用特殊字符例如英文句号等,不能用大括号括起来。测试发现#set($user.name = 'zhangsan'),#set(${age} = 18)均...
-
25
通过本系列教程的前几章内容(API开发、数据访问)。我们已经具备完成一个涵盖数据存储、提供HTTP接口的完整后端服务了。依托这些技能,我们已经可以配合前端开发人员,一起来完成一些前后端分离的Web项目,或是一些小程序、或者是App之类...
-
16
概述 Thymeleaf提供了一组Spring集成,使您可以将其用作Spring MVC应用程序中JSP的全功能替代品。 这些集成将使您能够: @Controller像使用JSP一样,将Spring MVC 对象中的映射方法转发到Thymeleaf管理...
-
17
SpringBoot系列(六)集成thymeleaf详解版 1. thymeleaf简介 1. Thymeleaf是适用于Web和独立环境的现代服务器端Java模板引擎。 2. Thymeleaf的主要目标是为您的开发工作流程带来优雅的自然模板 -HTML可以在...
-
12
如果你和我一样,是一名 Java 道路上的编程男孩,其实我不太建议你花时间学 Thymeleaf,当然他的思想还是值得借鉴的。但是他的本质在我看来就是 Jsp 技术的翻版(Jsp 现在用的真的很少很少)。弄前端完全可以直接上手前端框架 vue。并竟学Ja...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK