Зарегистрируйтесь, чтобы продолжить обучение

Шаблонизатор Java: Веб-технологии

В этом уроке мы познакомимся с шаблонизаторами.

Зачем нужны шаблонизаторы

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. Чтобы попрактиковаться, добавим первый шаблон для главной страницы сайта. Для этого выполним два действия:

  1. Создадим шаблон 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>
    
  2. Укажем обработчику главной страницы использовать этот шаблон:

    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);
    }
}

Рассмотрим, по какому алгоритму обработчики обычно работают с шаблонами:

  1. Сначала они извлекают все необходимые данные
  2. Затем они создают объект дата-класса и заполняют его этими данными
  3. В итоге они передают этот объект в шаблон в виде 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, по которому выводится список курсов с описанием и ссылками на курсы:

  1. Начнем с дата-класса. Кроме курсов, добавим еще и заголовок для разнообразия:

    // Путь 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;
    }
    
  2. Перейдем к обработчику:

    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);
        }
    }
    
  3. В конце переходим к шаблону:

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


Самостоятельная работа

  1. Выполните все шаги из этого урока на своем компьютере
  2. Добавьте обработчики и шаблоны для просмотра списка всех курсов и вывода страницы конкретного курса. С базой данных мы еще не работаем, поэтому список List из нескольких курсов Course можно сформировать вручную. Еще один вариант — использовать класс Data по аналогии с тем, что был в домашней работе
  3. Запустите приложение и выполните запросы к страницам /courses — например, courses/1
  4. Убедитесь, что все работает
  5. Залейте изменения на GitHub

Дополнительные материалы

  1. Официальная документация jte

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка приложений на языке Java
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»