- Зачем нужны шаблонизаторы
- Как начать работать с JTE
- Как работает отображение данных
- Какие управляющие конструкции используются в jte
В этом уроке мы познакомимся с шаблонизаторами.
Зачем нужны шаблонизаторы
Javalin и другие подобные фреймворки позволяют создавать полноценные сайты без дополнительных инструментов. Рабочий сайт должен просто возвращать ответ с HTML-кодом, который браузер отобразит как страницу:
package org.example.hexlet;
import io.javalin.Javalin;
public class HelloWorld {
public static void main(String[] args) {
var app = Javalin.create(config -> {
config.bundledPlugins.enableDevLogging();
});
// Название параметров мы выбрали произвольно
app.get("/courses/{id}", ctx -> {
var id = ctx.pathParam("id");
var course = /* Курс извлекается из базы данных */
// Предполагаем, что у курса есть метод getName()
ctx.result("<h1>" + course.getName() + "</h1>");
});
app.start(7070);
}
}
Здесь мы видим упрощенный пример, в котором мы возвращаем только заголовок H1.
В реальных приложениях возвращаемый HTML состоит из сотен и тысяч строк. Работать с такими объемами стандартным способом очень сложно. Вот лишь некоторые проблемы, с которыми мы столкнемся:
- Такой код сложно формировать, редактировать и поддерживать
- В таком коде очень легко допустить ошибку и очень сложно ее обнаружить
- В таком коде будут возникать проблемы с одинарными или двойными кавычками, придется их экранировать и постоянно следить за этим
Для решения этой проблемы используются шаблонизаторы. Это библиотеки, которые позволяют формировать HTML в отдельных файлах с подсветкой и удобной подстановкой данных. Другими словами, мы получаем не HTML внутри кода, а код внутри HTML.
В Java-мире есть несколько разных шаблонизаторов. Для этого курса мы выбрали jte (Java Template Engine), потому что он официально поддерживается в Javalin.
Посмотрим, как выглядит HTML c jte:
@import org.example.Page
@param Page page
<html>
<head>
@if(page.getDescription() != null)
<meta name="description" content="${page.getDescription()}" />
@endif
<title>${page.getTitle()}</title>
</head>
<body>
<h1>${page.getTitle()}</h1>
<p>Welcome to my example page!</p>
</body>
</html>
Как начать работать с JTE
Чтобы начать работу с jte, нужно добавить в зависимости две строчки:
// Шаблонизатор и его интеграция с Javalin
implementation("gg.jte:jte:3.1.9")
implementation("io.javalin:javalin-rendering:6.1.3")
Пакет javalin-rendering добавляет в Javalin поддержку нескольких популярных шаблонизаторов, в том числе jte. В конфигурации приложения нужно указать, что мы хотим использовать jte в качестве шаблонизатора в нашем приложении:
import io.javalin.rendering.template.JavalinJte;
var app = Javalin.create(config -> {
// ...
config.fileRenderer(new JavalinJte());
});
Шаблоны JTE с HTML хранятся в директории src/main/jte и имеют расширение jte. Чтобы попрактиковаться, добавим первый шаблон для главной страницы сайта. Для этого выполним два действия:
Создадим шаблон src/main/jte/index.jte со следующим содержимым:
<!-- Изменения HTML в шаблоне не требуют перезапуска сервера --> <!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Hello Hexlet!</title> </head> <body> <div class="col-lg-8 mx-auto p-4 py-md-5"> <main> <h1 class="text-body-emphasis">Привет, Хекслет!</h1> <p>Javalin + jte</p> </main> </div> </body> </html>
Укажем обработчику главной страницы использовать этот шаблон:
app.get("/", ctx -> ctx.render("index.jte"));
Обратите внимание на метод ctx.render()
в коде выше. Он выполняет рендеринг указанного шаблона и добавляет результат в HTTP-ответ.
Путь до шаблона указывается относительно директории src/main/jte:
# Например, структура может выглядеть так
jte
├── .jteroot
├── courses
│ ├── edit.jte
│ ├── index.jte
│ ├── build.jte
│ └── show.jte
├── index.jte
└── users
├── edit.jte
├── index.jte
└── build.jte
Сами шаблоны могут располагаться и на более глубоком уровне. Это становится важно, когда количество шаблонов увеличивается.
Шаблонизатор не задает правила именования и внутренней структуры шаблонов. Но работать без правил слишком сложно, поэтому со временем мы самостоятельно выработаем правила и будем их придерживаться.
В директории src/main/jte помимо самих шаблонов может быть расположен файл .jteroot. Это просто пустой файл, который указывает JTE, где находится корневая директория с шаблонами. Хотя в Javalin наличие этого файла не является обязательным, так как шаблонизатор уже заранее сконфигурирован фреймворком для поиска шаблонов в директории src/main/jte, мы рекомендуем его создавать. Этот файл помогает IntelliJ IDEA точно определять расположение шаблонов и корректно подсвечивать синтаксис, что в свою очередь упрощает работу с проектом.
Как работает отображение данных
Как правило, HTML внутри шаблонов формируется на основе данных, которые мы хотим вывести. Например, чтобы вывести информацию о курсе на странице /courses/{id}, мы передаем объект этого курса в шаблон и формируем HTML на основе его содержимого.
Попробуем проделать этот путь. Создадим класс для курса с тремя полями — идентификатором, названием и описанием:
// Путь src/org/example/hexlet/model/Course.java
package org.example.hexlet.model;
@Getter
@Setter
@ToString
public final class Course {
private Long id;
@ToString.Include
private String name;
private String description;
public Course(String name, String description) {
this.name = name;
this.description = description;
}
}
По идее, обработчик маршрута /courses/{id} получает курс из базы данных и передает его в шаблон. Для этого создадим дата-класс:
// Путь src/org/example/hexlet/dto/courses/CoursePage.java
package org.example.hexlet.dto.courses;
import org.example.hexlet.model.Course;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class CoursePage {
private Course course;
}
Осталось написать обработчик и посмотреть, как все это работает в связке:
package org.example.hexlet;
import io.javalin.Javalin;
import io.javalin.rendering.template.JavalinJte;
import static io.javalin.rendering.template.TemplateUtil.model;
import org.example.hexlet.model.Course;
import org.example.hexlet.dto.courses.CoursePage;
public class HelloWorld {
public static void main(String[] args) {
var app = Javalin.create(config -> {
config.bundledPlugins.enableDevLogging();
config.fileRenderer(new JavalinJte());
});
app.get("/courses/{id}", ctx -> {
var id = ctx.pathParam("id");
var course = /* Курс извлекается из базы данных. Как работать с базами данных мы разберем в следующих уроках */
var page = new CoursePage(course);
ctx.render("courses/show.jte", model("page", page));
});
app.start(7070);
}
}
Рассмотрим, по какому алгоритму обработчики обычно работают с шаблонами:
- Сначала они извлекают все необходимые данные
- Затем они создают объект дата-класса и заполняют его этими данными
- В итоге они передают этот объект в шаблон в виде Map, созданного при помощи статического метода
model()
, который предоставляет Javalin
Остался последний шаг — вывести данные курса в шаблоне. Для этого в шаблонах используется специальный синтаксис. Через него мы указываем, какой дата-класс мы используем и под каким именем хотим передать его объект в шаблон. В нашем случае это объект класса CoursePage
, переданный под именем page
:
<!-- src/main/jte/courses/show.jte -->
@import org.example.hexlet.dto.courses.CoursePage
@param CoursePage page
<!-- Для наглядности мы не стали включать сюда основную структуру HTML -->
<main>
<h1>${page.getCourse().getName()}</h1>
<p>${page.getCourse().getDescription()}</p>
</main>
В первых двух строчках шаблона мы используем директивы — специальные конструкции, которые начинаются со знака @
:
- Директива
@import
аналогична импорту в Java - Директива
@param
указывает, какие данные нужно использовать внутри Map, переданного в шаблон. Доступ к данным внутри шаблона идет через это имя
Дальше мы видим подстановку данных внутрь HTML. Этот способ называется интерполяцией. Он работает через указание Java-кода внутри структуры ${}
.
Какие управляющие конструкции используются в jte
Кроме обычного вывода значений, в шаблонах часто используются условные конструкции и циклы. С их помощью показываются списки, прячутся или показываются определенные блоки и так далее.
Подробнее обо всех этих конструкциях можно прочитать в официальной документации. А здесь мы разберем пример на базе маршрута /courses
, по которому выводится список курсов с описанием и ссылками на курсы:
Начнем с дата-класса. Кроме курсов, добавим еще и заголовок для разнообразия:
// Путь src/org/example/hexlet/dto/courses/CoursesPage.java package org.example.hexlet.dto.courses; import java.util.List; import org.example.hexlet.model.Course; import lombok.AllArgsConstructor; import lombok.Getter; @AllArgsConstructor @Getter public class CoursesPage { private List<Course> courses; private String header; }
Перейдем к обработчику:
package org.example.hexlet; import io.javalin.Javalin; import io.javalin.rendering.template.JavalinJte; import static io.javalin.rendering.template.TemplateUtil.model; import org.example.hexlet.model.Course; import org.example.hexlet.dto.courses.CoursesPage; public class HelloWorld { public static void main(String[] args) { var app = Javalin.create(config -> { config.bundledPlugins.enableDevLogging(); config.fileRenderer(new JavalinJte()); }); app.get("/courses", ctx -> { var courses = /* Список курсов извлекается из базы данных */ var header = "Курсы по программированию"; var page = new CoursesPage(courses, header); ctx.render("courses/index.jte", model("page", page)); }); app.start(7070); } }
В конце переходим к шаблону:
// src/main/jte/courses/index.jte @import org.example.hexlet.dto.courses.CoursesPage @param CoursesPage page <html> <head> <title>Хекслет</title> </head> <body> <h1>${page.getHeader()}</h1> @if(page.getCourses().isEmpty()) <p>Пока не добавлено ни одного курса</p> @else @for(var course : page.getCourses()) <div> <h2><a href="/courses/${course.getId()}">${course.getName()}</a></h2> <p>${course.getDescription()}</p> </div> @endfor @endif </body> </html>
Логика вывода здесь такая:
- Если список курсов пустой, то выводится соответствующее сообщение
- Если курсы в списке есть, то на основе этого списка формируются HTML-блоки с информацией о курсе и ссылкой на его страницу
Этих конструкций хватит для решения большинства стандартных задач, потому что внутри них можно использовать любой Java-код. Единственное, что нужно не забывать — это импортировать используемые классы через директиву @import
.
Самостоятельная работа
- Выполните все шаги из этого урока на своем компьютере
- Добавьте обработчики и шаблоны для просмотра списка всех курсов и вывода страницы конкретного курса. С базой данных мы еще не работаем, поэтому список
List
из нескольких курсовCourse
можно сформировать вручную. Еще один вариант — использовать классData
по аналогии с тем, что был в домашней работе - Запустите приложение и выполните запросы к страницам /courses — например, courses/1
- Убедитесь, что все работает
- Залейте изменения на GitHub
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.