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

На этом занятии поговорим о механизме расширения шаблонов в Jninja. Его еще называют наследованием. Это достаточно удобный инструмент, уменьшающий объем дублируемого кода в шаблонах. Рассмотрим принцип его работы на нашем примере с HTML-страницами сайта.

Для простоты восприятия возьмем целостную страницу и представим ее в таком виде (файл ex_main.htm):

<!DOCTYPE html>
<html>
<head>
         <meta charset="UTF-8">
         <title>{% block title %}{% endblock %}</title>
</head>
<body>
 
{% block content %}
{% endblock %}
 
</body>
</html>

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

{% block <имя блока> %}
{% endblock %}

Эти блоки как раз и используются для создания расширения базового шаблона страницы ex_main.htm для создания всех страниц текущего сайта. Расширение (или наследование) шаблона делается следующим образом (файл about.htm):

{% extends 'ex_main.htm' %}
 
{% block title%}О сайте{% endblock %}
 
{% block content %}
<h1>О сайте</h1>
<p>Классный сайт, если его доделать.</p>
{% endblock %}

Первой строчкой мы указываем базовый шаблон 'ex_main.htm', который собираемся расширять в шаблоне about.htm. Затем, указываем, что первый именованный блок title будет содержать строку «О сайте», а второй (content) – текст, помещенный в него.

Далее, в самой программе на Python мы делаем следующее:

from jinja2 import Environment, FileSystemLoader
 
file_loader = FileSystemLoader('templates')
env = Environment(loader=file_loader)
 
template = env.get_template('about.htm')
 
output = template.render()
print(output)

Используя файловый загрузчик, берем файл шаблона about.htm из подкаталога templates и, затем, обрабатываем его с помощью метода render. На выходе получим следующую HTML-страницу:

<!DOCTYPE html>
<html>
<head>
         <meta charset="UTF-8">
         <title>О сайте</title>
</head>
<body>
<h1>О сайте</h1>
<p>Классный сайт, если его доделать.</p>
</body>
</html>

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

Разумеется, если базовый шаблон находится в другом каталоге относительно дочернего about.htm, то в инструкции extends это явно нужно прописать, например, так:

{% extends 'layout/default.tpl' %}

Будет взят шаблон default.tpl из подкаталога layout, находящийся в каталоге templates.

Далее, если нам нужно один и тот же блок использовать несколько раз на странице, то это делается так:

{% extends 'layout/default.tpl' %}
 
{% block title%}О сайте{% endblock %}
 
{% block content %}
<h1>{{ self.title() }}</h1>
<p>Классный сайт, если его доделать.</p>
{% endblock %}

Здесь self ссылается на объект текущего шаблона, в котором имеется метод title, т.к. существует блок с таким именем. И, далее, вызывая его, будет напечатано содержимое этого блока. Так можно делать с любым именованным блоком.

Ну а раз есть параметр self, то должен быть и super, который обращается к блоку базового шаблона и берет информацию непосредственно из него. В качестве простой демонстрации добавим в блок content базового шаблона строку «Блок контента». А в дочернем шаблоне about.htm сделаем вызов:

{% block content %}
{{ super() }}
<h1>{{ self.title() }}</h1>
<p>Классный сайт, если его доделать.</p>
{% endblock %}

В итоге, получим результат:

Блок контента
<h1>О сайте</h1>
<p>Классный сайт, если его доделать.</p>

Если же вызов super убрать, то останутся только две строчки:

<h1>О сайте</h1>
<p>Классный сайт, если его доделать.</p>

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

Вложенные блоки

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

{% block content %}
         {% block table_contents %}
         <ul>
         {% for li in list_table -%}
         <li>{{li}}</li>
         {% endfor -%}
         </ul>
         {% endblock table_contents %}
{% endblock content %}

Обратите внимание, здесь после endblock дополнительно указано какой блок заканчивается. Это необязательная запись, но она помогает лучше ориентироваться в сложных шаблонах. Далее, в дочернем шаблоне about.htm обратиться к этому вложенному блоку можно так:

{% block content %}
{{ super() }}
<h1>{{ self.title() }}</h1>
<p>Классный сайт, если его доделать.</p>
{% endblock %}

Или, так:

{% block content %}
{% block table_contents %}{{ super() }}{% endblock %}
<h1>{{ self.title() }}</h1>
<p>Классный сайт, если его доделать.</p>
{% endblock %}

Вот этот второй вариант более гибкий, т.к. в block content базового шаблона могут присутствовать и другие именованные блоки и мы здесь добавляем только один - table_contents, остальные будут проигнорированы. Если же убрать строчку с вложенным блоком:

{% block content %}
<h1>{{ self.title() }}</h1>
<p>Классный сайт, если его доделать.</p>
{% endblock %}

То получим страницу без оглавления. Видите, это очень удобно: если нужно добавляем буквально одной строчкой, а если не надо, то ничего не пишем.

Область видимости блоков

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

{% for li in list_table -%}
<li>{% block item %}{{ li }}{% endblock %}</li>
{% endfor -%}

Если теперь выполнить программу, то внутри тегов li не будет никакой информации. Дело в том, что внутри блока item доступ к внешней переменной li нет. Чтобы исправить эту ситуацию и разрешить оперировать переменными из внешней области видимости, после имени блока следует прописать ключевое слово scoped:

<li>{% block item scoped %}{{ li }}{% endblock %}</li>

Теперь при запуске программа будет работать также, как и ранее. Но мы же добавили этот блок item не просто так, значит, собираемся его переопределять в дочернем шаблоне. И это можно сделать следующим образом:

{% block item %}<p class="item">{{ super() }}</p>{% endblock %}

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

         <ul>
         <li><p class="item">Математика</p></li>
         <li><p class="item">Физика</p></li>
         <li><p class="item">Русский</p></li>
         <li><p class="item">Информатика</p></li>
         </ul>

Вот такая гибкость может быть достигнута за счет использования механизма наследования и именованных блоков.

Вложенное наследование шаблонов

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

Можно организовать так:

  • файл base.tpl – такой же как и ex_main.htm:
  • файл child1.htm: {% extends 'base.tpl' %} …
  • файл child2.htm: {% extends 'child1.htm' %} …

И их дальнейшая обработка происходит аналогичным образом.

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

https://jinja.palletsprojects.com/en/2.11.x