Вступление
При разработке веб-приложений важно выбрать, какой движок будет заботиться о слое представления.
Страницы сервера Java (JSP) были очень популярны, хотя накладные расходы и затраты времени были одними из основных недостатков их использования. Они потребовали внесения значительных изменений в HTML-код страниц.
В настоящее время Thymeleaf широко применяется и используется в качестве механизма создания шаблонов для приложений Spring / MVC. Его также можно использовать для создания шаблонов электронной почты в формате HTML. В то время как JSP компилируются в классы сервлетов Java, Thymeleaf анализирует простые файлы шаблонов HTML. На основе выражений, присутствующих в файле, он генерирует статическое содержимое. Он способен обрабатывать HTML, XML, JS, CSS и т. Д.
Стандартные диалекты тимелеафа
Thymeleaf предоставляет широкий спектр процессоров атрибутов из коробки как часть своих стандартных диалектов . Этих процессоров достаточно для большинства типичных шаблонов. Тем не менее, вы также можете расширить их, чтобы в случае необходимости создавать собственные процессоры атрибутов.
Давайте посмотрим на самый важный сегмент диалекта - стандартные функции выражения . Вот некоторые из выражений, которые вы будете использовать довольно часто:
- Выражения переменных:
${...}
- Выражения переменных выбора:
*{...}
- Выражения сообщений:
#{...}
- Выражения URL ссылки:
@{...}
- Выражения фрагментов:
~{...}
Вот некоторые литералы, которые вы, вероятно, будете использовать:
- Текстовые литералы:
'hello world'
,'Welcome to stackabuse'
,… - Числовые литералы:
0
,123
,67.90
,… - Логические литералы:
true
,false
- Нулевой литерал:
null
Основные операции:
-
Конкатенация строк:
+
-
Буквальные замены:
|Welcome to ${city}|
-
Бинарные операторы:
+
,-
,*
,/
, `% -
Бинарные операторы:
and
,or
-
Логическое отрицание (унарный оператор)
!
not
Сравнения:
- Компараторы:
>
,<
,>=
,<=
(gt
,lt
,ge
,le
) - Операторы равенства:
==
!=
(eq
,ne
)
Условные:
- Если-то:
(if) ? (then)
- Если-то-еще:
(if) ? (then) : (else)
- По умолчанию:
(value) ?: (defaultvalue)
Все эти выражения можно использовать в сочетании друг с другом для получения желаемых результатов.
Зависимость от тимелеафа
Самый простой способ начать работу с Thymleaf через Maven - это включить зависимость:
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>${version}</version>
</dependency>
Или, если вы используете Gradle:
compile group: 'org.thymeleaf', name: 'thymeleaf', version: '${version}'
Механизм шаблонов и преобразователи шаблонов
Для Thymeleaf преобразователь шаблонов отвечает за загрузку шаблонов из заданного места, а механизм шаблонов отвечает за их обработку для заданного контекста. Нам нужно будет настроить оба в классе конфигурации:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public ClassLoaderTemplateResolver templateResolver() {
ClassLoaderTemplateResolver templateResolver =
new ClassLoaderTemplateResolver();
templateResolver.setPrefix("/templates/");
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
return templateEngine;
}
}
Здесь мы templateResolver
и установили его префикс и суффикс.
Представления будут располагаться в /templates
и оканчиваться на
.html
.
После этого мы настроили templateEngine
, просто установив
преобразователь и вернув его.
Давайте проверим, работает ли он, попробовав обработать сообщение:
StringWriter writer = new StringWriter();
Context context = new Context();
TemplateEngine templateEngine = templateEngine();
context.setVariable("message", "Welcome to thymeleaf article");
templateEngine.process("myTemplate", context, writer);
LOG.info(writer.toString());
Движок используется для обработки myTemplate.html
, расположенного в
src/main/resources/templates
. /resources
является каталогом по
умолчанию. Переменная передается в context
, что позволяет нам
ссылаться на нее в самом шаблоне:
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-3.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<body>
<h1 th:text="${message}"></h1>
</body>
</html>
th:text
будет оценивать значение этого message
и вставлять его в
тело тега, в котором оно находится. В нашем случае тело <h1>
:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<h1>Welcome to thymeleaf article</h1>
</body>
</html>
Работает отлично! Давайте продолжим и настроим ViewResolver
чтобы мы
могли заполнять представления через контроллеры, а не жестко кодировать
значения в контексте.
Просмотр Resolver
Прямо под другой конфигурацией давайте ViewResolver
. Он сопоставляет
имена представлений с фактическими представлениями. Это позволяет нам
просто ссылаться на представления в контроллерах, а не на значения
жесткого кодирования:
@Bean
public ViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
viewResolver.setCharacterEncoding("UTF-8");
return viewResolver;
}
Отображение атрибутов модели
Основное использование большинства движков, таких как Thymeleaf, - это отображение определенных свойств / атрибутов моделей. Давайте создадим обработчик запроса, который возвращает объект с парой установленных полей:
@GetMapping("/article")
public ModelAndView getArticle(ModelAndView modelAndView) {
Article article = new Article();
article.setAuthor(getName());
article.setContent(getArticleContent());
article.setTitle(getTitle());
modelAndView.addObject("article", article);
modelAndView.setViewName("articleView");
return modelAndView;
}
Обработчик отправляет обратно представление с именем articleView
и
объект с именем article
. Эти двое теперь взаимосвязаны. Мы можем
получить доступ к article
на странице articleView
Это похоже на то,
как мы в прошлый раз message
Context
Давайте посмотрим, как мы можем получить доступ к объекту и отобразить его значения на странице:
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-3.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<link th:href="@{/css/app.css}" rel="stylesheet"/>
<body class='typora-export os-windows'>
<div id='write' class='is-node'>
<h1 th:text="${article.title}">Article title</h1>
<h4 th:text="${article.author}">Author name</h4>
<p th:text="${article.content}">contetnt</p></div>
</body>
</html>
Используя переменное выражение, ${...}
, мы ссылаемся на article
и
соответственно вставляем поля в атрибуты th:text
Вот как будет
выглядеть отрисованная страница:
{.ezlazyload}
Примечание. Если у тега есть тело, th:text
заменит его. Если
значение отсутствует или есть проблемы с его отображением, вместо него
будет использоваться тело.
Локальные переменные
Локальные переменные в Thymeleaf очень удобны. Локальные переменные определяются в определенном фрагменте шаблона. Они доступны только в рамках определяющего фрагмента.
С локальными переменными мы избавляемся от необходимости делать все в контроллере и выполнять операции на самой странице. Давайте взглянем:
<tr th:each="article : ${articles}">
<td th:text="${article.name}">name</td>
<td th:text="${article.author}">author</td>
<td th:text="${article.description">description</td>
</tr>
Здесь article
- это локальная переменная. Он представляет собой
article
объект из articles
списка. Мы не можем ссылаться на
article
за пределами таблицы HTML.
article
не передавалась контроллером - она была определена на самой
странице. th:each
будет назначать новые значения article
при каждом
проходе списка.
Это выглядело бы примерно так:
{.ezlazyload}
Другой способ определить локальные переменные - через атрибут th:with
<div th:with="article=${articles[0]}">
<p>
This article is writen by <span th:text="${article.author}">John Doe</span>.
</p>
</div>
Здесь мы определили переменную через th:with
как первый элемент
списка, переданного контроллером. Мы можем ссылаться на эту переменную
из <div>
она определена.
Точно так же мы можем определить несколько переменных с помощью одного
атрибута th:with
<div th:with="article=${articles[0]}, category=${categories[1]}">
<p>
This article is writen by <span th:text="${article.author}">John Doe</span>.
</p>
<p>
Category <span th:text="${category.name}">John Doe</span>.
</p>
</div>
Мы также можем использовать эти локальные переменные для выполнения манипуляций с данными или их извлечения, чтобы уменьшить количество обращений к контроллерам:
<div th:with="article=${articles[0]}, author=${authors[article.author]}">
</div>
Обратите внимание , что мы используем article
переменные , чтобы
получить author
детали из карты автора. Это позволяет нам повторно
использовать переменную в том же атрибуте.
Кроме того, теперь нам больше не нужно зависеть от контроллера, чтобы делиться данными об авторе для каждой статьи, мы можем просто передать список авторов помимо списка статей:
@GetMapping("/articles")
public ModelAndView getArticles(ModelAndView modelAndView) {
modelAndView.addObject("articles", getArticles());
modelAndView.addObject("authors", getAuthors());
modelAndView.setViewName("articles");
return modelAndView;
}
Вам не нужно устанавливать привязанные к объектам локальные переменные. Вы можете так же легко использовать строковые литералы или числа:
<div th:with="name = 'John', age = 25}">
<p> Hello, <span th:text="${name}"></span>!</p>
</div>
Выражения переменных выбора
Здесь стоит отметить выражения переменных выбора . Давайте посмотрим, как они работают:
<div th:object="${article}">
<td th:text="*{name}">name</td>
<td th:text="*{author}">author</td>
<td th:text="*{description">description</td>
</tr>
Вместо написания ${article.name}
, ${article.author}
и т. Д. Мы
можем просто указать выражение *{...}
th:object
определяет, к какому
объекту принадлежат указанные поля.
Создание форм и входов
Работа с формами происходит часто, и это один из основных способов отправки информации на наш сервер. Thymeleaf предоставляет различные атрибуты для создания и обработки отправленных форм.
- th:action
атрибут заменяет HTML action
атрибут <form>
.
th:object
используется для привязки полей формы к объекту. Это похоже
на modelAttribute
или commandName
вы обычно используете с JSP.
Давайте посмотрим на определение формы:
<form th:action="@{/article}" th:object="${article}" method="post">
</form>
Здесь через выражение ссылки форма отправляет запрос POST на URL-адрес
/article
Связанный объект - это article
. Теперь нам нужно ввести
несколько полей для ввода, чтобы фактически заполнить информацию о
article
:
<form th:action="@{/article}" th:object="${article}" method="post">
<div class='is-node custom-form'>
<label>Title:</label>
<input type="text" th:field="*{title}"/>
</div>
<div class='is-node custom-form'>
<label>Content:</label>
<textarea th:field="*{content}"/>
</div>
</form>
Мы связали article
с этой формой, поэтому упомянутый title
и
content
принадлежат ей.
Теперь, когда пользователь вводит контент в эти поля, мы захотим
обработать его и сохранить в базе данных. Давайте /form
который
сначала отобразит форму на странице:
@GetMapping("/form")
public ModelAndView getArticleForm(ModelAndView modelAndView) {
Article article = new Article();
modelAndView.addObject("article", article);
modelAndView.setViewName("articleForm");
return modelAndView;
}
{.ezlazyload}
Мы должны добавить в форму пустой article
, иначе th:object
будет
недопустимым. Теперь давайте создадим обработчик запроса POST, который
попадает в форму:
@PostMapping("/article")
public String saveArticle(@ModelAttribute Article article) {
articleService.saveArticle(article);
return "articles";
}
Здесь @ModelAttribute
связывает полученную модель с обрабатывающим ее
объектом. Все это упаковано в article
который затем сохраняется с
помощью классической службы, расширяющей CrudRepository
.
Хотя такой рудиментарной формы часто бывает недостаточно. Давайте посмотрим, как мы можем добавить переключатели, флажки, раскрывающиеся меню и т. Д.
Радио-кнопки
Чтобы добавить переключатель, мы должны создать классический <input>
и
определить его тип через HTML. Задача Thymeleaf - привязать поле и
значение этого переключателя к th:object
формы:
<form th:action="@{/article}" th:object="${article}" method="post">
<div>
<label>Select a Category:</label>
<div th:each="category : ${categories}">
<input type="radio" th:field="*{category}" th:value="${category}" />
<label th:for="${#ids.prev('category')}" th:text="${category}"></label>
</div>
</div>
</form>
После рендеринга это будет выглядеть примерно так:
{.ezlazyload}
Флажки
Флажки работают точно так же:
<form th:action="@{/article}" th:object="${article}" method="post">
<div class='is-node custom-form'>
<label>Select Areas:</label>
<div th:each="area : ${areas}">
<input type="checkbox" th:field="*{area}" th:value="${area}"/>
<label th:for="${#ids.prev('area')}" th:text="${area}"></label>
</div>
</div>
</form>
Это выглядело бы так:
{.ezlazyload}
Меню опций
И, наконец, давайте посмотрим, как мы можем добавить несколько опций:
<form th:action="@{/article}" th:object="${article}" method="post">
<div class='is-node custom-form'>
<label>Select a Technology:</label>
<select th:field="*{technology}">
<option th:each="technology : ${technologies}" th:value="${technology}"
th:text="${technology}">
</option>
</select>
</div>
</form>
Обычно варианты представлены списком. В этом случае мы создали
<option>
для каждой technology
в списке и присвоили technology
может видеть пользователь.
Это выглядело бы примерно так:
{.ezlazyload}
Условные утверждения
Веб-сайты не статичны. В зависимости от определенных оценок элементы отображаются, скрываются, заменяются или настраиваются. Например, мы можем выбрать отображение сообщения вместо таблицы, если в базе данных нет строк.
Давайте посмотрим на некоторые основные условные операторы в Thymeleaf:
<body>
<table th:if="${not #list.isEmpty(articles)}">
<tr>
<th>Name</th>
<th>Author</th>
<th>Description</th>
<th>Category</th>
<th>Date</th>
</tr>
<tr th:each="article : ${articles}">
<td th:text="${article.name}">name</td>
<td th:text="${article.author}">author</td>
<td th:text="${article.description">description</td>
<td th:text="${article.category}">category</td>
<td th:text="${article.date}">date</td>
</tr>
</table>
<div th:if="${#lists.isEmpty(kv)}">
<h2>No data found</h2>
</div>
</body>
th:if
используется как обычный оператор if
Если articles
не
пустой, мы заполняем таблицу - если она пуста, мы выводим сообщение.
Здесь #list
- служебный объект, используемый для выполнения
вспомогательных методов в коллекциях.
Кроме того, у нас также могут быть операторы th:switch
и th:case
.
Они довольно просты:
<div>
<td th:switch="${article.category}">
<span th:case="'TECHNOLOGY'" th:text="Technical Articles"/>
<span th:case="'FASHION'" th:text="About latest fashion trends"/>
<span th:case="'FOOD'" th:text="Are you hungry..."/>
</td>
</div>
Отображается только соответствующий регистр.
Внешний текст для интернационализации
По умолчанию Thymeleaf поддерживает интернационализацию. Создайте
myTemplate.properties
в том же каталоге, что и ваши шаблоны.
Создадим сообщение и присвоим ему значение:
welcome.message=Welcome to Stack Abuse
Теперь в любом шаблоне мы можем ссылаться на значение, вызывая
welcome.message
с выражением сообщения :
<body>
<h1 th:text="#{welcome.message}"></h1>
</body>
myTemplate_de.properties
создайте больше файлов, например
myTemplate_de.properties. При создании контекста для шаблона в исходной
настройке просто передайте ему локаль:
Context context = new Context(Locale.GERMAN);
Фрагменты и макеты
Некоторые вещи на странице не сильно меняются во всем интерфейсе. А именно, верхний и нижний колонтитулы обычно идентичны. Кроме того, как только они будут изменены / обновлены, вам нужно будет перейти на каждую страницу и обновить код там.
Этот шаблонный код можно использовать повторно и просто ссылаться на него на каждой странице. Thymeleaf предлагает нам фрагменты , которые представляют собой отдельные файлы, которые вы можете вставить в другой файл. Создадим фрагмент заголовка и включим его в другой шаблон:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="header_fragment">
<h1>Welcome to Stack Abuse</h1>
</div>
</body>
</html>
Мы сохраним этот файл с именем header.html
в том же каталоге, что и
другие шаблоны. Хотя многие сохраняют их в подкаталоге, называемом
fragments
.
Теперь мы захотим включить этот заголовок на другую страницу. Обратите
внимание, что это не будет включать весь файл . Просто <div>
мы
пометили как th:fragment
. Поместим этот заголовок над нашим
приветственным сообщением:
<body>
<div id="holder" th:insert="header :: header_fragment"></div>
<h1 th:text="#{welcome.message}"></h1>
</body>
Когда мы визуализируем этот файл, HTML-страница будет выглядеть так:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<div id="holder">
<div>
<h1>Welcome to Stack Abuse Article</h1>
</div>
</div
<h1>Welcome to world</h1>
</body>
</html>
Теперь есть три способа включить фрагменты: th:insert
, th:replace
и
th:include
.
th:insert
добавляет фрагмент как дочерний узел внутри включающего
тега. Как мы видим в приведенном выше примере, фрагмент заголовка
вставляется в <div>
с идентификатором holder
th:replace
заменит текущий тег на фрагмент:
<body>
<div id="holder" th:replace="header :: header_fragment"></div>
<h1 th:text="#{welcome.message}"></h1>
</body>
Это будет выглядеть как:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<div>
<h1>Welcome to Stack Abuse Article</h1>
</div>
<h1>Welcome to world</h1>
</body>
</html>
<div>
с идентификатором holder
теперь заменяется фрагментом.
th:include
является предшественником th:replace
и работает точно так
же. Теперь это устарело.
Обработка ошибок и сообщений об ошибках
Обработка ошибок - очень важный аспект веб-приложений. Когда что-то не так, мы хотим помочь пользователю исправить пользовательские проблемы, например неправильную отправку формы.
Для простоты мы будем использовать javax.validations
для проверки
полей отправки формы:
@PostMapping("/article")
public String saveArticle(@ModelAttribute @Valid Article article, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "articleForm";
}
articleService.saveArticle(article);
return "redirect:articles";
}
Это классический обработчик отправки формы. Мы упаковали информацию в
article
и сохранили ее в базе данных. Однако на этот раз мы пометили
article
как @Valid
и добавили проверку для экземпляра
BindingResult
@Valid
гарантирует, что полученная и упакованная информация об объекте
соответствует проверкам, которые мы установили в модели Article
public class Article {
@NotNull
@Size(min = 2, max = 30)
private String title;
private String author;
@NotNull
@Size(min = 2, max = 1000)
private String content;
private String category;
private String technology;
private String area;
}
Если есть какие-либо нарушения этих правил, bindingResults.hasErrors()
вернет true
. И таким образом возвращаем форму обратно. вместо
перенаправления пользователя на страницу /articles
Ошибки будут отображаться в форме в обозначенных местах, которые мы
установили с помощью th:errors
:
<form th:action="@{/article}" th:object="${article}" method="post">
<div class='is-node custom-form'>
<label>Title:</label>
<input type="text" th:field="*{title}"/>
<span class="field-error" th:if="${#fields.hasErrors('title')}" th:errors="*{title}">Name Error</span>
</div>
<div class='is-node custom-form'>
<label>Content:</label>
<textarea th:field="*{content}"/>
<span class="field-error" th:if="${#fields.hasErrors('content')}" th:errors="*{content}">Name Error</span>
</div>
</form>
Используя пару условных #fields.hasErrors()
, мы можем сообщить
пользователю, что не так с проверками, и вежливо попросить пересмотреть
предоставленную информацию.
Вот как будет выглядеть отрисованная страница:
{.ezlazyload}
В качестве альтернативы мы также можем сгруппировать все ошибки вместе,
используя подстановочный знак или all
:
<li class="field-error" th:each="error : ${#fields.errors('*')}" th:text="${error}" />
<li class="field-error" th:each="error : ${#fields.errors('all')}" th:text="${error}" />
{.ezlazyload}
Заключение
Эта статья предназначена для ознакомления с Thymeleaf, очень популярным современным механизмом создания шаблонов для приложений Java / Spring.
Хотя мы не сделали глубокого погружения в движок, который является довольно обширным, описанного материала должно быть более чем достаточно, чтобы вы начали с хорошей основы для более продвинутых функций.