Шаблоны Django: лучшие практики

Оглавление

Введение в шаблоны Django

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

Простой старт с шаблонами Django

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

Тэги: Тэги обеспечивают произвольную логику в процессе рендеринга. Django оставляет это определение довольно расплывчатым, но теги могут выводить содержимое, получать содержимое из базы данных (подробнее об этом позже), или выполнять управляющие операции, такие как операторы if или циклы for.

Примеры тегов:

{% firstof user.is_active user.is_staff user.is_deleted %}

Тег firstof выведет первую предоставленную переменную, которая оценивается в True. Это хорошая замена для большого блока if/elif/elif/elif/elif, который просто оценивает истинность в ваших шаблонах Django.

<ul>
{% for product in product_list %}
    <li>{{ product.name }}: ${{ product.price }}</li>
{% endfor %}
</ul>

Тег for в Django перебирает каждый элемент списка, делая этот элемент (product, в данном случае) доступным в контексте шаблона до того, как тег будет закрыт с помощью endfor. Это широко используемый шаблон при работе со списками экземпляров моделей Django, которые были возвращены из представления.

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

Примеры фильтров:

{{ value|date:'D d M Y' }}

Фильтр date отформатирует дату (value, в примере), заданную строкой с некоторыми символами формата. В примере будет выведена строка: Mon 01 Apr 2019.

{{ value|slugify }}

Фильтр slugify преобразует пробелы в строке в дефисы и переводит строку в нижний регистр, среди прочего. Вывод этого примера would-look-something-like-this.

Структура проекта

Django по умолчанию делает некоторые предположения о структуре нашего проекта при поиске шаблонов. Зная это, мы можем настроить наш проект с каталогом шаблонов и каталогами шаблонов приложений .

Представьте себе проект, облако, со следующей структурой:

cloud/
	accounts/
		urls.py
		models.py
		views.py
		templates/
			accounts/
				login.html
				register.html
	blog/
		urls.py
		views.py
		models.py
		templates/
			blog/
				create.html
				post.html
				list.html
	config/
		settings/
			base.py
			local.py
		urls.py
	manage.py
	templates/
		includes/
			messages.html
			modal.html
		base.html
		logged_in.html

Как работает наследование для шаблонов Django

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

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

Например, base.html может содержать:

{% load static %}
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  {% block page_meta %}
  {% endblock %}

  {# Vendor styles #}
  {% block vendor_css %}
    <link rel="stylesheet" type="text/css" media="all" href="{% static 'css/vendor.css' %}" />
  {% endblock %}

  {# Global styles #}
  {% block site_css %}
    <link rel="stylesheet" type="text/css" media="all" href="{% static 'css/application.css' %}" />
  {% endblock %}

  {# Page-specific styles #}
  {% autoescape off %}
    {% block page_css %}{% endblock %}
  {% endautoescape %}

  {% block extra_head %}
    {# Extra header stuff (scripts, styles, metadata, etc) #}
  {% endblock %}

  <title>{% block page_title %}{% endblock %}</title>
</head>
<body class="{% block body_class %}{% endblock %}">
{% block body %}
    {# Page content will go here #}
{% endblock %}

{# Modal HTML #}
{% block modals %}
{% endblock %}

{# Vendor javascript #}
{% block vendor_js %}
  <script src="{% static 'js/vendor.js' %}"></script>
{% endblock %}

{# Global javascript #}
{% block site_js %}
  <script src="{% static 'js/application.js' %}"></script>
{% endblock %}

{# Shared data for javascript #}
<script type="text/javascript">
  window._sharedData = {
    {% autoescape off %}
      {% block shared_data %}
        'DEBUG': {% if debug %}true{% else %}false{% endif %},
      {% endblock %}
    {% endautoescape %}
  }
</script>

{# Page javascript #}
{% autoescape off %}
  {% block page_js %}
  {% endblock %}
{% endautoescape %}
</body>
</html>

В этом примере есть несколько вещей, сделанных специально для наследования. Прежде всего, в этом базовом шаблоне определены блоки почти для каждого настраиваемого аспекта базового HTML. Определены блоки для включения CSS, JavaScript, HTML-заголовка, метатегов и многого другого.

Мы используем тег шаблона Django autoescape вокруг блоков, где мы не хотим, чтобы Django автоскриптовал наши HTML-теги или JavaScript, а воспринимал содержимое блока буквально.

Блок shared_data позволяет нам заполнить глобальный объект JavaScript переменными и данными, которыми мы можем захотеть поделиться между Django и любым запущенным JavaScript на странице (например, заполнить компоненты React или Vue.js)

Например, если бы мы хотели передать Django URL одному из наших JavaScript файлов, мы могли бы сделать что-то вроде этого:

{% extends 'base.html' %}

{% block shared_data %}
  {{ block.super }}
  'USERS_AUTOCOMPLETE_ENDPOINT': '{% url 'api:users:autocomplete' %}',
{% endblock %}

Django загружает страницу и возвращает JavaScript объект, который вы можете использовать в JavaScript файлах на странице:

<script type="text/javascript">
    window._sharedData = {      
    	'DEBUG': false,
    	'USERS_AUTOCOMPLETE_ENDPOINT': '/api/users/autocomplete/',
    }
</script>

Внутри JS-консоли после загрузки страницы:

>> window._sharedData.DEBUG
false
>> window._sharedData.USERS_AUTOCOMPLETE_ENDPOINT
'/api/users/autocomplete/'

Обработка наборов запросов

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

Система шаблонов Django тесно связана со слоем объектно-реляционного отображения Django, который возвращает нам данные из базы данных. Без должного внимания к этой связи вы можете непреднамеренно привести к тому, что количество запросов, выполняемых при каждой загрузке страницы, подскочит до неуправляемых величин. В некоторых случаях это может привести к тому, что база данных станет слишком медленной для работы некоторых страниц вашего сайта, или, что еще хуже, произойдет сбой и потребуется перезапуск.

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

Рассмотрим этот распространенный шаблон Django:

accounts/views.py

class UserListView(ListView):
    template_name = 'accounts/list.html'
    model = User
    paginate_by = 25
    context_object_name = 'users'
    queryset = User.objects.all()

accounts/templates/accounts/list.html

...
<table>
  <thead>
  <tr>
    <th>Username</th>
    <th>Email</th>
    <th>Profile photo URL</th>
    <th>Joined</th>
  </tr>
  </thead>
  <tbody>
  {% for user in users %}
    <tr>
      <td>{{ user.username }}</td>
      <td>{{ user.email_address }}</td>
      <td>{{ user.profile.avatar_url }}</td>
      <td>{{ user.created_at }}</td>
    </tr>
  {% endfor %}
  </tbody>
</table>
...

Можете ли вы заметить проблему? Сначала это может быть не очевидно, но посмотрите на эту строку:

<td>{{ user.profile.avatar_url }}</td>

Когда Django обрабатывает и визуализирует наш шаблон (строка за строкой), ему потребуется выполнить дополнительный запрос, чтобы получить информацию из объекта profile, поскольку это связанное поле. В нашем примере мы постранично просматриваем 25 пользователей, поэтому эта одна строка в шаблоне может содержать дополнительные 25 запросов (на каждом запросе страницы к объекту profile, как и ко всем связанным объектам и моделям в Django), которые не включены в исходный запрос для 25 пользователей. Вы можете представить, как это могло бы стать очень медленной страницей, если бы мы включали поля из других связанных объектов в нашу таблицу, или если бы мы делали пагинацию по 100 пользователям вместо 25.

Чтобы решить эту проблему, мы изменим одну строку в нашем представлении, accounts/views.py, чтобы выбрать связанные (select_related) объекты, когда мы выполняем наш первоначальный запрос для пользователей:

class UserListView(ListView):
    template_name = 'accounts/list.html'
    model = User
    paginate_by = 25
    context_object_name = 'users'
    queryset = User.objects.select_related('profile')

Заменяя User.objects.all() на User.objects.select_related(‘profile’), мы указываем Django включать связанные экземпляры профиля, когда он выполняет запрос для наших пользователей. Это включит модель Profile в каждый экземпляр User, предотвращая необходимость выполнения дополнительного запроса каждый раз, когда мы запрашиваем информацию из профиля в шаблоне.

Функциональность select_related в Django не работает с отношениями модели "многие ко многим" или "многие к одному". Для этого мы должны использовать метод Django prefetch_related.

В отличие от select_related, prefetch_related делает свою магию в Python, в отличие от SQL select statements, объединяя связанные объекты в экземпляры, к которым можно обращаться в шаблонах, как мы делали выше. Он не выполняет все за один запрос, как это делает select_related, но это гораздо эффективнее, чем выполнять запрос каждый раз, когда вы запрашиваете связанный атрибут.

Предварительная выборка для связанных проектов и организаций и отношений "один-ко-многим" из модели User будет выглядеть следующим образом:

class UserListView(ListView):
    template_name = 'accounts/list.html'
    model = User
    paginate_by = 25
    context_object_name = 'users'
    queryset = User.objects.prefetch_related('projects', 'organizations')

Вы можете использовать такие инструменты, как django-debug-toolbar для исследования шаблонов и представлений в вашем Django-приложении, которые могут выиграть от использования select_related и prefetch_related. После установки django-debug-toolbar может показать, какие запросы выполняются при выполнении представления и загрузке шаблона. Это невероятно полезно для отладки медленных страниц, написанный вами шаблон может выполнять сотни запросов

URL namespacing

Хотя технически это не является лучшей практикой, специфичной для системы шаблонов Django, использование пространств имен в ваших URL Django значительно упрощает разработку внутренних шаблонов.

Я считаю, что пример 2 (ниже) гораздо легче для быстрого понимания, чем пример 1.

Пример 1

<a href="{% url 'news-year-archive' year %}">{{ year }} Archive</a></li>

Пример 2

<a href="{% url 'news:archive:year' year %}">{{ year }} Archive</a></li>

Пространства имен

URL позволяют нам иметь уникальные имена URL, даже если другое приложение использует такое же имя URL (например, create, detail и edit). Без использования пространств имен URL проект Django не мог бы иметь два URL с именем create. С помощью пространств имен мы можем называть и ссылаться на наши URL просто, без необходимости длинных сложных имен для каждого URL в нашем приложении.

URL с именем blog-article-create станет blog:articles:create, или users:profile:create, поскольку create больше не зарезервирован одним приложением в нашем проекте. Настроить это довольно просто.

Файл urls.py для примера 1 (выше) будет выглядеть примерно так:

blog/urls.py

from django.urls import path

from . import views

urlpatterns = [
    #...
    path('articles//', views.year_archive, name='news-year-archive'),
    #...
]

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

blog/urls.py

from django.urls import path

from . import views

archive_patterns = [
    path('/', views.year_archive, name='year'),
]

urlpatterns = [
    #...
    path('', include(archive_patterns, namespace='archive')),
    #...
]

urls.py

from django.urls import include, path

urlpatterns = [
    path('articles/', include('blog.urls', namespace='blog')),
]

Это позволяет нам перейти из пространства имен URL blog в пространство имен archive, где мы можем обозначить URL, которые будут вести себя точно так же, как путь articles/ внутри приложения blog. Рендеринг URL с помощью тега шаблона url также прост и интуитивно понятен (см. пример 2 выше)

Итоги

С шаблонами Django работать не сложно, но, как мы видели выше, есть несколько способов сделать работу в шаблонах Django еще проще:

  • Изучение и знание того, какие теги и фильтры являются встроенными и могут нам помочь
  • Структурирование наших папок шаблонов способами, предсказуемыми для Django и других разработчиков
  • Для оптимизации скорости страниц и соединений с базой данных, выбирайте и предварительно получайте связанные объекты перед доступом к атрибутам, которые охватывают отношения
  • Расстановка имен URL делает ссылки на них в ваших шаблонах предсказуемыми и доступными для людей, разрабатывающих ваше приложение или проект
Вернуться на верх