Java 前端模板引擎学习:thymeleaf 模板引擎
模板引擎接口 ITemplateEngine
一、后台数据与外部数据
1.处理后台数据
$表达式是个变量表达式,用于处理在 request parameters and the request, session and application 中的变量
${x}
will return a variablex
stored into the Thymeleaf context or as a request attribute.${param.x}
will return a request parameter calledx
(which might be multivalued).${session.x}
will return a session attribute calledx
.${application.x}
will return a servlet context attribute calledx
.
1 | < input type="text" name="userName" value="James Carrot" th:value="${user.name}" /> |
2.处理外部数据
外部化的片段通常叫作 messages,#表达式用于处理这类消息。外部消息可以从数据库中获取,或从 .properties
files 中获取,这取决于 StandardMessageResolver 的实现。Thymeleaf 的默认实现为 StandardMessageResolver。
1 | < p th:text="#{home.welcome}">Welcome to our grocery store!</ p > |
为了实现属性名的 i18n,消息解析器 StandardMessageResolver 将/WEB-INF/templates/home.html
映射于同文件夹同名文 propreties 上,比如
/WEB-INF/templates/home_en.properties
for English texts./WEB-INF/templates/home_es.properties
for Spanish language texts./WEB-INF/templates/home_pt_BR.properties
for Portuguese (Brazil) language texts./WEB-INF/templates/home.properties
for default texts (if the locale is not matched).
注意在 spring boot 中,国际化消息直接映射到资源目录下,如 /i18n/ messages.properties 或 /i18n/ messages _en.properties
二、文本数据处理
1.不转义
如果 $-expression 获取的值包含 < >等,默认会对其转义为 < >
比如:
1 | home.welcome=Welcome to our < b >fantastic</ b > grocery store! |
使用 th:text ,默认转义成返回
1 2 3 | <p th:text= "#{home.welcome}" >Welcome to our grocery store!</p> <p>Welcome to our <b>fantastic</b> grocery store!</p> |
使用 th:utext 返回
1 2 3 | < p th:utext="#{home.welcome}">Welcome to our grocery store!</ p > < p >Welcome to our < b >fantastic</ b > grocery store!</ p > |
三、# 表达式
#{...} expressions 外部消息常用于实现国际化
1 2 3 | < p th:utext="#{home.welcome}">Welcome to our grocery store!</ p > i18n:home.welcome=¡Bienvenido a nuestra tienda de comestibles! |
你可以指定 #-expression 的 key 从 $-表达式获取:
1 2 3 | < p th:utext="#{${welcomeMsgKey}"> Welcome to our grocery store, Sebastian Pepper! </ p > |
#-expression 中夹带 变量进行结合输出,#{} 中使用 () 包裹传入的变量,多个变量使用 , 分隔
1 2 3 4 5 | home.welcome=¡Bienvenido a nuestra tienda de comestibles, {0}! < p th:utext="#{home.welcome(${session.user.name})}"> Welcome to our grocery store, Sebastian Pepper! </ p > |
* 特殊情况
模板连接变量,形成消息
1 | < td th:text="#{|seedstarter.type.${sb.type}|}">Wireframe</ td > |
消息变成变量,预处理
1 | < p th:text="${__#{article.text('textVar')}__}">Some text here...</ p > |
四、$ 表达式
${...}
expressions 用于操作变量,基于 OGNL表达式 (在 springmvc 中使用 spel 代替 ognl),对其相应的 context中的 变量进行映射
为了更灵活地使用 ognl,也包含了 7 个预置对象,
#ctx
: the context object.#vars:
the context variables.#locale
: the context locale.#request
: (only in Web Contexts) theHttpServletRequest
object.#response
: (only in Web Contexts) theHttpServletResponse
object.#session
: (only in Web Contexts) theHttpSession
object.#servletContext
: (only in Web Contexts) theServletContext
object.
以 # 号开始以引用这些预置对象。示例:
1 | Established locale country: < span th:text="${#locale.country}">US</ span >. |
* 内置对象的使用技巧:
访问 param, session, application 属性时,不需要加 #
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 直接访问对象域的属性 ${param.foo} // Retrieves a String[] with the values of request parameter 'foo' ${param.number[0]} // Retrieves a String with the values of request parameter 'number', ${param.size()} ${param.isEmpty()} ${param.containsKey('foo')} ${session.foo} // 获取 session 中的属性 ${session.size()} ${session.isEmpty()} ${session.containsKey('foo')} ${application.foo} // Retrieves the ServletContext atttribute 'foo' ${application.size()} ${application.isEmpty()} ${application.containsKey('foo')} # 直接访问 javax.servlet.http.HttpSession 对象 ${#session.getAttribute('foo')} ${#session.id} ${#session.lastAccessedTime} |
v注意:返回的是一个字符串数组:
除了 7 个预置顶级对象,还有 16 个工具对象:
#execInfo
: information about the template being processed.#messages
: methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.#uris
: methods for escaping parts of URLs/URIs#conversions
: methods for executing the configured conversion service (if any).#dates
: methods forjava.util.Date
objects: formatting, component extraction, etc.#calendars
: analogous to#dates
, but forjava.util.Calendar
objects.#numbers
: methods for formatting numeric objects.#strings
: methods forString
objects: contains, startsWith, prepending/appending, etc.#objects
: methods for objects in general.#bools
: methods for boolean evaluation.#arrays
: methods for arrays.#lists
: methods for lists.#sets
: methods for sets.#maps
: methods for maps.#aggregates
: methods for creating aggregates on arrays or collections.#ids
: methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).
工具对象的使用,请查看原文。 常用工具类对象的操作方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | # 格式化数字 ${#numbers.formatDecimal(num,3,2)} // 整数位显示位数与小数精确位数 ${#numbers.formatCurrency(num)} // 货币 ${#numbers.formatPercent(num, 3, 2)} // 百分号 # 格式化日期 ${#dates.second(date)} ${#dates.createNow()} ${#dates.create(year,month,day)} ${#dates.format(date, 'dd/MMM/yyyy HH:mm')} # 操作字符串 ${#strings.toString(obj)} //打印对象 ${#strings.trim(str)} //去除首尾空白 ${#strings.containsIgnoreCase(name,'ez')} // 判断是否包含 ${#strings.startsWith(name,'Don')} // also array*, list* and set* ${#strings.endsWith(name,endingFragment)} // 判断前缀与后缀 ${#strings.abbreviate(str,10)} // 大于此位数内容,以省略号 ...代替 ${#strings.indexOf(name,frag)} ${#strings.substring(name,3,5)} ${#strings.substringAfter(name,prefix)} ${#strings.substringBefore(name,suffix)} // 截取字符串 ${#strings.replace(name,'las','ler')} // 替换字符串 ${#strings.arrayJoin(namesArray,',')} ${#strings.listJoin(namesList,',')} ${#strings.setJoin(namesSet,',')} ${#strings.toUpperCase(name)} // also array*, list* and set* ${#strings.toLowerCase(name)} ${#strings.capitalizeWords(str)} //首字母大写 ${#strings.arraySplit(namesStr,',')} // 返回 String[],用于遍历 # 聚集函数 ${#aggregates.sum(array)} // 求何 ${#aggregates.avg(array)} // 平均数 #随机数 ${#strings.randomAlphanumeric(count)} #遍历数字的工具 ${#numbers.sequence(from,to)} ${#numbers.sequence(from,to,step)} # 类型转换 ${#conversions.convert(object, 'java.util.TimeZone')} #转义字符串,防止JavaScript or JSON 注入 ${#strings.escapeXml(str)} ${#strings.escapeJava(str)} ${#strings.escapeJavaScript(str)} ${#strings.unescapeJava(str)} ${#strings.unescapeJavaScript(str)} |
五、* 表达式
当对象被选择(使用 th:object )后,可以使用 *{} 获取对象属性
1 2 3 4 5 | < div th:object="${session.user}"> < p >Name: < span th:text="*{firstName}">Sebastian</ span >.</ p > < p >Surname: < span th:text="*{lastName}">Pepper</ span >.</ p > < p >Nationality: < span th:text="*{nationality}">Saturn</ span >.</ p > </ div > |
下面几处用法效果等同
1 2 3 4 5 | < div th:object="${session.user}"> < p >Name: < span th:text="${#object.firstName}">Sebastian</ span >.</ p > < p >Surname: < span th:text="${session.user.lastName}">Pepper</ span >.</ p > < p >Nationality: < span th:text="*{nationality}">Saturn</ span >.</ p > </ div > |
此外,如果没有对象被选择,则 *-expression 与 $-expression 完全相当
1 2 3 4 5 | < div > < p >Name: < span th:text="*{session.user.name}">Sebastian</ span >.</ p > < p >Surname: < span th:text="*{session.user.surname}">Pepper</ span >.</ p > < p >Nationality: < span th:text="*{session.user.nationality}">Saturn</ span >.</ p > </ div > |
六、A 标签处理
1.标签 th:href 与 @{} expressions
1 2 3 4 5 6 7 8 9 | <!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) --> < a href="details.html" th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</ a > <!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) --> < a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</ a > <!-- Will produce '/gtvg/order/3/details' (plus rewriting) --> < a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</ a > |
* 为什么使用 th:href="@{}"
使用模板引擎管理 link 地址,方便对链接进行额外处理(如版本处理之类)
2.注意不可变部分,需要作预处理
1 | < a href="#" th:href="@{__${#request.requestURI}__/learn}">Learn</ a > |
七、一些特殊的表达式语法
① 模板字符串代替 + 连接
1 2 3 | < span th:text="'Welcome to our application, ' + ${user.name} + '!'"> < span th:text="|Welcome to our application, ${user.name}!|"> |
下文 https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#expressions-on-selections-asterisk-syntax
②预编译(预处理)
1 2 3 4 | < tr th:each="book, itemStat : *{books}"> < td >< input th:field="*{books[__${itemStat.index}__].title}" /></ td > < td >< input th:field="*{books[__${itemStat.index}__].author}" /></ td > </ tr > |
也可以拆开写成 th:name 与 th:value (不推荐)
1 2 3 4 5 | < tr th:each="book, itemStat : ${form.books}"> < td >< input hidden th:name="|books[${itemStat.index}].id|" th:value="${book.getId()}"/></ td > < td >< input th:name="|books[${itemStat.index}].title|" th:value="${book.getTitle()}"/></ td > < td >< input th:name="|books[${itemStat.index}].author|" th:value="${book.getAuthor()}"/></ td > </ tr > |
③ 代数运算,可以使用 div
(/
), mod
(%
). 等
1 2 3 | < div th:with="isEven=(${prodStat.count} % 2 == 0)"> < div th:with="isEven=${prodStat.count % 2 == 0}"> |
④ 比较运算,可以使用 gt
(>
), lt
(<
), ge
(>=
), le
(<=
), not
(!
). Also eq
(==
), neq
/ne
(!=
)
1 2 | < div th:if="${prodStat.count} > 1"> < span th:text="'Execution mode is ' + ( (${execMode} == 'dev')? 'Development' : 'Production')"> |
⑤ 条件表达式,其中 else 部分也可以忽略不写,则当为否时返回 null
1 2 3 4 5 6 7 8 | < tr th:class="${row.even}? 'even' : 'odd'"> ... </ tr > < tr th:class="${row.even}? 'alt'"> ... </ tr > |
⑥ 默认值表达式,当变量没有赋任何值时,使用该默认值
1 2 3 4 | < div th:object="${session.user}"> ... < p >Age: < span th:text="*{age}?: '(no age specified)'">27</ span >.</ p > </ div > |
也可以在子表达式中,效果类似于:
1 2 3 4 | < p > Name: < span th:text="*{firstName}?: (*{admin}? 'Admin' : #{default.username})">Sebastian</ span > </ p > |
⑦ 无操作符号 no-operation token ,即使用当前文字节点
1 | < span th:text="${user.name} ?: _">no user authenticated</ span > |
相当于
1 | < span th:text="${user.name} ?: 'no user authenticated'">...</ span > |
⑧ 自动应用转换服务
服务端配置文件 —— application.yml (这种配置只在页面格式化管用,在向 controller 传参时并不管用)
1 2 3 | spring: mvc: date-format: yyyy-mm-dd |
或 JavaCofig (页面和传参都管用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Override public void addFormatters( final FormatterRegistry registry) { super .addFormatters(registry); registry.addFormatter(varietyFormatter()); registry.addFormatter(dateFormatter()); } @Bean public VarietyFormatter varietyFormatter() { return new VarietyFormatter(); } @Bean public DateFormatter dateFormatter() { return new DateFormatter(); } |
页面使用 双括号 {{ 与 }}
1 | < td th:text="${{sb.datePlanted}}">13/01/2011</ td > |
⑨ 静态方法调用与 Bean 方法调用
将字符串转换为 Integer
1 | T(Integer).parseInt(param.pagenum) |
或者引用自定义工具类
1 | th:text="${T(com.package.SUtils).reverseString('hello')}" |
使用 @引用 Bean
1 | < span th:text="${@sUtils.reverseString('hello')}"></ span > |
⑩ 对集合数组进行 map 与 filter 处理
- selection means filtering each element of the collection using its properties. The result of such an operation is a subset of the initial collection, only matching elements are retained
.
- projection means filtering the property of each element in the collection. The result of a projection operation is a a new collection, with the same number of elements than the original one, but containing only the filtered property of the elements, not the whole element object itself
示例:
map ( projection )
1 2 3 4 | < tr th:each="artist,rowStat : ${listArtits.![firstname+' '+lastname]}"> < td class="center middle" th:text="${rowStat.count}">1</ td > < td class="center middle" th:text="${artist}">Michael Jackson</ td > </ tr > |
filter ( selection )
1 2 3 4 5 6 | < tr th:each="artist,rowStat : ${listArtits.?[alive == true]}"> < td class="center middle" th:text="${rowStat.count}">1</ td > < td class="center middle" th:text="${artist.name}">Michael Jackson</ td > < td class="center middle" th:text="${artist.discography}">Got to Be There, Ben, Music & Me, Forever Michael...</ td > < td class="center middle" th:text="${artist.bio}">Michael Joseph Jackson (August 29, 1958 - June 25, 2009) was an American recording artist, entertainer, and businessman...</ td > </ tr > |
八、操作标签的属性值
th:attr
1 2 3 4 5 6 | < form action="subscribe.html" th:attr="action=@{/subscribe}"> < fieldset > < input type="text" name="email" /> < input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/> </ fieldset > </ form > |
多个属性用逗号分开
1 2 3 4 | < img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" /> < img src="/gtgv/images/gtvglogo.png" title="Logo de Good Thymes" alt="Logo de Good Thymes" /> |
或者属性联写
1 2 | < img src="../../images/gtvglogo.png" th:src="@{/images/gtvglogo.png}" th:alt-title="#{logo}" /> |
也可以直接使用 th:value 、 th:src、th:action 特殊属性(见 setting-value-to-specific-attributes )
1 2 3 4 5 6 7 | < img src="../../images/gtvglogo.png" th:src="@{/images/gtvglogo.png}" th:title="#{logo}" th:alt="#{logo}" /> or < img src="../../images/gtvglogo.png" th:src="@{/images/gtvglogo.png}" th:alt-title="#{logo}" /> |
增加属性值(Appending and prepending)
1 | < input type="button" value="Do it!" class="btn" th:attrappend="class=${' ' + cssStyle}" /> |
如果 cssStyle 是 warning 的话,则 class 会增加一个值
1 | < input type="button" value="Do it!" class="btn warning" /> |
也可是使用更简便的 th:classappend
1 | < tr th:each="prod : ${prods}" class="row" th:classappend="${prodStat.odd}? 'odd'"> |
九、遍历迭代
普通的迭代
1 2 3 4 5 6 7 8 9 | < tr th:each="prod : ${prods}"> < td th:text="${prod.name}">Onions</ td > < td th:text="${prod.price}">2.41</ td > < td th:text="${prod.inStock}? #{true} : #{false}">yes</ td > </ tr > < tr th:each="prod : ${prods}"> < td th:text="${prod.name}">Onions</ td > < td th:text="${prod.price}">2.41</ td > < td th:text="${prod.inStock}? #{true} : #{false}">yes</ td > </ tr > |
指定迭代状态量 th:each="prod,iterStat : ${prods}"
1 2 3 4 5 6 7 8 9 10 11 12 | < table > < tr > < th >NAME</ th > < th >PRICE</ th > < th >IN STOCK</ th > </ tr > < tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'"> < td th:text="${prod.name}">Onions</ td > < td th:text="${prod.price}">2.41</ td > < td th:text="${prod.inStock}? #{true} : #{false}">yes</ td > </ tr > </ table > |
注意状态的几种属性
1 2 3 4 5 6 | index: the current iteration index, starting with 0 (zero) count: the number of elements processed so far size: the total number of elements in the list even/odd: checks if the current iteration index is even or odd first: checks if the current iteration is the first one last: checks if the current iteration is the last one |
* 如果不手动设定,可以使用默认实现的 状态变量后缀Stat,比如这里的 prodStat
1 2 3 4 5 6 7 8 9 10 11 12 | < table > < tr > < th >NAME</ th > < th >PRICE</ th > < th >IN STOCK</ th > </ tr > < tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'"> < td th:text="${prod.name}">Onions</ td > < td th:text="${prod.price}">2.41</ td > < td th:text="${prod.inStock}? #{true} : #{false}">yes</ td > </ tr > </ table > |
* 如果遍历的内容需要更多 tr标签,可以使用 th:block,比如:
1 2 3 4 5 6 7 8 9 10 11 | < table > < th:block th:each="user : ${users}"> < tr > < td th:text="${user.login}">...</ td > < td th:text="${user.name}">...</ td > </ tr > < tr > < td colspan="2" th:text="${user.address}">...</ td > </ tr > </ th:block > </ table > |
渲染结果
1 2 3 4 5 6 7 8 9 10 11 | < table > <!--/*/ <th:block th:each="user : ${users}"> /*/--> < tr > < td th:text="${user.login}">...</ td > < td th:text="${user.name}">...</ td > </ tr > < tr > < td colspan="2" th:text="${user.address}">...</ td > </ tr > <!--/*/ </th:block> /*/--> </ table > |
十、判断语句
th:if
- If value is a boolean and is
true
. - If value is a number and is non-zero
- If value is a character and is non-zero
- If value is a String and is not “false”, “off” or “no”
- If value is not a boolean, a number, a character or a String.
1 2 3 | < a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:if="${not #lists.isEmpty(prod.comments)}">view</ a > |
相反 th:unless
1 2 3 | < a href="comments.html" th:href="@{/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</ a > |
或者,使用 switch-case 语句
1 2 3 4 5 | < div th:switch="${user.role}"> < p th:case="'admin'">User is an administrator</ p > < p th:case="#{roles.manager}">User is a manager</ p > < p th:case="*">User is some other thing</ p > </ div > |
十一、模板布局
1.基本用法
使用 th:fragment 定义 partial 模板
1 2 3 4 5 6 7 8 9 10 11 12 13 | <! DOCTYPE html> < html xmlns:th="http://www.thymeleaf.org"> < body > < div th:fragment="copy"> © 2011 The Good Thymes Virtual Grocery </ div > </ body > </ html > |
使用 th:insert 或 th:replace 插入模板(th:include 也可以,但在 3.0 后并不建议使用)
1 2 3 4 5 6 7 | < body > ... < div th:insert="footer :: copy"></ div > </ body > |
或者也可以给引号部分加一个 ~{} ,将器封装起来
1 2 3 4 5 6 7 | < body > ... < div th:insert="~{footer :: copy}"></ div > </ body > |
2. th:insert 、th:replace 与 th:include 区别
定义模板
1 2 3 | < footer th:fragment="copy"> © 2011 The Good Thymes Virtual Grocery </ footer > |
使用 div 标签引用模板
1 2 3 4 5 6 7 8 9 10 11 | < body > ... < div th:insert="footer :: copy"></ div > < div th:replace="footer :: copy"></ div > < div th:include="footer :: copy"></ div > </ body > |
分别渲染出的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | < body > ... < div > < footer > © 2011 The Good Thymes Virtual Grocery </ footer > </ div > < footer > © 2011 The Good Thymes Virtual Grocery </ footer > < div > © 2011 The Good Thymes Virtual Grocery </ div > </ body > |
得出结论:
- th:insert 包括自身标签和子标签,
- th:replace 包括子标签但不包括自身标签,
- th:include 则是包括自身标签不包括子标签。
3.没有 th:fragment
定义模板
1 2 3 4 5 | ... < div id="copy-section"> © 2011 The Good Thymes Virtual Grocery </ div > ... |
使用 id 属性引用该模板
1 2 3 4 5 6 7 | < body > ... < div th:insert="~{footer :: #copy-section}"></ div > </ body > |
4.模板中传递参数
① 普通变量
定义模板
1 2 3 | < div th:fragment="frag (onevar,twovar)"> < p th:text="${onevar} + ' - ' + ${twovar}">...</ p > </ div > |
调用模板,并传递相应的变量(注意这里调用的是当前模板文件中的 fragment,因此可以省略写成 th:replace=":: 函数名称 "
1 | < div th:replace="::frag (${value1},${value2})">...</ div > |
② 局部变量
定义模板
1 2 3 | < div th:fragment="frag"> ... </ div > |
传递局部变量
1 | < div th:replace="::frag (onevar=${value1},twovar=${value2})"> |
上面的表达式,相当于
1 | < div th:insert="::frag" th:with="onevar=${value1},twovar=${value2}"> |
5.常见的技巧
① head中引用 title 与 link 块
定义模板
1 2 3 4 5 6 7 8 9 10 11 12 13 | < head th:fragment="common_header(title,links)"> < title th:replace="${title}">The awesome application</ title > <!-- Common styles and scripts --> < link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}"> < link rel="shortcut icon" th:href="@{/images/favicon.ico}"> < script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></ script > <!--/* Per-page placeholder for additional links */--> < th:block th:replace="${links}" /> </ head > |
调用模板(注意这里调用的模板文件名称是 base
1 2 3 4 5 6 7 8 9 | ... < head th:replace="base :: common_header(~{::title},~{::link})"> < title >Awesome - Main</ title > < link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> < link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}"> </head |
渲染结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | ... < head > < title >Awesome - Main</ title > <!-- Common styles and scripts --> < link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css"> < link rel="shortcut icon" href="/awe/images/favicon.ico"> < script type="text/javascript" src="/awe/sh/scripts/codebase.js"></ script > < link rel="stylesheet" href="/awe/css/bootstrap.min.css"> < link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css"> </ head > ... |
* 传递到模板的参数,除了使用 ${} 符号引入变量,或者使用 ~{:: 嵌入当前的标签,还可以使用 no-operation token 或者 参数留空。比如:
无操作符号 no-operation token
1 2 3 4 5 6 7 8 9 10 | ... < head th:replace="base :: common_header(_,~{::link})"> < title >Awesome - Main</ title > < link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> < link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}"> </ head > ... |
参数留空
1 2 3 4 5 6 | < head th:replace="base :: common_header(~{::title},~{})"> < title >Awesome - Main</ title > </ head > ... |
② 条件调用模板
根据条件决定是否调用模板
1 2 3 | ... < div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : ~{}">...</ div > ... |
或者条件不满足,调用默认模板
1 2 3 4 5 | ... < div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : _"> Welcome [[${user.name}]], click < a th:href="@{/support}">here</ a > for help-desk support. </ div > ... |
③ 隐藏或移除 mock 数据(感觉跟 fragment 没多大关系)
通常在我们会定义一些 mock 数据,用来做静态网页测试,查看渲染效果。像下面这样后两段 <tr> 作为 mock 数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | < table > < tr > < th >NAME</ th > < th >PRICE</ th > < th >IN STOCK</ th > < th >COMMENTS</ th > </ tr > < tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'"> < td th:text="${prod.name}">Onions</ td > < td th:text="${prod.price}">2.41</ td > < td th:text="${prod.inStock}? #{true} : #{false}">yes</ td > < td > < span th:text="${#lists.size(prod.comments)}">2</ span > comment/s < a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</ a > </ td > </ tr > < tr class="odd" th:remove="all"> < td >Blue Lettuce</ td > < td >9.55</ td > < td >no</ td > < td > < span >0</ span > comment/s </ td > </ tr > < tr th:remove="all"> < td >Mild Cinnamon</ td > < td >1.99</ td > < td >yes</ td > < td > < span >3</ span > comment/s < a href="comments.html">view</ a > </ td > </ tr > </ table > |
由于使用 th:remove 标记,该内容将在服务端渲染时被移除。
移除的形式有很多种:
all
: Remove both the containing tag and all its children.body
: Do not remove the containing tag, but remove all its children.tag
: Remove the containing tag, but do not remove its children.all-but-first
: Remove all children of the containing tag except the first one.none
: Do nothing. This value is useful for dynamic evaluation.
鉴于此,在 tbody 标签上使用 all-but-first
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | < table > < thead > < tr > < th >NAME</ th > < th >PRICE</ th > < th >IN STOCK</ th > < th >COMMENTS</ th > </ tr > </ thead > < tbody th:remove="all-but-first"> < tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'"> < td th:text="${prod.name}">Onions</ td > < td th:text="${prod.price}">2.41</ td > < td th:text="${prod.inStock}? #{true} : #{false}">yes</ td > < td > < span th:text="${#lists.size(prod.comments)}">2</ span > comment/s < a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</ a > </ td > </ tr > < tr class="odd"> < td >Blue Lettuce</ td > < td >9.55</ td > < td >no</ td > < td > < span >0</ span > comment/s </ td > </ tr > < tr > < td >Mild Cinnamon</ td > < td >1.99</ td > < td >yes</ td > < td > < span >3</ span > comment/s < a href="comments.html">view</ a > </ td > </ tr > </ tbody > </ table > |
* 根据条件移除
1 | < a href="/something" th:remove="${condition}? tag">Link text not to be removed</ a > |
④ 单独文件作为 partial 模板
定义模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <! DOCTYPE html> < html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org"> < head > < title th:replace="${title}">Layout Title</ title > </ head > < body > < h1 >Layout H1</ h1 > < div th:replace="${content}"> < p >Layout content</ p > </ div > < footer > Layout footer </ footer > </ body > </ html > |
调用模板,并传入相应的参数
1 2 3 4 5 6 7 8 9 10 11 12 | <! DOCTYPE html> < html th:replace="~{layoutFile :: layout(~{::title}, ~{::section})}"> < head > < title >Page Title</ title > </ head > < body > < section > < p >Page content</ p > < div >Included on page</ div > </ section > </ body > </ html > |
十二、局部变量
定义一个或多个局部变量,并使用它们
1 2 3 4 5 6 7 8 9 | < div th:with="firstPer=${persons[0]},secondPer=${persons[1]}"> < p > The name of the first person is < span th:text="${firstPer.name}">Julius Caesar</ span >. </ p > < p > But the name of the second person is < span th:text="${secondPer.name}">Marcus Antonius</ span >. </ p > </ div > |
一个实用的例子,比如根据 i18n 文件读取格式化方式,来格式化日期,比如:
home_en.properties
1 | date.format=MMMM dd'','' yyyy |
home_es.properties
1 | date.format=dd ''de'' MMMM'','' yyyy |
定义局部变量,并引用它
1 2 3 | < p th:with="df=#{date.format}"> Today is: < span th:text="${#calendars.format(today,df)}">13 February 2011</ span > </ p > |
或者
1 2 3 4 5 | < p > Today is: < span th:with="df=#{date.format}" th:text="${#calendars.format(today,df)}">13 February 2011</ span > </ p > |
十三、特殊页面处理
1. 分页页面
2. 表单页面
① 基本用法
1 2 3 4 | < form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post"> < input type="text" th:field="*{datePlanted}" /> ... </ form > |
checkbox 处理(需要传两个对象,一个是全部属性 allFeatures,一个是当前选择的 features)
1 2 3 4 5 6 7 | < ul > < li th:each="feat : ${allFeatures}"> < input type="checkbox" th:field="*{features}" th:value="${feat}" /> < label th:for="${#ids.prev('features')}" th:text="#{${'seedstarter.feature.' + feat}}">Heating</ label > </ li > </ ul > |
radio 处理
1 2 3 4 5 6 | < ul > < li th:each="ty : ${allTypes}"> < input type="radio" th:field="*{type}" th:value="${ty}" /> < label th:for="${#ids.prev('type')}" th:text="#{${'seedstarter.type.' + ty}}">Wireframe</ label > </ li > </ ul > |
dropdown 处理
1 2 3 4 5 | < select th:field="*{type}"> < option th:each="type : ${allTypes}" th:value="${type}" th:text="#{${'seedstarter.type.' + type}}">Wireframe</ option > </ select > |
② 动态 field:
需求: 当 command 对象有一个 collection property,对这个属性进行遍历,需要生成不同的 field
示例:
创建书籍(显示页面)
1 2 3 4 5 6 7 8 9 10 11 | @GetMapping("/create") public String showCreateForm(Model model) { BooksCreationDto booksForm = new BooksCreationDto(); for (int i = 1; i <= 3; i++) { booksForm.addBook(new Book()); } model.addAttribute("form", booksForm); return "books/createBooksForm"; } |
表单页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | < form action="#" th:action="@{/books/save}" th:object="${form}" method="post"> < fieldset > < input type="submit" id="submitButton" th:value="Save"> < input type="reset" id="resetButton" name="reset" th:value="Reset"/> < table > < thead > < tr > < th > Title</ th > < th > Author</ th > </ tr > </ thead > < tbody > < tr th:each="book, itemStat : *{books}"> < td >< input th:field="*{books[__${itemStat.index}__].title}" /></ td > < td >< input th:field="*{books[__${itemStat.index}__].author}" /></ td > </ tr > </ tbody > </ table > </ fieldset > </ form > |
* 说明:
上面的模板通过 * 表达式选择该 集合属性,然后使用__ 预处理__表达式生成不同的 field(即带有数组样式的 id 与 name)渲染结果类似
1 | < input id="rows1.seedsPerCell" name="rows[1].seedsPerCell" type="text" value="" /> |
添加书籍(提交页面)
1 2 3 4 5 6 7 | @PostMapping("/save") public String saveBooks(@ModelAttribute BooksCreationDto form, Model model) { bookService.saveAll(form.getBooks()); model.addAttribute("books", bookService.findAll()); return "redirect:/books/all"; } |
③ 表单验证(* 关于 controller 部分请参考文末参考链接)
判断错误是否存在
1 2 | < input type="text" th:field="*{datePlanted}" th:class="${#fields.hasErrors('datePlanted')}? fieldError" /> |
或者直接使用 th:errorclass
1 | < input type="text" th:field="*{datePlanted}" class="small" th:errorclass="fieldError" /> |
遍历该 field 的错误
1 | < li th:each="err : ${#fields.errors('datePlanted')}" th:text="${err}" /> |
直接输出该 field 的错误
1 | < p th:if="${#fields.hasErrors('datePlanted')}" th:errors="*{datePlanted}">Incorrect date</ p > |
获取全部 field 错误
1 2 3 | < div th:if="${#fields.hasAnyErrors()}"> < p th:each="err : ${#fields.allErrors()}" th:text="${err}">...</ p > </ div > |
注意:
123#fields.hasErrors('*') <=> #fields.hasAnyErrors()
#fields.errors('*') <=> #fields.allErrors()
或通过 detailedErrors 详细遍历
1 2 3 4 5 6 | < ul > < li th:each="e : ${#fields.detailedErrors()}" th:class="${e.global}? globalerr : fielderr"> < span th:text="${e.global}? '*' : ${e.fieldName}">The field name</ span > | < span th:text="${e.message}">The error message</ span > </ li > </ ul > |
233
参考链接
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#integrating-thymeleaf-with-spring
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix