本文首發於 歐雷流。因爲我會時不時對文章進行補充、修正和潤色,爲了保證所看到的是最新版本,請閱讀 原文。
本文並不是 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"
。至此,本文已接近尾聲,若是你在看過以後茅塞頓開,那我這就是一篇成功的文章!