Если цены на хлеб начнут повышаться, люди станут покупать его больше.
статья
Жорж Парадокс

Расширение для jinja2, кэширование фрагментов шаблона в Django

Привет всем!

Наверное одним из самых популярных фреймворкеров в среде программистов WEB-разработчиков на Python является Django. И скорее всего ни для кого из них не секрет о функциональной слабости встроенного в Django шаблонизатора. По этой причине программисты на Django довольно часто, в конечном счёте, переходят на какой-либо другой шаблонизатор, и в Django это проще всего сделать на Jinja2, переход на который, в принципе, уже предусмотрен прямо из коробки.

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

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

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

from jinja2 import nodes
from jinja2 import lexer
from jinja2.ext import Extension

from django.core.cache import caches
from django.core.cache.utils import make_template_fragment_key


class TemplateCacheExtension(Extension):
    """ Расширение кеширование фрагмента шаблона для Jinja2 """
    tags = {"cache"}

    def parse(self, parser):
        lineno = next(parser.stream).lineno

        args, kwargs = self._get_token_args(parser)

        body = parser.parse_statements(end_tokens=["name:endcache"], drop_needle=True)
        return nodes.CallBlock(
            self.call_method("_cache_support", args, kwargs), [], [], body
        ).set_lineno(lineno)

    @classmethod
    def _get_token_args(cls, parser):
        """ Возвращает параметры блока кеширования """
        args = []
        kwargs = {}

        # Пока не будет достигнут конец знака блока
        while parser.stream.current.type != lexer.TOKEN_BLOCK_END:
            # будем анализировать входные параметры.
            # Во первых надо пропустить все запятые.
            if parser.stream.skip_if(lexer.TOKEN_COMMA):
                continue
            # Двлее получим очередной параметр,
            token = parser.parse_expression()
            # и проверим, если это параметр 'using', значит это указатель на вид cache-а,
            if isinstance(token, nodes.Name) and token.name == 'using':
                # и тогда перепрыгиваем знак равно,
                next(parser.stream)
                # получаем значение типа кэша, и добавляем его в виле ключевого параметра,
                kwargs = [nodes.Keyword('using', parser.parse_expression())]
                # и выходим из анализа параметров. так этот параметр должен быть последним.
                break
            # Все остальные параметры добавляем в позиционные параметры.
            args.append(token)
        return args, kwargs

    @classmethod
    def _cache_support(cls, *args, **kwargs):
        """ Кеширует указанный фрагмент шаблона """
        # Первым параметром должно быть время жизни кэша.
        timeout = args[0]
        # Вторым параметром должно быть название фрагмента.
        fragment_name = args[1]
        ext_name = []
        # Далее надо пройтись по всем оставшимся параметрам.
        if len(args) > 2:
            for i in range(2, len(args)):
                ext_name.append(args[i])
                # Если в блоке указан ключевой параметр 'using', значит через него передан алиас кэширования.
        cache = caches['default'] if 'using' not in kwargs else caches[kwargs['using']]
        # В эту переменную сохраняется функция возврата содержимого текущего фрагмента шаблона.
        caller = kwargs['caller']

        # Для начала надо сформировать ключ фрагмента, причем с учетом дополнительных параметров фрагмента.
        key = make_template_fragment_key(fragment_name, ext_name)
        # По ключу попробуем взять фрагмент из кэша.
        fragment = cache.get(key)
        # Если в кэше этого фрагмента ещё нет, или срок его истёк,
        if fragment is None:
            # то получим фрагмент из шаблона,
            fragment = caller()
            # и сразу отправим его в кэш.
            cache.set(key, fragment, timeout)
        # По окоанчанию работы с кэшем его лучше закрыть.
        cache.close()
        # Возвращаем полученный фрагмент.
        return fragment

Примерно вот так вот оно подключается:

TEMPLATES = [
    {
        'NAME': 'jinja2',
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        'APP_DIRS': True,
        'OPTIONS': {
            'environment': 'DjangoProject.jinja2.environment',
            'autoescape': False,
            'trim_blocks': True,
            "extensions": [
                "jinja2.ext.do",
                "jinja2.ext.i18n",
                "path_to_extensions.TemplateCacheExtension"
            ],
        }
    }
]

или вот так:

env = Environment(extensions=["path_to_extensions.TemplateCacheExtension"], **options)

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

...       
 <div class="container">
            <div class="row">
                <!-- LEFT-SIDEBAR -->
                <div id="left-sidebar">
                    <div class="sidebar-data">
                    {% cache 3600, "left-sidebar", request.user.username, using="cacheAliasName" %}
                        {% include "for_inclusion/left_menu.html" %}
                    {% endcache %}
                    </div>
                </div><!-- /.left-sidebar -->
                <!-- MAIN-CONTENT -->
                <div id="content">
...

Тэг кэширования "cache" первым параметром принимает время жизни кэша в секундах, вторым параметром принимает уникальное наименование фрагмента шаблона. Далее можно указать сколько угодно дополнительных параметров, которые ещё больше определят уникальность данного фрагмента кэша, в данном примере указана переменная логина авторизованного пользователя. Если в приложении используется несколько видов кэширования, например кэширование в базу данных, и кэширование в файловую систему, то в конце параметров можно указать ключевой параметр "using", через который передать наименование алиаса кэша, с помощью которого данный фрагмент должен быть кэширован. Эти алиасы указаны в настройках Django, как раз в разделе настроек кэша.

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

from django.core.cache import caches
from django.core.cache.utils import make_template_fragment_key


def clear_left_sidebar_cache(self, username):
    key = make_template_fragment_key("left-sidebar", [username])
    self.caches["cacheAliasName"].delete(key)

GitHub

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