Пишем асинхронное web-приложение на Python3, шаблонизация

newbie

Опубликован:  2021-01-06T07:00:29.142291Z
Отредактирован:  2021-01-12T08:08:57.889281Z
1
0
0
Вы неавторизованы, рекомендую зарегистрироваться и авторизоваться.

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

1. Предварительная подготовка

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

cd ~/workspace/auriz
source venv/bin/activate

eU4wqJsiYv.png

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

Кроме этого, мне нужно запускать отладочный web-сервер, конечно же это можно сделать в этом же терминале, используя некоторые продвинутые консольные инструменты, но я так делать не буду. Я работаю в "иксах", поэтому имею возможность открыть второе окно терминала, в котором тоже войду в базовый каталог приложения и активирую виртуальное окружение. В этом терминале я запущу отладочный сервер. Так как проект в зачаточном состоянии, дебагер ещё не подключен, сервер ошибок отсутствует, отладочную информацию придётся черпать из этого терминала, и будет удобно иногда поставить два терминала рядом - "иксы" это позволяют.

pFPRaqlR6T.png

Кроме этого, я запущу web-браузер, зайду на стартовую страницу приложения и отключу кэш на вкладке "Network" инструментов разработчика.

Jv0w9qxXbq.png

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

2. Зачем нам шаблонизатор?

Вот так на текущий момент выглядит функция представления стартовой страницы приложения.

async def show_index(request):
    name = request.app.config.get('SITE_NAME')
    return HTMLResponse(f'<p>{name}, стартовая страница.</p>')

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

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

pip install jinja2

9wawVhUNwJ.png

Чтобы зависимость не потерялась, сразу после установки обновлю файл requirements.txt, в котором хранится список зависимостей, с помощью которого впоследствии можно будет восстановить виртуальное окружение, если это потребуется.

pip freeze > requirements.txt

trYXufBzje.png

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

3. Расширяем возможности приложения

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

vim auriz/__init__.py

В этом файле мне потребуются новые импорты:

...
import jinja2
import typing
...
from starlette.templating import Jinja2Templates

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

...
templates = os.path.join(base, 'templates')
...

Чтобы задать параметры шаблонизатора, я создам новый класс - наследник класса Jinja2Templates, и перепишу в этом классе метод get_env.

...
class J2Templates(Jinja2Templates):
    def get_env(self, directory: str) -> "jinja2.Environment":
        @jinja2.contextfunction
        def url_for(
                context: dict, name: str, **path_params: typing.Any) -> str:
            request = context["request"]
            return request.url_for(name, **path_params)

        loader = jinja2.FileSystemLoader(directory)
        env = jinja2.Environment(
            loader=loader, autoescape=True, extensions=[])
        env.globals["url_for"] = url_for
        return env
...

А экземпляр этого класса я подвяжу в свойство приложения.

...
app.jinja = J2Templates(directory=templates)

После всех правок файл получил следующий вид в редакторе.

zjdK82xwfF.png

Отлично, открываю в текстовом редакторе файл views.py из каталога main.

vim auriz/main/views.py

И модифицирую в нём функцию представления стартовой страницы - show_index:

...
async def show_index(request):
    return request.app.jinja.TemplateResponse(
        'main/index.html',
        {'request': request})

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

h5QfqbHl2g.png

Иду в браузер, стучусь по url-адресу стартовой страницы и получаю ожидаемый фэйл.

vztFonNc2F.png

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

mkdir -p auriz/templates/main
touch auriz/templates/main/index.html

0k8DVQ3ipu.png

Возвращаюсь в браузер и обновляю страницу.

iZNJZvtO3X.png

Теперь статус код запроса в норме, но меня постигла новая неудача, полученная страница не имеет кода, что естественно, я же просто создал index.html с помощью touch. Открываю этот файл в текстовом редакторе.

vim auriz/templates/main/index.html

И пишу в него следующее.

<!DOCTYPE html>
<html lang="ru">
  <head>
    <title>
      {{ request.app.config.get('SITE_NAME') }}: Сначала
    </title>
  </head>
  <body>
    <div>
      Сайт в стадии разработки, попробуйте зайти позже.
    </div>
  </body>
</html>

1NAHt0SGXG.png

Снова возвращаюсь в браузер и обновляю страницу.

TXcmUk9Izk.png

Аха... У страницы появился заголовок, который видно на вкладке и в заголовке окна браузера. Если попросить браузер отобразить исходник страницы, получится вот так.

ODPuDwVylV.png

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

4. Фронтенд, html и таблицы стилей

Итак, давайте посмотрим, что мы имеем на текущий момент...

Когда от браузера поступает запрос на сервер по url-адресу стартовой страницы, сервер отдаёт этот запрос в приложение auriz, которое в соответствии с картой url-адресов (параметр routes объекта app в файле конфигурации) передаёт этот запрос заданной функции представления (корутина show_index в файле views.py из каталога main), которая обрабатывает запрос, загружает шаблон, делает все заданные в шаблоне подстановки, формирует HTTP-ответ с полученным в результате обработки шаблона html-кодом и отдаёт клиенту. Все перечисленные действия выполняет сервер - работает backend приложения.

Браузер, получив от сервера HTTP-ответ, обрабатывает заголовок и совершает разбор полученного с ответом html-кода, затем рендерит в своём окне страницу в зависимости от содержащихся в html-коде таблиц стилей и исполняет имеющиеся в html-коде сценарии JavaScript. Все перечисленные действия выполняет клиент - работает frontend приложения.

Что это значит? Это значит, чтобы раскрасить полученную страницу в браузере, мне необходимо продумать и разработать html-код шаблона. Открываю в текстовом редакторе файл шаблона.

vim auriz/templates/main/index.html

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

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-COMPATIBLE" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="description"
          content="{{ request.app.config.get('SITE_DESCRIPTION') }}">
    <title>
      {{ request.app.config.get('SITE_NAME') }}: Сначала
    </title>
    <link rel="icon"
          href="{{ request.app.url_path_for(
            'static', path='images/favicon.ico') }}"
          type="image/vnd.microsoft.icon">
    <link rel="stylesheet" type="text/css"
          href="{{ request.app.url_path_for(
            'static', path='vendor/bootstrap/css/bootstrap.css') }}">
    <link rel="stylesheet" type="text/css"
          href="{{ request.app.url_path_for(
            'static', path='vendor/bootstrap/css/bootstrap-theme.css') }}">
  </head>
  <body>
    <nav id="navigation" class="navbar navbar-default">
      <div class="container-fluid">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle"
                  data-toggle="collapse" data-target=".navbar-collapse">
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand"
             href="{{ request.app.url_path_for('index') }}">
            <img alt="logo"
                 src="{{ request.app.url_path_for(
                   'static', path='images/logo.png') }}"
                 width="28" height="28">
          </a>
        </div>
        <div class="collapse navbar-collapse">
          <ul class="nav navbar-nav">
            <li class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                Области <b class="caret"></b>
              </a>
              <ul class="dropdown-menu">
                <li>
                  <a href="">В блогах</a>
                </li>
              </ul>
            </li>
          </ul>
          <ul class="nav navbar-nav navbar-right">
            <li class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                Действия <b class="caret"></b>
              </a>
              <ul class="dropdown-menu">
                <li>
                  <a href="">Войти</a>
                </li>
                <li>
                  <a href="">Получить пароль</a>
                </li>
              </ul>
            </li>
          </ul>
        </div>
      </div>
    </nav>
    <div id="main-container" class="container-fluid">
      <div class="row">
        <div id="content"
             class="col-lg-8 col-md-10 col-sm-10
                    col-lg-offset-2 col-md-offset-1 col-sm-offset-1">
          <div class="row">
            <div id="sub-content"
                 class="col-lg-10 col-md-10 col-lg-offset-1 col-md-offset-1">
              <div class="alert alert-warning">
                <div class="today-field"></div>
                <div class="message-text">
                  Сайт в стадии разработки, попробуйте зайти позже.
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <footer id="footer">
      <div class="container-fluid">
        <div class="footer-block"></div>
        <div class="footer-content">
          <div class="footer-left text-left">
            <img alt="right finger"
                 src="{{ request.app.url_path_for(
                   'static', path='images/footer-left.png') }}"
                 width="24" height="24">
          </div>
          <div class="footer-center text-center">
            <a id="footer-link"
               href="{{ request.app.url_path_for('index') }}">
              {{ request.app.config.get('SITE_NAME') }}
            </a>
          </div>
          <div class="footer-right text-right">
            <img alt="left finger"
                 src="{{ request.app.url_path_for(
                   'static', path='images/footer-right.png') }}"
                 width="24" height="24">
          </div>
          <div class="footer-bottom"></div>
        </div>
        <div class="footer-block"></div>
      </div>
    </footer>
    <script src="{{ request.app.url_path_for(
              'static', path='vendor/jquery-3.5.1.js') }}"></script>
    <script src="{{ request.app.url_path_for(
              'static', path='vendor/bootstrap/js/bootstrap.js') }}"></script>
    <script src="{{ request.app.url_path_for(
              'static', path='vendor/moment.js') }}"></script>
    <script src="{{ request.app.url_path_for(
              'static', path='vendor/ru.js') }}"></script>
  </body>
</html>

Здесь следует обратить внимание на заголовок страницы (<head></head>), у страницы появилась группа тегов meta, в том числе с именем "description" - описание сайта, текст этого тега берётся из соответствующей переменной файла настроек приложения, его туда необходимо вписать.

vim .env

Вписываю в этот файл одну единственную строчку:

...
SITE_DESCRIPTION='Списки auriz.ru - простое web приложения для ведения блогов в сети Internet'

Далее, следует обратить внимание на группу тегов link, которые задают иконку для избранного и стили Bootstrap.

В теле документа (<body></body>) тоже появились новые теги, которые используют классы Bootstrap. Поскольку я изменил содержимое файла .env, нужно остановить отладочный сервер и запустить его вновь, чтобы новые настройки подцепились приложением. Обновляю страницу в браузере.

956a2fmghq.png

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

vim auriz/static/css/base.css

И вписываю в этот файл следующий код.

html {
  position: relative;
  min-height: 100%;
}

body {
  margin-bottom: 90px;
}

#li-counter {
  display: none;
}

#navigation {
  margin: 0;
  background-image: linear-gradient(to bottom, #fffef2, #f3f2e7);
  box-shadow: 0 0 6px #f1dbc2;
}

.navbar-brand {
  padding-top: 10px;
}

#main-container {
  padding-top: 6px;
  padding-bottom: 6px;
  color: #83858f;
}

.error {
  color: #a94442;
  padding-left: 8px;
  margin-bottom: 0;
}

.alert {
  margin: 0;
}

.to-be-hidden {
  display: none;
}

#footer {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 90px;
  box-shadow: 0 0 1px #7f8c7f;
  background-color: #687368;
  background-image: linear-gradient(to bottom, #a2a19a, #74736e);
}

.footer-block {
  height: 30px;
}

.footer-left {
  width: 5%;
  float: left;
  margin-top: 5px;
}

.footer-center {
  width: 90%;
  float: left;
  padding-top: 4px;
  color: white;
}

#footer-link {
  color: white;
  text-decoration: none;
  text-shadow: 0 0 8px #d9dcec;
}

#footer-link:hover {
  cursor: pointer;
  text-shadow: 0 0 0 #d9dcec;
}

.footer-link-text {
  font-style: italic;
}

.footer-right {
  width: 5%;
  float: left;
  margin-top: 5px;
}

.footer-bottom {
  clear: both;
}

.current-user-name {
  margin: 0 4px 0 5px;
}

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

mkdir auriz/static/css/main
vim auriz/static/css/main/index.css

Содержимое этого файла будет выглядеть следующим образом:

.today-field {
  font-style: italic;
  font-weight: 600;
  font-size: 0.9em;
}

.message-text {
  font-style: italic;
}

Адреса новых файлов необходимо вписать в заголовок шаблона index.html.

Jfyi3UmNUq.png

Опять иду в браузер и обновляю страницу.

iFyTvNTRfC.png

Замечательно..! Если есть такая необходимость, теперь со страницей можно поэкспериментировать, меняя содержание файлов base.css и index.css, после каждого изменения обновляя страницу в браузере и отслеживая изменения внешнего вида страницы и её элементов в реальном времени, но в рамках этого обзора я так делать не буду, поскольку использую уже готовые, разработанные ранее таблицы стилей. А чтобы страница приняла окончательный для этого этапа вид, я добавлю ей ещё пару штрихов с помощью сценариев JavaScript, об этом далее...

5. Фронтенд и сценарии JavaScript

Используемые на странице сценарии JavaScript определяют поведение страницы и отклик её элементов на действия пользователя. На стартовой странице мне хотелось бы иметь тикающие цифровые часы в области сообщения, кроме этого, я хочу обработать определённым образом текст в подвале страницы.

Для обработки дат и времени я буду использовать библиотеку MomentJS, и первым делом я локализую эту библиотеку, русифицирую вывод её функций. Создаю новый файл.

vim auriz/static/js/custom-moment.js

И вписываю в него следующий текст.

function customizeMoment(moment) {
  moment.locale('ru');
  moment.updateLocale('ru', {
    longDateFormat: {
        LT: 'HH:mm',
        LTS: 'HH:mm:ss',
        L: 'DD.MM.YYYY',
        LL: 'D MMMM YYYY г.',
        LLL: 'D MMMM YYYY г., HH:mm',
        LLLL: 'dddd, D MMMM YYYY г., HH:mm'
    }
  });
  return moment;
}

Часики в области сообщений я реализую с помощью ещё одного сценария.

vim auriz/static/js/today-field.js
function showDateTime(moment) {
  return moment().format('LL') + ', ' + moment().format('HH:mm:ss');
}

function renderTodayField(cls, moment) {
  $(cls).text(showDateTime(moment));
  setInterval(
  function() {
    $(cls).each(function() {
      $(this).text(showDateTime(moment));
    });
  }, 1000);
}

Обрабатывать текст в подвале страницы я собираюсь с помощью ещё одного сценария.

vim auriz/static/js/footer.js
function formatFooter(moment) {
  var $footer = $.trim($('#footer-link').text());
  var html = '<span class="footer-link-text">' +
             $footer + ', ' + moment().format('YYYY') + ' г.</span>';
  $('#footer-link').html(html);
}

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

mkdir auriz/static/js/main
vim auriz/static/js/main/index.js

Этот файл получает следующий текст.

$(function() {
  moment = customizeMoment(moment);
  if ($('.today-field').length) renderTodayField('.today-field', moment);
  formatFooter(moment);
});

Чтобы всё это великолепие заработало, достаточно вписать полученные скрипты в конец шаблона index.html.

OHmboPytBT.png

В очередной раз возвращаюсь в браузер и обновляю страницу.

MKzLTTFwYS.png

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

6. Сохраняем проект в Git

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

cp .env env.template

Текущие изменения Git-репозитория выглядят следующим образом.

FkwRi8KVTg.png

Теперь можно сделать очередной коммит. Посмотреть текущее состояние auriz можно по этой ссылке.

7. Продолжение следует

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

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

Комментарии: