Данные приложения. Использование шаблонизаторов

Данные приложения. Использование шаблонизаторов #

Васильев Андрей Михайлович, 2023

Версии презентации


Архитектура приложения #

flowchart LR classDef process fill:#c6e9afff,stroke:#5aa02cff subgraph Компьютер клиента browser-1[Веб-браузер] end subgraph Компьютер клиента browser-2[Веб-браузер] end subgraph Сервер приложения subgraph http-server-1[HTTP-сервер приложения] web-app-1["Приложение"] resources-1>Ресурсы\nприложения] end http-server-1:::process filesystem>Файловая\nсистема] end subgraph Сервер Икс rest-data-provider[Внутренний\nHTTP-сервер] end subgraph Сервер СУБД database[СУБД] end browser-1 --> http-server-1 browser-2 --> http-server-1 web-app-1 -.-> filesystem web-app-1 -.-> resources-1 web-app-1 ---> rest-data-provider web-app-1 ---> database

  • Веб-сервер обслуживает множество клиентов
  • Клиентам необходимо множество документов, для получения которых выполняется несколько HTTP-запросов
    • HTML-документ с данными для отображения
    • CSS-документы с описанием классов для стилизации HTML
    • JS-документы для выполнения динамических действий на стороне клиента
  • Для формирования документов серверу недостаточно данных, которые хранятся в его оперативной памяти
    • Необходимо обратиться к хранилищу данных (СУБД или другая система хранения) за динамическими данными: список товаров, их количество, данные пользователя
    • Необходимо обратиться к внутренним ресурсам для получения статической информации: CSS/JS-документы, шаблоны страниц
  • Клиентам необходимо обратиться за документами, которые были загружены или сформированы приложением: изображения, архивы, PDF-документы

Максимальное использование локальных ресурсов #

flowchart LR classDef process fill:#c6e9afff,stroke:#5aa02cff subgraph Компьютер клиента browser-1[Веб-браузер] end subgraph Компьютер клиента browser-2[Веб-браузер] end subgraph Сервер приложения subgraph http-server-1[HTTP-сервер приложения] web-app-process-1["Процесс 1"] web-app-process-2["Процесс 2"] resources-1>Ресурсы\nприложения] in-memory-data[["Данные\nв оперативной\nпамяти"]] end http-server-1:::process filesystem>Файловая\nсистема] end subgraph Сервер Икс rest-data-provider[Внутренний\nHTTP-сервер] end subgraph Сервер СУБД database[СУБД] end browser-1 --> http-server-1 browser-2 --> http-server-1 web-app-process-1 -.-> resources-1 web-app-process-1 -.-> filesystem web-app-process-1 -.-> in-memory-data web-app-process-2 -.-> resources-1 web-app-process-2 -.-> filesystem web-app-process-2 -.-> in-memory-data http-server-1 ---> rest-data-provider http-server-1 ---> database

  • Современные системы предоставляют множество процессоров, способных независимо обрабатывать информацию
  • HTTP-запросы хорошо масштабируются, т.к. каждый запрос не зависит от других запросов
  • HTTP-сервера обычно запускают множество процессов (и потоков), которые используют разные процессоры
  • Потоки имеют доступ к общим данным:
    • Файловая система
    • Ресурсы приложения
    • Данные в оперативной памяти
  • Для изменяемых данных необходимо аккуратно подходить к задаче редактирования общих данных: данные в оперативной памяти и на файловой системе
  • Библиотека http4k по умолчанию запускает несколько процессов

Балансировка запросов между компьютерами #

flowchart LR classDef process fill:#c6e9afff,stroke:#5aa02cff subgraph Компьютер клиента browser-1[Веб-браузер] end subgraph Компьютер клиента browser-2[Веб-браузер] end subgraph Сервер балансировки balancer["Балансировщик\nHTTP-запросов"] end subgraph Сервер приложения 1 subgraph http-server-1[HTTP-сервер приложения] web-app-1["Приложение"] resources-1>Ресурсы\nприложения] end http-server-1:::process filesystem-1>Файловая\nсистема] end subgraph Сервер приложения 2 subgraph http-server-2[HTTP-сервер приложения] web-app-2["Приложение"] resources-2>Ресурсы\nприложения] end http-server-2:::process filesystem-2>Файловая\nсистема] end subgraph Сервер Икс rest-data-provider[Внутренний\nHTTP-сервер] end subgraph Сервер СУБД 1 database-1[СУБД] end browser-1 --> balancer browser-2 --> balancer balancer --> http-server-1 balancer --> http-server-2 balancer --> rest-data-provider web-app-1 -.-> filesystem-1 web-app-1 -.-> resources-1 web-app-1 ---> database-1 web-app-2 -.-> filesystem-2 web-app-2 -.-> resources-2 web-app-2 ---> database-1

  • Для обработки большого числа запросов от клиентов мощностей одного компьютера может не хватать
  • Можно организовать обработку запросов с помощью множества одинаковых приложений, запущенных на нескольких компьютерах
  • Выводы из данной архитектуры:
    • Файловая система не может служить подходящим средством для хранения бинарных данных
    • Все данные приложения должны находится в единых системах хранения: кластере СУБД-серверов, серверах хранения больших данных и т.д.
    • Приложение должно считывать конфигурацию из сетевой службы.
    • Невозможно организовать доступ к общим данным в оперативной памяти, необходимо использовать специализированные инструменты
  • При разработке локального приложения стоит изначально рассматривать сложности перехода к запуску нескольких приложений на нескольких серверах

Локальные данные приложения #

Рассмотрим следующие источники данных, доступные JVM-приложениям:

  • Ресурсы приложения
  • Файловая система

Общие свойства #

  • Предоставляют данные в виде файлов
  • Предоставляют данные в иерархической структуре каталогов
  • Для построения путей к файлам можно использовать абсолютные и относительные пути

Отличия #

  • Данные внутри ресурсов доступны только на чтение
  • Ресурсы поставляются вместе с исполняемым кодом приложения
  • Файлы поставляются отдельно
  • Абсолютные пути внутри ресурсов будут работать на любом компьютере для файлов стоит использовать относительные

Добавление ресурсов приложения #

В рамках системы сборки Gradle для добавления ресурсов внутрь приложения необходимо их разместить в каталоге src/main/resources

  • Данный каталог является корневым каталогом для ресурсов
  • Структура каталогов в ресурсах может совпадать со структурой каталогов в Java и Kotlin-проектах, что в свою очередь соотносится с пакетами

Результатом компиляции приложения на Kotlin является Jar-файл, включающий в себя все бинарные компоненты приложения. Jar-файлы являются Zip-архивами

flowchart LR kotlin>Исходники на Kotlin] resources>Ресурсы приложения] compiler[Компилятор] class_files>Class-файлы] jar_file>JAR-файл] kotlin --> compiler compiler --> class_files class_files --> jar_file resources --> jar_file

Сборка приложения #

Для создания дистрибутива приложения с использованием системы сборки Gradle достаточно выполнить одну из задач: distZip, distTar, installDist

Например, в результате выполнения задачи distZip будут выполнены следующие задачи

flowchart LR distZip --> jar distZip --> startScripts startScripts --> jar jar --> classes jar --> compileJava jar --> compileKotlin classes --> compileJava classes --> processResources compileJava --> compileKotlin
  • Внутри Zip-архива будут находится: Jar-файл приложения, Jar-файлы библиотек, скрипты для запуска приложения под тремя целевыми операционными системами
  • Все команды dist* создают архивы в каталоге build/distributions
  • Команда installDist создаёт структуру каталогов build/install

Доступ к ресурсам приложения #

Для доступа к ресурсам можно воспользоваться классами ClassLoader и Class, которые в Kotlin можно получить следующим образом

  • ClassLoader: SomeClass::class.java.classLoader
  • Class: SomeClass::class.java

Данный код может быть запущен только на платформе JVM

Можно воспользоваться следующими методами класса Class для чтения данных из ресурсов:

  • getResource(name: String): URL
  • getResourceAsStream(name: String): InputStream

В качестве аргумента можно указать относительный путь к файлу относительно пакета, в котором содержится указанный класс

Интерфейс класса ClassLoader предоставляет большое количество методов для поиска файлов среди ресурсов, их загрузки и т.д. Рекомендуется для самостоятельного изучения


Работа с файловой системой #

В рамках стандартной библиотеки языка Kotlin предоставляется пакет kotlin.io, включающий собственные классы и функции-расширения для стандартных классов Java

Например для считывания содержимого файла можно воспользоваться функцией

fun File.readText(charset: Charset = Charsets.UTF_8): String

Построение пути к файлам #

При работе с файловой системой необходимо обеспечить работу приложения на множестве компьютеров, для чего можно:

  • Использовать известные стандартные пути, например C:/Program Files/
  • Использовать относительные пути
    • Относительно рабочего каталога приложения
    • Относительно исполняемого файла приложения (относительно Jar-файла приложения)

Рабочий каталог приложения #

При запуске приложения с использованием системы сборки Gradle рабочим каталогом приложения становится каталог, в котором

  • Находится конфигурационный файл build.gradle
  • Внутри которого находится конфигурация плагина application, который определил задачу run

Таким образом для считывания содержимого текстового файла, расположенного в каталоге data/info.txt внутри каталога с приложением достаточно:

val text = File("data/info.txt").readText()

Стоит обратить внимание, что данные файлы по умолчанию не будут включены в дистрибутив приложения, который формируют команды distZip и distTar. Если они потребуются пользователю приложения для его корректной работы, то необходимо их добавить в конфигурацию Gradle для дистрибуции, однако автоматически определить путь к установочному каталогу достаточно сложно


Использование файловой системы #

  • Хранение временных файлов
    • Каждая ОС предоставляет средства для поиска каталога со временными файлами
  • Хранение данных, загруженных пользователем
  • Хранение постоянных данных в структурированном формате, например для встраиваемых БД
  • Хранение журналов работы приложения

Как было ранее сказано, файловая система не является подходящим решением для распределённых серверных систем. Вместо файловой системы можно воспользоваться:

  • Сервер СУБД вместо встраиваемой БД
  • Сервер для хранения и обработки файлов вместо каталога хранения
  • Сервер для обработки журналов работы приложения

Однако их тоже кто-то должен настроить и обеспечить работоспособность

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


Использование стандартных форматов данных #

На заре разработки информационных технологий можно было оправдать использование собственных форматов данных. В настоящее время в первую очередь стоит рассмотреть один из существующих форматов:

  • CSV, Comma-separated values, для базовых табличных данных
  • JSON, JavaScript Object Notation, для произвольных данных, которые можно описать с помощью массивов и ассоциативных массивов
  • YAML, YAML Ain’t Markup Language, для аналогичных задач, для описания настроек
  • TOML, Tom’s Obvious Minimal Language, для описания настроек
  • XML, Extensible Markup Language, для описания сложных структурированных документов, лежит в основе docx
  • ProtoBuf, Protocol Buffers, для обмена жёстко структурированными сообщениями
  • Ещё 1000+ разных форматов обмена данными

Для чтения и записи данных форматов существует множество библиотек. Каждый из форматов имеет как свои плюсы, так минусы

Всегда можно предложить свой формат, но для этого необходимо чётко понимать отличия от существующих: плюсы, минусы, ограничения


Раздача статических данных #

С точки зрения кода HTTP-кода все данные, которые не проходят обработку перед передачей их клиенту считаются статическими

К ним можно отнести:

  • CSS-файлы, JS-файлы, изображения для оформления документов и т.п.
  • Файлы, загруженные пользователем и не обрабатываемые с точки зрения приложения

Библиотека http4k предоставляет реализацию HTTP-обработчика, способного дать доступ к этим файлам

fun static(...): RoutingHttpHandler

В качестве аргумента ей необходимо передать объект интерфейса ResourceLoader

  • Classpath для предоставление данных из ресурсов
  • Directory для предоставления данных из файловой системы

Статические данные в ресурсах #

Функция Classpath принимает в качестве аргумента путь к пакету, данные из которого будут доступны клиенту

Предположим, что CSS и JS-файлы расположены в пакете ru.yarsu.public, который располагается по пути src/main/resources/ru/yarsu/public

src
└── main
    └── resources
        └── ru
            └── yarsu
                └── public
                    ├── css
                    └── js

Следующая конфигурация ClassPath обеспечит доступ клиентам доступ к этим каталогам:

routes(
    ...
    static(ResourceLoader.Classpath("/ru/yarsu/public")),
)

При выполнении запроса

GET /css/foundation.css HTTP/1.1

В качестве ответа будет передан файл, находящийся внутри ресурсов по пути /ru/yarsu/public/css/foundation.css


Некорректная настройка Classpath #

Рассмотрим структуру каталогов, которая была представлена на предыдущем слайде

Предположим, что разработчик сформулировал настройки следующим образом:

routes(
    ...
    static(ResourceLoader.Classpath("/ru/yarsu/")),
)

Пользователи всё-равно смогут получить доступ к статическим файлам, но по другому пути:

GET /public/css/foundation.css HTTP/1.1

Основная проблема такого решения — пользователь сможет считать не публичную информацию тоже:

  • class-файлы приложения: GET /domain/Prices.class HTTP/1.1
  • Приватные данные: GET /data/certificate.pem HTTP/1.1

Необходимо предоставлять неконтролируемый доступ только для пакетов, содержащих публичную информацию


Раздача данных с файловой системы #

В отличие от Classpath, функция Directory принимает в качестве аргумента путь к данным внутри файловой системы

fun Directory(baseDir: String = "."): ResourceLoader

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

Один из вариантов применения — обеспечение доступа к файлам из каталога ресурсов. Это позволит вносить изменения в статические файлы без перекомпиляции приложения

routes(
    ...
    static(ResourceLoader.Directory("src/main/resources/ru/yarsu/")),
)

Это становится возможным, т.к. система сборки Gradle использует корневой каталог приложения для запуска при использовании команды run


Формирование динамических данных #

Помимо статических данных пользователю необходимо предоставить документы на основании хранилища динамических данных (СУБД и другие средства хранения)

  • Данные могут быть представлены в машиночитаемых форматах: CSV, JSON, XML и т.д. Для формирования их строкового представления следует использовать специализированные библиотеки
  • Данные могут быть ориентированы на пользователя: HTML-документы, изображения и т.д. Для создания HTML-документов можно:
    • Выполнять формирование «в ручную» средствами обычного языка программирования
    • Использовать специализированные инструменты — шаблонизаторы

Задача шаблонизатора — формирование сложных текстовых документов с использованием специализированного синтаксиса, т.е. с помощью специализированного языка программирования


Шаблонизаторы в http4k #

Библиотека http4k включает в себя поддержку большинства продуктовых шаблонизаторов, доступных для платформы JVM

Принципиальная схема работы шаблонизатора

flowchart LR renderer[Шаблонизатор] template>Шаблон] base_template>Базовый шаблон] partial>Частичный шаблон] data[Данные\nдля отображения] text>Текстовый\nдокумент] base_template --> template partial --> template template --> renderer data --> renderer renderer --> text
  • Шаблонизатор — это компонент, который формирует текстовый документ на основании шаблона и данных для отображения этого шаблона
  • Шаблон — это размеченный специальным образом текст, в котором указаны места вставки данных
  • Шаблон может быть как одним файлом, так и быть связанным с другими файлами

Использование шаблонизатора в http4k #

Для использования шаблонизатора необходимо:

  1. Подключить соответствующую библиотеку к проекту в build.gradle
  2. Создать объект шаблонизатора
  3. Для каждой отдельной страницы
    1. Создать модель для отображения данных
    2. Создать файл шаблона в ресурсах, преобразующий данные в текст

Шаблонизаторы TemplateRenderer #

Для описания шаблонизатора в рамках http4k используется тип TemplateRenderer

typealias TemplateRenderer = (ViewModel) -> String

Функциональный тип, выполняющий преобразование данных типа ViewModel в строковое представление


Создание объекта шаблонизатора #

fun main() {
    val renderer = PebbleTemplates().HotReload("src/main/resources")
}
  • HotReload — горячая перезагрузка шаблонов из директории, которая указана в качестве аргумента

    fun HotReload(baseTemplateDir: String): TemplateRenderer
  • Caching — загрузка шаблонов из каталога с ускорением путём кеширования

    fun Caching(baseTemplateDir: String): TemplateRenderer
  • CachingClasspath — загрузка шаблонов из ресурсов с кешированием

    fun CachingClasspath(baseClasspathPackage: String): TemplateRenderer

Первый вариант подходит для разработки, два других ориентированы на продуктовое использование


Формирование данных для отображения #

Для описания данных шаблона необходимо создать класс-наследник интерфейса ViewModel

package ru.yarsu.domain
class PersonVM(val name: String, val birthDate: LocalDate) : ViewModel

Интерфейс ViewModel не требует реализации никаких методов, он только предоставляет реализацию метода

open fun template(): String

Данный метод позволяет определить путь к шаблону, который может быть использован для формирования строки на основании этих данных

Для класса из примера ru.yarsu.domain.PersonVM путём будет ru/yarsu/domain/PersonVM

  • Базой для пути является полное имя класса, реализующего интерфейс ViewModel
  • Шаблонизатор добавит к пути расширение, для PebbleTemplates — это .peb
  • Если данные в шаблон передавать не надо, то можно создать объект без данных

Расположение Pebble-шаблонов #

После создания класса-модели с данными необходимо создать Pebble-файл, учитывая:

  • Название класса-модели, описывающее относительный путь
  • Тип шаблонизатора, описывающий расширение для файла
  • Конфигурацию шаблонизатора, описывающего логику для поиска файлов-шаблонов

Рекомендуется размещать шаблоны в ресурсах приложения, т.к. ресурсы будут доступны приложению после создания дистрибутива приложения

  • В условиях, показанных на предыдущих слайдах следует создать файл по пути src/main/resources/ru/yarsu/domain/Person.peb
  • Для работы шаблонизатора необходимо описать класс-модель и файл шаблона для её отображения

Использование Pebble-шаблона #

val renderer: TemplateRenderer = PebbleTemplates().HotReload("src/main/resources")
val model = PersonVM("Марина", LocalDate.of(2000, 01, 01))
val text = renderer(model)

Наполнение Pebble-шаблона #

Шаблонизатор Pebble реализует язык, похожий на шаблонизаторы TWIG из PHP и Jinja2 из Python

<html>
  <head>
    <title>Пример шаблона</title>
  </head>
  <body>
    <p>Имя: {{ model.name }}</p>
    <p>День рождения: {{ model.birthDate | date("yyyy-MM-dd") }}
  </body>
</html>
  • Объект модели доступен через переменную model
  • Возможен вызов любых методов модели
  • Для вывода содержимого выражения в результирующий текст используется конструкция {{ }}
  • Шаблонизатор предоставляет множество фильтров, которые можно применить к данным {{ SOURCE | FILTER-1 | FILTER-2 }}

Доступ к переменным #

При обращении к полю переменной model.data внутри шаблона Pebble попытается у переданного объекта вызвать следующие методы:

  • Если model является словарём (наследником Map), то будет вызван метод model.get("data")
  • model.getData()
  • model.isData()
  • model.hasData()
  • model.data()
  • model.data

Если в переменной list содержится список, то к его полям можно обращаться с помощью list[0] вместо list.get(0)

Если возвращённое значение на каком-то этапе будет null, то шаблонизатор вернёт пустую строку


Передача списков данных в шаблоны #

Шаблонизаторы поддерживает работу и со списками данных

class Event(val start: LocalDateTime, val description: String)
class EventsList(val events: List<Event>) : ViewModel

Шаблон для отображения списка событий:

{% for event in model.events %}
  <h3>Событие {{ event.description }}</h3>
  <p>Начало события: {{ event.start | date("yyyy-MM-dd HH:mm",
    timeZone="UTC") }}</p>
{% else %}
  <p>События не описаны</p>
{% endfor %}

Управляющие конструкции, вывод которых не должен попасть в текст документа, помещаются внутри {% %}


Управляющие конструкции #

{% if category == "news" %}
  {{ news }}
{% elseif category == "sports" %}
  {{ sports }}
{% else %}
  <p>Пожалуйста укажите категорию</p>
{% endif %}
  • С помощью условных конструкций необходимо адаптировать данные для отображения
  • Запрещено помещать логику по обработке, группировке, сортировке и т.п. внутри шаблона. Все эти действия необходимо произвести внутри основной логики приложения, а в шаблон необходимо передать уже подготовленные данные
  • Условные выражения поддерживают логическое объединение выражений:
    • and — логическое И
    • or — логическое ИЛИ
    • not — логическое отрицание
    • () — группировка

Проверки #

В рамках условных выражений Pebble позволяет вызывать операцию is и связывать её с проверками:

{% if 3 is odd %}
  3 является нечётным числом
{% endif %}

При использовании оператора можно проверить отрицание проверки:

{% if name is not null %}
  Имя не указано!
{% endif %}

Наследование шаблонов #

Шаблонизатор Pebble поддерживает концепцию иерархических шаблонов

  • Базовый шаблон включает в себя основное содержимое и точки расширения
  • Шаблон-расширение указывает содержимое для точек расширения

Базовый шаблон, например layout.peb:

<html>
  <body>
    <div id="content">
      {% block content %} {% endblock %}
    </div>
    <div id="footer">
      {% block footer %} Default content {% endblock %}
    </div>
  </body>
</html>

Шаблон-расширение:

{% extends "./layout.peb" %}
{% block content %} Содержимое {% endblock %}

Включение шаблонов #

Помимо иерархического разделения Pebble поддерживает включение одних шаблонов в другие

  • Шаблоны с описанием макросов
  • Шаблоны для описания типовых компонентов
  • Подменяемые статические конструкции страниц
<div class="sidebar">
  {% include "advertisement.html" %}
</div>

Архитектура приложения и её проектирование #

Процесс проектирования архитектуры приложения может быть

  • выделеным, когда происходит разработка подходов к решению задач
  • оперативным, когда создаются отдельные файлы, функции и классы

Архитектура приложения — это комбинация выбранных подходов к формированию приложения, а также результирующая структура исходного кода

Обычно архитектура проявляется в подходе к разделению зон ответственности исходного кода приложения на части


Подходы к разделению #

Деление по функциям предметной области #

Приложение разделяется по логически связным частям предметной области, для каждой функции выделяются отдельные части приложения

  • Управление комнатами и списками постояльцев
  • Управление персоналом гостиницы
  • Управление автопарком гостиницы

Для каждой из этих областей можно выделить класс или набор классов в выделенном пакете


Деление по техническим аспектам #

Выделение кода, описывающего предметную область #

Любое достаточно сложное приложение содержит нетривиальную логику по модификации данных, эту логику удобно отделить от технической части приложения

  • Минимизация зависимостей позволяет легче тестировать классы предметной области
  • Каждая часть приложения будет достаточно сложной
  • В каждой части можно будет решать одновременно меньше задач

Выделение различных технических частей приложения #

Относительно веб-приложения можно рассмотреть следующие части

  • Обработчики HTTP-запросов
  • Фильтры
  • Слой валидации данных
  • Слой визуализации данных в HTML-документы или JSON-документы

Вариант разделения частей для веб-приложения #

Можно предложить следующий подход к разделению небольшого приложения на части:

  • Описание предментной области, пакет .domain
    • Подсистема хранения данных, пакет .domain.storage или .domain.database
    • Подсистема изменения данных, пакет .domain.operations
  • Статические данные приложения, пакет .public
  • Логика обработки HTTP-запросов, пакет .web
    • Фильтры по обработке HTTP-запросов, пакет .web.filters
    • Обработчики HTTP-запросов, пакет .web.handlers
    • Шаблоны для отображения информации, пакет .web.models
    • Подсистема валидации входящих запросов от пользователя .web.validation

© A. M. Васильев, 2023, CC BY-SA 4.0, andrey@crafted.su