Thymeleaf 入门记录

Just Use Thymeleaf

jsp 更好用的模板引擎,值得学习,盲目推崇前后端完全分离是错误的。

2020-7-17 20:57 前后文,标题,简介一点都不一致,大概后边就没动过手去写代码尝试用法了,主要是他的用法实在太多了一点,我要是每一个都去试一下一天时间肯定是不够的,而且我阅读英文的文档有很大的障碍,对于谷歌翻译来说一些计算机专业词汇翻译并不准确,需要我一个个单词去了解然后得出句意...本文只能算一个学习过程的记录,我也能说我知道了 Thymeleaf 的用法了,需要整理出一个可供查阅的 ”笔记“、”脑图“,我现在还在想怎么去整理用法。

项目起步

就现在这个 JAVA Web 生态,我觉得可以直接使用 Spring Boot搭建项目,以前那一套SSM已经过时了。Spring Boot会帮我们做好一切,我们直接使用即可, 无论是Spring MVC还是Thymeleaf,任何配置都不需要。首先来看看项目结构。

依赖介绍

  • 模板引擎 Thymeleaf ,注意使用 spring 官方整合的。
  • spring web mvc,作为一个 web 项目,这是不能少的。
  • lombok,这个可选,能够简化我们的Java Bean,学习阶段可以试试。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

启动类

很普通的一个 Spring Boot 启动类,我这是改了一下类名。

package io.xuqu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

控制器

这个 Controller 简直不要太简单了,我们没有配置任何的视图解析器,直接返回模板名即可,这就是Spring Boot

package io.xuqu.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/th")
public class HelloController {

    @GetMapping("/hello")
    public String sayHello() {
        return "index";
    }
}

模板

这就...非常纯正的 HTML 内容,当然,我们暂时只是把项目跑起来,所以很简单。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
</head>
<body>
    <h1>Hello, Thymeleaf!</h1>
</body>
</html>

效果

总结

我知道 Thymeleafjsp 好用很多,但是现在我只感觉 Spring Boot 是真的方便。在我最开始学 Spring Web MVC 的时候,首先需要手动配置 DispatcherServlet ,然后需要写注解配置类或者是 xml 配置文件扫描 Component ,然后你可能还需要手动配置 jsp 的视图解析器,就这个过程,当你完成了普通 Spring Web MVC 的配置过程,同样是写 Hello Spring Web 项目的我已经把项目跑完了...

当然,Spring Boot 不只是自动帮我们配置而已,它还留了一个配置文件给我们,需要改动什么就直接上手,比如:

server:
  port: 8021
  servlet:
    context-path: /jtx

spring:
  thymeleaf:
    enabled: true
    check-template: true
    mode: HTML
    prefix: classpath:/templates/
    suffix: .html
    encoding: UTF-8
    cache: true
    servlet:
      content-type: text/html

如果我需要修改端口号和上下文路径,仅仅只用改动几行,然后我们重启 Spring Boot 启动类即可。至于下面的 Thymeleaf 都是用的默认值,这里我相当于只是把它们列举出来方便查阅而已,足以体现 Spring Boot 是多么的方便,如果使用 IDEA 开发,这种配置文件都是有代码提示的,我甚至不用记住任何东西,靠开发工具就能解决所有配置。

简单入门

就让我半吊子水平直接看官方文档吧,没有什么看不看得懂的,你只要找到用法即可。

文字处理

不知道是我的问题还是怎么。我这边测试是加了 th:text 属性的标签肯定会覆盖标签原本的内容,而官网的文档每个使用了此属性的标签都有内容,到底是不是直接覆盖?我没有仔细查看,可能和 spring 整合的也有关系,这个文档已经两年没更新了.

静态配置文件

使用 # 可以读取配置文件的值,但是需要注意配置文件的名字:messages.properties

当然,这个静态配置文件官网说的很清楚了。但是我觉得并不重要,谁会用HTML读取配置文件?

Externalizing text is extracting fragments of template code out of template files so that they can be kept in separate files (typically .properties files) and that they can be easily replaced with equivalent texts written in other languages (a process called internationalization or simply i18n). Externalized fragments of text are usually called “messages”.

index.html

<h1 >Hello, <span th:text="#{index.name}"></span></h1>

messages.properties

index.name=Jack

需要注意的是,如果配置文件中没有这个属性,那么得到的将是如下:

Hello, ??index.name_zh_CN??

所以千万注意 classpath:messages.properties 是否存在,同时必须有 th:text="#{index.name}" 这个键值对。

未转义的文本

这个我感觉也用不到呀...和上面一样,如果配置文件找不到或者说没有键值对,最后也不会正常显示标签的内容啊,为什么官网的文档里面这些标签都写了内容?既然无论如何都不会显示,我直接空标签不好吗?难道是我做错了吗?

index.html

<p th:utext="#{index.welcome}"></p>

messages.properties

index.welcome=Welcome to our <b>fantastic</b> grocery store!

Variables

这应该才是我们用得比较多的:使用和显示变量,也是很简单的东西呀。当然,如果你没有往 Session 中存数据,页面是不会显示出来的,就算你在标签体里写了内容。这点和上面那种用法很详细,不过上面那个如果不存在是会显示一个很奇怪的东西,而这个用法不存在只会显示为空。

index.html

As you can see, we are still using the th:text attribute for the job (and that’s correct, because we want to replace the tag’s body), but the syntax is a little bit different this time and instead of a #{...} expression value, we are using a ${...} one. This is a variable expression, and it contains an expression in a language called OGNL (Object-Graph Navigation Language) that will be executed on the context variables map we talked about before.

<p>Today is: <span th:text="${today}"></span></p>

HelloController

这里也就是往 Session 存数据,具体的方法有很多,简单点就用 ModelAndView 吧。

@GetMapping("/today")
public ModelAndView getToday() {
    LocalDate now = LocalDate.now();
    var dateTimeFormatter = DateTimeFormatter.ofPattern("dd MMMM yyyy");

    ModelAndView index = new ModelAndView("index");
    index.addObject("today", dateTimeFormatter.format(now));
    return index;
}

文本

我们可以直接在 th:text 或者 th:utext 中写入文本,而不是表达式:

但是不能写 html 标签...而且只能是一个单词...额,看这里吧。

<div th:text="something"></div>
<div th:utext="something"></div>

标准表达语法

前面已经学了两个简单的表达式:#{}${},也就是信息和变量表达。其实 Thymeleaf 有很多表达式,而且可以有组合的方式,可以看一下文档的介绍。

  • Simple expressions:
    • Variable Expressions: ${...}
    • Selection Variable Expressions: *{...}
    • Message Expressions: #{...}
    • Link URL Expressions: @{...}
    • Fragment Expressions: ~{...}
  • Literals
    • Text literals: 'one text', 'Another one!',…
    • Number literals: 0, 34, 3.0, 12.3,…
    • Boolean literals: true, false
    • Null literal: null
    • Literal tokens: one, sometext, main,…
  • Text operations:
    • String concatenation: +
    • Literal substitutions: |The name is ${name}|
  • Arithmetic operations:
    • Binary operators: +, -, *, /, %
    • Minus sign (unary operator): -
  • Boolean operations:
    • Binary operators: and, or
    • Boolean negation (unary operator): !, not
  • Comparisons and equality:
    • Comparators: >, <, >=, <= (gt, lt, ge, le)
    • Equality operators: ==, != (eq, ne)
  • Conditional operators:
    • If-then: (if) ? (then)
    • If-then-else: (if) ? (then) : (else)
    • Default: (value) ?: (defaultvalue)
  • Special tokens:
    • No-Operation: _

All these features can be combined and nested:

'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))

Messages

我们可以在信息中添加参数,然后嵌套变量取值。

messages.properties

index.today=Message: Today is {0}

index.html

如果说从变量中取的值可能会被转义,那我们应该使用th:utext

<p th:text="#{index.today(${today})}"></p>

Variables

For detailed info about OGNL syntax and features, you should read the OGNL Language Guide

In Spring MVC-enabled applications OGNL will be replaced with SpringEL, but its syntax is very similar to that of OGNL(actually, exactly the same for most common cases).

/*
 * Access to properties using the point (.). Equivalent to calling property getters.
 */
${person.father.name}

/*
 * Access to properties can also be made by using brackets ([]) and writing 
 * the name of the property as a variable or between single quotes.
 */
${person['father']['name']}

/*
 * If the object is a map, both dot and bracket syntax will be equivalent to 
 * executing a call on its get(...) method.
 */
${countriesByCode.ES}
${personsByName['Stephen Zucchini'].age}

/*
 * Indexed access to arrays or collections is also performed with brackets, 
 * writing the index without quotes.
 */
${personsArray[0].name}

/*
 * Methods can be called, even with arguments.
 */
${person.createCompleteName()}
${person.createCompleteNameWithSeparator('-')}

我想说的是,别记它,用就完事了。当然,这种用法还是比较简单的,感觉和 python 语法有点像,简直和 jinja 一模一样...

表达式基本对象

  • #ctx: the context object.
  • #vars: the context variables.
  • #locale: the context locale.
  • #request: (only in Web Contexts) the HttpServletRequest object.
  • #response: (only in Web Contexts) the HttpServletResponse object.
  • #session: (only in Web Contexts) the HttpSession object.
  • #servletContext: (only in Web Contexts) the ServletContext object.

这些东西稍微需要记一下,首先从上到下基本是作用域递减的过程,比如说:

#ctx
#vars == #ctx.#vars
#locale == #vars.#locales == #ctx.#locale
...

来说,我们比较注重的应该在于 #request 以及 #session,如下:

#reqeust#httpServletRequest 是一样的,可以直接使用 #{request} 可能就是为了方便吧,然后这些对象是可以使用 java 方法的,比如说我要从 request 域中取出 today 的值,可以用:

<p th:text="${#httpServletRequest.getAttribute('today')}"></p>
<p th:text="${#request.getAttribute('today')}"></p>
<p th:text="${today}"></p>

那么一样的有:#session 等同于 #httpSession,但是还有一个 session 和他们不一样:

如果我们想要从 session 域中取数据,可以使用:

<div th:text="${session.today}"></div>
<div th:text="${session['today']}"></div>
<div th:text="${#session.getAttribute('today')}"></div>
<div th:text="${#httpSession.getAttribute('today')}"></div>

你甚至还可以使用 ${#response} 设置一些东西,比如说字符编码,比如状态码等等:

<div th:text="${#response.setStatus(404)}"></div>
<div th:text="${#response.getLocale()}"></div>
<div th:text="${#locale == #response.getLocale()}"></div>
<div th:text="${#response.getWriter().println('<h1>Hi!</h1>')}"></div>

表达式工具对象

这个就不做多介绍了,我也只是想入门而已,真要用比如直接把文档做参考工具书随时查阅开发...

Besides these basic objects, Thymeleaf will offer us a set of utility objects that will help us perform common tasks in our expressions.

  • #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 for java.util.Date objects: formatting, component extraction, etc.
  • #calendars: analogous to #dates, but for java.util.Calendar objects.
  • #numbers: methods for formatting numeric objects.
  • #strings: methods for String 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).
<div th:text="${#calendars.format(today, 'dd MMMM yyyy')}"></div>

选择表达式(星号语法)

官网的大概意思就是说,*{}${} 在没有选中 object 的时候是一样的,这个也很好理解。

<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>

等同于:

<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>
<div>
    <p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
    <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
    <p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div>

也可以混合使用:

<div th:object="${session.user}">
    <p>Name: <span th:text="*{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>

有选中的对象之后,可以通过 #{object}${} 使用:

<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>

Mix Summary

我看了上面那个变量的一些用法,这基本和 jsp 差不多呀,它还是算一种 java 类,不过只是用 html 的格式,怎么说呢,这东西比jsp 会更简单,也更容易开发,其中这些有关 servlet 的对象都是之前学过的,在这里面可以直接使用,大概可以理解为,我的Java控制器返回一个视图给另外一个 ‘类’ ,然后它再做处理之后才返回正常的html 数据给浏览器。

也就是前文说到的 @{}的使用

There are different types of URLs:

  • Absolute URLs: http://www.thymeleaf.org
  • Relative URLs, which can be:
    • Page-relative: user/login.html
    • Context-relative: /itemdetails?id=3 (context name in server will be added automatically)
    • Server-relative: ~/billing/processInvoice (allows calling URLs in another context (= application) in the same server.
    • Protocol-relative URLs: //code.jquery.com/jquery-2.0.3.min.js
<!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) -->
<a th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>

<!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) -->
<a th:href="@{/order/details(orderId=${o.id})}">view</a>

<!-- Will produce '/gtvg/order/3/details' (plus rewriting) -->
<a th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>

首先,官网在使用了 th:href 之外,还写了一个原生的 href,我尝试了一下,href是不会有任何作用的,就跟之前用 th:text 一样,会覆盖,而不是什么为空就作为默认值这种用法,我觉得那没有任何意义...

然后就是关于查询参数:(id=*,max=$),也就是?id=1&max=8...

参数使用 () 包裹起来,然后通过逗号分开,最后会自动解析成正常的。

还有一个是路径参数:/user/{id}(id=${id}),得到的是/user/3...

也就是说将原本是查询参数的值替换到了路径上,这个路径上的 {id} 就相当于一个变量。

需要注意的是:

使用 th:href 会在前面加上 context-path,而原生的 href 不会加上。

<!--  '/th/hello'  -->
<a href="/th/hello">hello</a>

<!--  '/jtx/th/hello'  -->
<a th:href="@{/th/hello}">hello</a>

当然,@{} 中也可以使用表达式:

<a th:href="@{${url}(orderId=${o.id})}">view</a>
<a th:href="@{'/details/'+${user.login}(orderId=${o.id})}">view</a>

Fragments

就是拆分模板。别处引用...

官网说,在那个布局的章节会详细介绍...写模板太累了,直接看官网的例子吧:

你可以使用 th:insert 或者 th:replace 来引用其它已经定义的模板

<div th:insert="~{commons :: main}">...</div>

还可以作为变量使用:

<div th:with="frag=~{footer :: #main/text()}">
    <p th:insert="${frag}">
</div>

Literals

  • th:text 或者 th:utext 中直接写入文字内容,单引号包含起来,如果文本中有单引号需要 \ 转义。

  • 同样可以在这里面写入数字计算...

  • 布尔类型

  • NULL

  • 不使用'',当然你也只能写入一个单词。

可是使用 th:if 判断加载 dom 与否,以及使用 th:class 指定类名。

<div th:utext="'<h1>Hello, I\'m Ben.</h1>'"></div>
<div th:text="1856287+114269"></div>
<div th:if="${#request.requestURL} != null" th:text="${#request.requestURL}"></div>
<div th:if="${#request.isSecure()} == true" th:text="${today}"></div>
<div th:class="red">RED?</div>

Appending texts

追加文本

<span th:text="'The name of the user is ' + ${user.name}">

Literal substitutions

文本替换

<span th:text="|Welcome to our application, ${user.name}!|">

等同于:

<span th:text="'Welcome to our application, ' + ${user.name} + '!'">

结合其它表达式:

Only variable/message expressions (${...}, *{...}, #{...}) are allowed inside |...| literal substitutions.

<span th:text="${onevar} + ' ' + |${twovar}, ${threevar}|">

Arithmetic operations

可以使用基本的加减乘除余:

Note that textual aliases exist for some of these operators: div (/), mod (%).

<div th:with="isEven=(${prodStat.count} % 2 == 0)">

等同于:

<div th:with="isEven=${prodStat.count % 2 == 0}">

Comparators and Equality

自然,也可以比较:

A simpler alternative may be using textual aliases that exist for some of these operators: gt (>), lt (<), ge (>=), le (<=), not (!). Also eq (==), neq/ne (!=).

对于整个三目运算符需要一个括号。

<div th:if="${prodStat.count} &gt; 1">
<span th:text="'Execution mode is ' + ( ${execMode} == 'dev' ? 'Development' : 'Production' )">

Conditional expressions

条件表达式,也就是上面那个三目运算符吧...

<tr th:class="${row.even} ? 'even' : 'odd'">
  ...
</tr>

同样可以整合其它表达式:

<tr th:class="${row.even} ? (${row.first} ? 'first' : 'even') : 'odd'">
  ...
</tr>

像这个表达式的话,如果 ${isDev}true 或者你 设置的是 "true",则其 class 将被设置为 dev ,否则无设置。

<div th:class="${isDev} ? 'dev'"></div>

Default expressions (Elvis operator)

....这个好像和三目运算符有点像,语法糖吧...简直了...

<span th:text="*{user.age} ?: '(no age specified)'"></span>

等价于:

<span th:text="*{age != null} ? *{age} : '(no age specified)'"></span>

The No-Operation token

这文档,为何不搞在一起呢?这个 _ 在 idea 中还报错了,虽然不影响运行....

<span th:text="${user.name} ?: 'no user authenticated'"></span>
<span th:text="${user.name} ?: _">no user authenticated</span>

Data Conversion / Formatting

这玩意....看看就好吧。ta 说在 ${}*{} 表达式可以使用双括号的方式进行格式转化,就是在写成 html 之前将原本的类型转成字符串,虽然我并不知道有什么意义,最后返回给浏览器的无论转不转换不都是字符串吗?

<td th:text="${{user.lastAccessDate}}">...</td>

Preprocessing

预处理,官方举例是国际化...我好像也没怎么看见过国内比较大的网站做国际化处理,很多都是直接开发几套的...

Messages_fr.properties

article.text=@myapp.translator.Translator@translateToFrench({0})

Messages_en.properties

article.text=@myapp.translator.Translator@translateToSpanish({0})

预处理表达式:

<p th:text="${__#{article.text('textVar')}__}"></p>

然后大概是通过 #locale 判断语言环境决定渲染成什么...may be...

<p th:text="${@myapp.translator.Translator@translateToFrench(textVar)}"></p>

设置属性值

Setting the value of any attribute

给一个标签设置任何属性的值

<form th:attr="action=@{/subscribe}">
    <fieldset>
        <input type="text" name="email" />
        <input type="submit" th:attr="value=#{subscribe.submit}"/>
    </fieldset>
</form>

等同于:

<form action="/gtvg/subscribe">
    <fieldset>
        <input type="text" name="email" />
        <input type="submit" value="¡Suscríbe!"/>
    </fieldset>
</form>

当然可以一次设置多个属性值:

<img 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" />

Setting value to specific attributes

上文说使用 th:attr 可以设置任何属性的值,但是这样代码不够美观?

<input type="submit" th:value="#{subscribe.submit}"/>
<form th:action="@{/subscribe}">
<a th:href="@{/product/list}">Product List</a>

详细可以参考官方文档:这里

Setting more than one value at a time

其实和上面的差不多,但是这个东西为了表示自己的方便,甚至搞出了一个同时设置多个属性值而且使用的th属性又有一定的含义

  • th:alt-title will set alt and title.
  • th:lang-xmllang will set lang and xml:lang.
<img th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

等同于:

<img th:src="@{/images/gtvglogo.png}" th:title="#{logo}" th:alt="#{logo}" />

也可以是这样...但是我觉得变得更加复杂了,不是很推荐:

<img th:src="@{/images/gtvglogo.png}" th:alt-title="#{logo}" />

Appending and prepending

Thymeleaf also offers the th:attrappend and th:attrprepend attributes, which append (suffix) or prepend (prefix) the result of their evaluation to the existing attribute values.

<input type="button" value="Do it!" class="btn" th:attrappend="class=${' ' + cssStyle}" />

如果设置的 cssStyle 是 "warning",你将得到:

<input type="button" value="Do it!" class="btn warning" />

他说,同样有两个特别的th 标签:大概就是添加class、style...

th:classappend and th:styleappend attributes, which are used for adding a CSS class or a fragment of style to an element without overwriting the existing ones:

<tr th:each="prod : ${prods}" class="row" th:classappend="${prodStat.odd} ? 'odd'">

Fixed-value boolean attributes

参考官方文档:这里

<input type="checkbox" name="option2" checked /> <!-- HTML -->
<input type="checkbox" name="option1" checked="checked" /> <!-- XHTML -->

你可以使用:

<input type="checkbox" name="active" th:checked="${user.active}" />

如果 $user.activeNULL|false|"false" 那么你将得到:

<input type="checkbox" name="active" />

其它情况包括:true、任意字符串、数字

<input type="checkbox" name="active" checked="checked" />

Setting the value of any attribute (default attribute processor)

上面都是设置一下 html 已有的标签属性值,这个是自定义

Thymeleaf offers a default attribute processor that allows us to set the value of any attribute, even if no specific th:* processor has been defined for it at the Standard Dialect.

<span th:whatever="${user.name}">...</span>

你将得到:

<span whatever="John Apricot">...</span>

Support for HTML5-friendly attribute and element names

这个我似乎在文档开头就有看到... 看个人选择...这种写法可以不引入 xmlns:th 的约束,但是 'idea' 就没有代码提示了。

<table>
    <tr data-th-each="user : ${users}">
        <td data-th-text="${user.login}">...</td>
        <td data-th-text="${user.name}">...</td>
    </tr>
</table>

迭代

基础

Using th:each

可以理解为 Java 中的列表增强 for ,注意迭代之后的标签是包含这个 th:each 的标签。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Good Thymes Virtual Grocery</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/gtvg.css}" />
    </head>
    <body>
        <h1>Product list</h1>
        <table>
            <tr>
                <th>NAME</th>
                <th>PRICE</th>
                <th>IN STOCK</th>
            </tr>
            <tr th:each="prod : ${prods}">
                <td th:text="${prod.name}"></td>
                <td th:text="${prod.price}"></td>
                <td th:text="${prod.inStock} ? #{true} : #{false}"></td>
            </tr>
        </table>
        <p><a th:href="@{/}">Return to home</a></p>
    </body>
</html>

Iterable values

官网说:java.utils.List 并不是为了的能够被迭代的...同时还有:

  • Any object implementing java.util.Iterable
  • Any object implementing java.util.Enumeration.
  • Any object implementing java.util.Iterator, whose values will be used as they are returned by the iterator, without the need to cache all values in memory.
  • Any object implementing java.util.Map. When iterating maps, iter variables will be of class java.util.Map.Entry.
  • Any array.
  • Any other object will be treated as if it were a single-valued list containing the object itself.

迭代状态

必备的吧... jsp 好像也有

Status variables are defined within a th:each attribute and contain the following data:

  • The current iteration index, starting with 0. This is the index property.
  • The current iteration index, starting with 1. This is the count property.
  • The total amount of elements in the iterated variable. This is the size property.
  • The iter variable for each iteration. This is the current property.
  • Whether the current iteration is even or odd. These are the even/odd boolean properties.
  • Whether the current iteration is the first one. This is the first boolean property.
  • Whether the current iteration is the last one. This is the last boolean property.
<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}"></td>
        <td th:text="${prod.price}"></td>
        <td th:text="${prod.inStock} ? #{true} : #{false}"></td>
    </tr>
</table>

首先,第一个 prod 代表迭代器中的元素,第二个 iterStat 是迭代状态变量,名字可以随意:

Optimizing through lazy retrieval of data

想不到需要使用的地方,有需要直接看官方文档吧,而且它这里把 th:each 写在一个标签上,然后在这个标签上使用迭代元素,恕我没法接受这种写法,我一直都有参照 vue.js 的语法学习,这种写法也太....

<ul th:if="${condition}">
    <li th:each="u : ${users}" th:text="${u.name}">user name</li>
</ul>

条件

Simple conditionals: “if” and “unless”

通过 th:if 可以决定是否加载 dom

<a th:href="@{/product/comments(prodId=${prod.id})}" 
   th:if="${not #lists.isEmpty(prod.comments)}">view</a>
</td>

这是令人比较恼火的规则,好好就 true 和 false 判断不好吗?

  • If value is not null:
    • 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.
  • (If value is null, th:if will evaluate to false).

当然,如果...则... 也可以是:除非...则不...

<a th:href="@{/comments(prodId=${prod.id})}" 
   th:unless="${#lists.isEmpty(prod.comments)}">view</a>

th:if 那里是添加了一个not 表否定,如果列表不为空则显示,这里是除非列表不为空则不显示....

Switch statements

现在就直接般官方文档了...我也只想让自己过一遍,每一个都去试一下感觉不太显示...

<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>

大概能清楚吧,如果 ${user.role} 的值是'admin',则显示:

<div><p>User is an administrator</p></div>

以此类推,其中 th:case="*" 代表默认值。

模板布局

这应该是重点吧...

Including template fragments

我看可以定义一个 组件,然后在其他处使用:

footer.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <body>
    <div th:fragment="copy">
      &copy; 2011 The Good Thymes Virtual Grocery
    </div>
  </body>
</html>

然后:

你可以使用:

  • th:insert
  • th:replace

虽然我完全不知道这到底有什么区别...

<body>
  <div th:insert="~{footer :: copy}"></div>
</body>

在表达式简单的情况下,这个也可以写出:

<body>
  <div th:insert="footer :: copy"></div>
</body>

Fragment specification syntax

Difference between th:insert and th:replace (and th:include)

其中 th:include 已经不推荐使用了...

<body>
    
  <div th:insert="footer :: copy"></div>
    
  <div th:replace="footer :: copy"></div>
    
  <div th:include="footer :: copy"></div>
    
</body>

你将得到:

<body>
    
  <div>
    <footer>
      &copy; 2011 The Good Thymes Virtual Grocery
    </footer>
  </div>
    
  <footer>
    &copy; 2011 The Good Thymes Virtual Grocery
  </footer>
    
  <div>
    &copy; 2011 The Good Thymes Virtual Grocery
  </div>
    
</body>

Parameterizable fragment signatures

比如我们先定义一个模板:

<div th:fragment="frag (onevar,twovar)">
    <p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>

然后使用它:

<div th:replace="::frag (${value1},${value2})">...</div>
<div th:replace="::frag (onevar=${value1},twovar=${value2})">...</div>

他说,第二种使用方法参数顺序并无要求:

<div th:replace="::frag (twovar=${value2},onevar=${value1})">...</div>

Fragment local variables without fragment arguments

模板没有定义参数,我也要传?

<div th:fragment="frag">
    ...
</div>

可以使用上面使用的第二种方法,也就是指定参数名:

<div th:replace="::frag (onevar=${value1},twovar=${value2})">

这等同于:

<div th:replace="::frag" th:with="onevar=${value1},twovar=${value2}">

th:assert for in-template assertions

这还在搞异常处理呢?我就奇了怪了,一个模板引擎需要这么牛逼?

<div th:assert="${onevar},(${twovar} != 43)">...</div>

他说这对于检验模板中的参数是否有效很有用:

<header th:fragment="contentheader(title)" th:assert="${!#strings.isEmpty(title)}">...</header>

Flexible layouts: beyond mere fragment insertion

这就是套娃。给一个模板参数也是一个模板,就这...

<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>

然后使用:

这里大致可以把 ~{::title} 理解为这个代码块里面的title 标签,而 ~{::link} 则是这个代码块中的所有的 link 标签

<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>

结果:

<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>

...

Using the empty fragment

使用空分段...

<head th:replace="base :: common_header(~{::title},~{})">

    <title>Awesome - Main</title>

</head>

结果就是:

Note how the second parameter of the fragment (links) is set to the empty fragment and therefore nothing is written for the <th:block th:replace="${links}" /> block:

<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>

</head>

Using the no-operation token

<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>

你将得到:

设置为_之后,模板中调用这个参数的代码将不会被执行,和上面的空分段有点类似...

<head>

    <title>The awesome application</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>

Advanced conditional insertion of fragments

删除模板,简单来说:官方文档,就是我们定义了一个模板之后,虽然它很好用,但是在没有渲染的时候直接用浏览器打开是看不出效果的,因为没有数据加载,所以有时候我们需要伪造一些片段作为模板以供浏览器直接访问查看,那么到真正调用的时候我们应该把这些伪造的代码片段给删除掉。

我们只需要给模板内伪造的标签上添加 th:remove 即可,在真正数据渲染的时候会删除这些,下面是规则:

And what does that all value in the attribute, mean? th:remove can behave in five different ways, depending on its value:

  • 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.

最后一个说的什么动态评估?我不太能理解,而且他举例是这样的:

<a href="/something" th:remove="${condition}? tag : none">Link text not to be removed</a>
<a href="/something" th:remove="${condition}? tag">Link text not to be removed</a>

继承布局

感觉和模板没有太多的区别...

<!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>

然后使用它:

<!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>

Then, You will get:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Page Title</title>
    </head>
    <body>
        <h1>Layout H1</h1>
        <section>
            <p>Page content</p>
            <div>Included on page</div>
        </section>
        <footer>
            Layout footer
        </footer>
    </body>
</html>

局部变量

在前面使用 th:each 的时候已经有局部变量的使用了:

<tr th:each="prod : ${prods}">
    ...
</tr>

这个 prod 就是局部变量,可以在这个<tr> 标签内的任何标签的 th:* 里面使用。

当然,你也可以通过 th:with 声明一个变量:

<div th:with="firstPer=${persons[0]}">
    <p>
        The name of the first person is <span th:text="${firstPer.name}">Julius Caesar</span>.
    </p>
</div>

你还可以这样:

通过逗号声明多个变量,而且可以在另外一个变量声明时使用已经声明的变量

<div th:with="company=${user.company + ' Co.'},account=${accounts[company]}">...</div>

属性优先级

就像这个迷惑的写法:

我想说的是,直接把 th:each 写在 ul 标签上不好吗?

<ul>
    <li th:each="item : ${items}" th:text="${item.description}">Item description here...</li>
</ul>

还是看一下优先级吧...

注释和块

Standard HTML/XML comments

可以直接使用 HTML 的注释方式

<!-- User info follows -->
<div th:text="${...}">
  ...
</div>

Thymeleaf parser-level comment blocks

HTML 那种注释是会返回给浏览器的,而这种注释中间的内容发布时会被删除

<!--/* This code will be removed at Thymeleaf parsing time! */-->

比如这样:

会删除那些在注释之间的标签,那是不是和 th:remove 有点类似呢?

<table>
    <tr th:each="x : ${xs}">
        ...
    </tr>
    <!--/*-->
    <tr>
        ...
    </tr>
    <tr>
        ...
    </tr>
    <!--*/-->
</table>

Thymeleaf prototype-only comment blocks

作为静态资源时被注释,发布时删除注释,有点挠呀。

<span>hello!</span>
<!--/*/
<div th:text="${...}">
...
</div>
/*/-->
<span>goodbye!</span>

And You Will Get:

<span>hello!</span>

<div th:text="${...}">
    ...
</div>

<span>goodbye!</span>

Synthetic th:block tag

这个也...简单来说,就是你可以在一个块当中写代码,然后模板渲染后这个块会被删除,中间渲染出的数据会保留

比如这个,th:block 有一个 th:each 属性,所以 Thymeleaf 会渲染这个数组,最后会得到中间的数据。而不像是div那样每个元素都被这个div所包含...

<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>

可以结合原型注释使用:

这样 'idea' 会报错,但是不妨碍程序正常运行。

<!--/*/<th:block th:each="i : ${users}" >/*/-->
<div th:text="${i}"></div>
<!--/*/</th:block>/*/-->

内联

表达式内联

<p>Hello, [[${session.user.name}]]!</p>

等价于:

我终于明白为什么要在标签内写东西了,只是为了作为静态资源浏览器直接访问结构清晰....

<p>Hello, <span th:text="${session.user.name}">Sebastian</span>!</p>

这东西还是有点用,和大部分的 vue.jsjinja 这种语法有点相似。

还有一个 [()] 也是内联,不过是不转义。

msg = 'This is <b>great!</b>'

<p>The message is "[(${msg})]"</p>
<p>The message is "This is <b>great!</b>"</p>
<p>The message is "[[${msg}]]"</p>
<p>The message is "This is &lt;b&gt;great!&lt;/b&gt;"</p>

Inlining vs natural templates

我们应该总是使用 th:text 属性,而不是为了方便用内联,因为在模型设计的时候,浏览器会显示内联的内容(即使没有渲染)

Disabling inlining

<p th:inline="none">A double array looks like this: [[1, 2, 3], [4, 5]]!</p>

You can get it:

<p>A double array looks like this: [[1, 2, 3], [4, 5]]!</p>

文本内联

Text inlining is very similar to the expression inlining capability we have just seen, but it actually adds more power. It has to be enabled explicitly with th:inline="text".

Text inlining not only allows us to use the same inlined expressions we just saw, but in fact processes tag bodies as if they were templates processed in the TEXT template mode, which allows us to perform text-based template logic (not only output expressions).

We will see more about this in the next chapter about the textual template modes.

JavaScript inlining

好,重点在下一章节...

<script th:inline="javascript">
    var username = [[${session.user.name}]];
    var username1 = [(${session.user.name})];
</script>

This will result in:

<script th:inline="javascript">
    var username = "Sebastian \"Fruity\" Applejuice";
    var username1 = Sebastian "Fruity" Applejuice; // it's wrong
</script>

JavaScript natural templates

也就是说上面那种使用方式不太正确,因为这个模板引擎原型设计的时候需要浏览器直接打开访问,这样不可避免会出现语法错误..

<script th:inline="javascript">
    var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
</script>

This will result in:

javascript 中注释就是:/* something */ ,这样就没有语法错误了,而且在渲染模板的时候 Thymeleaf 会帮我们把注释前后的内容去掉,所以我们才能得到这个结果。

<script th:inline="javascript">
    var username = "Sebastian \"Fruity\" Applejuice";
</script>

Advanced inlined evaluation and JavaScript serialization

就像这样。

An important thing to note regarding JavaScript inlining is that this expression evaluation is intelligent and not limited to Strings. Thymeleaf will correctly write in JavaScript syntax the following kinds of objects:

  • Strings
  • Numbers
  • Booleans
  • Arrays
  • Collections
  • Maps
  • Beans (objects with getter and setter methods)
<script th:inline="javascript">
    var user = /*[[${session.user}]]*/ null;
</script>
<script th:inline="javascript">
    var user = {"age":null,"firstName":"John","lastName":"Apricot",
                "name":"John Apricot","nationality":"Antarctica"};
</script>

CSS inlining

Just see example

classname = 'main elems'
align = 'center'
<style th:inline="css">
    .[[${classname}]] {
      text-align: [[${align}]];
    }
</style>

And the result would be:

那个 \ 是转义来的,但是我没看出怎么个转义法,空格?这到底是啥呢?

<style th:inline="css">
    .main\ elems {
      text-align: center;
    }
</style>

Advanced features: CSS natural templates, etc.

Like JavaScript

<style th:inline="css">
    .main\ elems {
      text-align: /*[[${align}]]*/ left;
    }
</style>

文本模板模式

Textual syntax

首先,这确实是一个很了不起的功能,但是我看下来自我感觉实用性不会很高...简单介绍一下吧,文字的模板模式和标签的模板模式是不同的,文字这边有:TEXT, JAVASCRIPT and CSS,然后标签:HTML, XML

然后就是这玩意用来做邮件模板很合适,如下:

如果没有复杂的逻辑表达式,这样就可以正常执行了。

Dear [(${name})],

Please find attached the results of the report you requested
with name "[(${report.name})]".

Sincerely,
The Reporter

如果像这样:

[#th:block th:each="item : ${items}"]
  - [#th:block th:utext="${item}" /]
[/th:block]

其中[#th:block ...] ... [/th:block] 可以被简化为:[# ...] ... [/]

等同于:

[# th:each="item : ${items}"]
  - [# th:utext="${item}" /]
[/]

再使用内联:

[# th:each="item : ${items}"]
  - [(${item})]
[/]

怎么说呢?这就是一个类似 XML 的文本语法...使用 [#...] [/] 也可以是 [#.../] (自闭合标签),比如说这个:

Dear [(${customer.name})],

This is the list of our products:

[# th:each="prod : ${products}"]
   - [(${prod.name})]. Price: [(${prod.price})] EUR/kg
[/]

Thanks,
  The Thymeleaf Shop

你将得到:

Dear Mary Ann Blueberry,

This is the list of our products:

   - Apricots. Price: 1.12 EUR/kg
   - Bananas. Price: 1.78 EUR/kg
   - Apples. Price: 0.85 EUR/kg
   - Watermelon. Price: 1.91 EUR/kg

Thanks,
  The Thymeleaf Shop

你甚至可以在 js 文件中使用这种语法,注意是 *.js 文件,而不是 <script> 标签,当然我也没试....

var greeter = function() {

    var username = [[${session.user.name}]];

    [# th:each="salut : ${salutations}"]    
      alert([[${salut}]] + " " + username);
    [/]

};

执行后,你将得到:

var greeter = function() {

    var username = "Bertrand \"Crunchy\" Pear";

      alert("Hello" + " " + username);
      alert("Ol\u00E1" + " " + username);
      alert("Hola" + " " + username);

};

Escaped element attributes

没看懂,额,去尝试了一下又懂了...先看图:

然后看浏览器效果:

大概就是说:当我们使用这种文字模板用到了大于小于这种符号应该要用转义符,不然会被浏览器解析成标签...

Extensibility

别说了,没看懂。

One of the advantages of this syntax is that it is just as extensible as the markup one. Developers can still define their own dialects with custom elements and attributes, apply a prefix to them (optionally), and then use them in textual template modes:

[#myorg:dosomething myorg:importantattr="211"]some text[/myorg:dosomething]

Textual prototype-only comment blocks: adding code

表面注释

var x = 23;

/*[+

var msg  = "This is a working application";

+]*/

var f = function() {
    ...

你将得到:

var x = 23;

var msg  = "This is a working application";

var f = function() {
...

所以你可以在里面使用内联表达式:

var x = 23;

/*[+

var msg  = "Hello, " + [[${session.user.name}]];

+]*/

var f = function() {
...

Textual parser-level comment blocks: removing code

表面存在,即虽然未被注释,但是执行模板渲染之后被删除

var x = 23;

/*[- */

var msg  = "This is shown only when executed statically!";

/* -]*/

var f = function() {
...

Or this, in TEXT mode:

...
/*[- Note the user is obtained from the session, which must exist -]*/
Welcome [(${session.user.name})]!
...

Natural JavaScript and CSS templates

OK,就和内联那一节是一样的:

/*[# th:if="${user.admin}"]*/
	alert('Welcome admin');
/*[/]*/

执行后实际为:

[# th:if="${user.admin}"]
	alert('Welcome admin');
[/]

这一章节有点无语....这是要开辟一个新标记语言?

到现在,基本的语法已经差不多讲完了。

More And More

作为一个 Spring Boot 用户我完全没必要看这些配置.。。。

Template Cache

More on Configuration

Decoupled Template Logic

Last Summary

大概花了一天的时间把这个文档读了一下,其实这种东西很简单(虽然我还是有没搞懂的...),而且用法也比较复杂,为了支持很多东西, Thymeleaf 变得很强大也比较复杂了,特别是最后的 Textual template modes,其实平常是用不到的,可以选择性的学习,另外就是这种查阅学习方法其实并不怎么好,虽然我记了这么多东西,但是绝大部分都是直接照搬文档的。没有什么说服力,我需要做的是重新整理,直接得出用法,不需要任何介绍的一个工具参考文档,而不是说明。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://blog.imoyb.com/archives/simple-use-thymeleaf