Jinja2

Я уже много раз упоминал в комментариях и других постах, что когда мне приходится программировать, последние пару лет я чаще всего использую Python. При этом так забавно получилось, что в рубрике "Программирование" об этом языке практически ни слова, даже подрубрики не было. Сегодня я попробую потихоньку начать исправлять данную ситуацию, речь пойдет об одном из самых продвинутых шаблонизаторов под Python - Jinja2.

Встречаем!

Введение

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

Прежде чем перейти к делу, хочу напомнить что имеется ввиду под словом шаблонизатор: механизм для создания HTML-страниц путем заполнения HTML-шаблонов динамическими данными, получаемыми из СУБД или внешних источников. Шаблонизатор предоставляет некую надстройку над синтаксисом HTML для создания шаблонов и API для их использования.

Базовый функционал

Многое из этого можно увидеть и в альтернативных реализациях шаблонизаторов, так что ничего особенного:

  • {{ ... }} позволяет распечатать значение переменной или какого-то выражения, синтаксис достаточно свободный - можно обращаться к элементам коллекций, методам/атрибутам объектов и.т.п.
  • {% ... %} позволяет вызвать дополнительные теги, среди которых условные выражения, различные варианты циклов и многое другое.
  • Присутствиет концепция фильтров, сильно напоминающих UNIX pipes: начинается все с переменной или выражения, после чего можно через символ | указать как её обработать перед выводом в итоговый документ. Например, {{ foo|lower }} выведет содержимое строки foo в нижнем регистре. Как и в pipes, из фильтров можно делать цепочки.
  • Механизм наследования позволяет избежать избыточности в коде. В коде шаблона выделяются именованные блоки тегом {% block ... %}, после чего шаблон-потомок может переопределить содержимое блоков шаблона-родителя произвольным образом. Типичный пример использования:
    • Создается базовый шаблон страницы, состоящий из основного каркаса страницы  и всех общих для всего сайта элементов (ссылки на файлы стилей, общие JavaScript файлы и библиотеки, какие-то мета теги, title по-умолчанию)
    • В базовом шаблоне содержимое каждой части выделяется в именованный блок (как минимум шапка, место под контент, 1-2 сайдбара и подвал), иногда рядом со стилями и скриптами оставляют по пустому блоку на случай, если шаблонам-наследникам потребуется что-то специфичное.
    • Если какой-то блок будет содержать одну и ту же информацию на большинстве страниц сайта, то её тоже обычно помещают в базовый шаблон.
    • Создается по шаблону-потомку на каждый тип используемых на сайте страниц, в которых переопределяется как правило  (но далеко не всегда) только блок с конткентом и заголовок страницы. Из шаблонов-потомков также можно составить иерархию в случае, если у них есть много общей информации.
    • Стоит упомянуть, что есть альтернативный механизм включения (include) шаблонов по-аналогии с PHP-файлами, но я достойных применений ему не нашел.
  • Очень много внимания уделено экранированию символов, хотя особо на него надеяться не стоит - с точки зрения безопасности намного важнее фильтровать попадающие на сайт данные, а не выводимые в шаблонах. Хотя как дополнительная подстраховка не помешает.
  • Простая интеграция с gettext придется кстати интернациональным проектам.
  • Опциональное считывание шаблонов с диска при каждом запросе страницы незаменимо при разработке.

Производительность

Сравнительные тесты производительности шаблонизаторов под Python довольно условны, очень многое зависит от конкретных шаблонов и динамических данных. Тем не менее, во всех из них Jinja2 определенно не в аутсайдерах, в топ5 вполне стабильно.

Шаблоны компилируются в байткод для последующего использования, с этой особенностью связаны два момента, которые спользовать:

  • Байткод можно хранить в memcached или любом другом внешним хранилище, достаточно лишь реализовать минимальный get/set интерфейс.
  • Доступен опциональный модуль на C, который берет на себя часть работы по заполнению шаблонов, что делает этот процесс несколько быстрее.

Расширяемость

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

Webassets

Этот проект позволяет делать с подключаемыми внешними Javascript и CSS файлами практически все, что угодно. В типичном варианте использования используется тег {% assets %} для:

  • Указания списка изначальных CSS/JS файлов, для конкатенации и последующей обработки.
  • Указания окружения ссылки на итоговый файл, т.е. как именно должен выглядеть тег <script> или <style>.
  • Списка фильтров, для минимизации или других преобразований кода.
  • Возможно использование sass или less файлов вместо чистого CSS.
  • Отключение конкатенации и минимизации при разработке доступно изменением одного флага.

В итоге вопрос с подготовкой минимизированных статических файлов становится полностью автоматическим.

Доступна интегрирация и с другими Python шаблонизаторами, в Jinja2 он подключается просто как расширение.

Минимизация HTML

Этот вопрос решается путем наследования от поставляемого вместе с шаблонизатором загрузчика шаблонов. API позволяет делать между чтением шаблона и генерацией байткода что угодно с текстом шаблона, например можно пропустить все через примитивную регулярку (вернее через несколько) и свернуть тем самым весь HTML в одну строку. Хочется обратить внимание, что осуществляется этот процесс очень редко (особенно при использовании кэша байт кода), так что можно делать на этом этапе даже сильно ресурсоемкие преобразования текста.

Другой формат данных

В одном из моих проектов при первом заходе на сайт или при отключенном JS сервер полностью отрисовывал страницу, а при переходах по ссылкам внутри сайта делался AJAX-запрос и сервер выдавал какие блоки нужно обновить и каким содержимым в формате JSON. Про клиентскую часть всего этого дела можно легко написать отдельную здоровую статью, так что в подробности не вдаюсь.

Да, наверняка многие скажут, что в этой ситуации надо было использовать универсальные шаблоны для JS и серверной части, но на Jinja2 такое тожно можно реализовать, с той лишь разницей, что пришлось гонять по сети не только сами данные, но и часть HTML-разметки (что, порой, тоже не плохо). Реализуется как и минимизация HTML посредством переопределения загрузчика страниц, который использовался вместо стандартного, если запрос пришел через AJAX.

Сэкономим еще пару байт

С подобного рода оптимизацией не заборачивается, наверное и 0.01% интернет-проектов, но я в свое время как-то увлекся и написал штуку для "выжимания" десятка-другого байт с большинства страниц и CSS/JS файлов. "Проблема" состоит в следующем: классы и идентификаторы, использующиеся в HTML, в "культурных" проектах имеют хотя бы отдаленно человекочитаемые названия, что почти всегда означает их длинность, что, учитывая их частую повторяемость в коде, в свою очередь негативно влияет на итоговый размер HTML/JS/CSS документов. Теоретическое решение лежит на поверхности: использовать "длинные" идентификаторы и классы в HTML при разработке, а при развертывании на публику переименовывать их в "короткие": a, b, c, ..., aa, ab, ac, ...

На практике же все несколько сложнее: есть масса проблем с префиксностью и суффиксностью, в JS классы иногда неотличимы от других строк с точками (зависят от контекста), некоторые классы и идентификаторы генерируются динамически - на них прийдется либо забивать, либо обрабатывать индивидуально, и это далеко не все...

Если у кого-то возникнет желание тоже сделать что-то подобное средствами Jinja2, то советую "вмешиваться" в обработку JS/CSS посредством дополнительного фильтра в Webassets, а сами шаблоны редактировать как и в предыдущих разделах при считывании их с диска.

Спрайты и обработка изображений

Создание спрайтов как таковых не предусмотрено, так как по сути это не по части шаблонизатора. Но есть вариант подключить их к Webassets, например через интеграцию Ruby-проектом compass (у которого есть плагин-генератор спрайтов).

Если говорить просто о уменьшении размеров изображений, то это легко делается средствами самого Python и с шаблонизатором практически не взаимодействует: достаточно обрабатывать изображения при загрузке их пользователями и держать статические изображения "в форме".

Идеи для фильтров и глобальных функций

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

  • Форматирования даты/времени по шаблону
  • Фильтрации HTML с белым списком тегов
  • Получения атрибута объекта с неизвестным заранее именем (getattr)
  • Вывода строки в режиме "первая - заглавная, остальные - прописные
  • Генерации часто используемых HTML-тегов, например <a href="..."></a>

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

Подводим итоги

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

С удовольствием бы опубликовал упоминавшиеся в статье куски кода в opensource, но для этого нужно взять себя в руки и состряпать из них что-то "отчуждаемое" от тех проектов, для которых оно писалось.

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

Спасибо за внимание, подписавшись на Insight IT можно узнавать о новых материалах одним из первых :)

19 февраля 2012 |  Иван Блинков  |  Python