Подключаем статические файлы в web-приложение на Python и Starlette

programming

Опубликован:
2024-11-30T07:29:21.909546Z
Отредактирован:
2024-11-30T07:29:21.909546Z
Статус:
публичный
20
0
0

Web-приложение на Python и Starlette, разработку которого мы исследуем в этом цикле статей, будет иметь код, состоящий из двух составляющих: код исполняемый на сервере (backend), и код исполняемый на клиенте (frontend). На текущий момент в структуре каталогов разрабатываемого приложения уже предусмотрены каталоги для хранения серверного кода, а вот с клиентским кодом пока всё плохо, его пока положить некуда. Этот недочёт я и буду исправлять в этой демонстрации — мне предстоит создать структуру для хранения статических файлов и обеспечить статические файлы адекватными url-адресами. Об этом далее в мелких деталях, с пояснениями и картинками, как мы любим.

Об особенностях статических файлов

Статические файлы называют так потому, что они остаются неизменными на всём протяжении времени исполнения web-приложения, в отличие от HTML страниц, которые генерируются сервером и могут меняться от запроса к запросу. Статика обычно делится на два типа:

  1. Бинарные файлы, в эту группу падают файлы изображений;

  2. Тестовые файлы, к этой группе относятся файлы с таблицами стилей и сценариев JavaScript.

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

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

Установка Webassets

Как и любую другую стороннюю библиотеку, Webassets необходимо установить в виртуальное окружение проекта. Для этого запускаю терминал, вхожу в корневой каталог приложения и запускаю его виртуальное окружение. У Webassets есть две необязательные зависимости, с помощью которых осуществляется минимизация кода каждого типа, это cssmin — для минимизации .css файлов, и rjsmin — для минимизации .js файлов. Находясь в терминале с активным виртуальным окружением я могу установить в него все три библиотеки одной простой командой.

$ pip install webassets cssmin rjsmin

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

CMtRtba3Fi.png

Каталог для хранения статических файлов

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

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

$ vim webapp/dirs.py

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

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

Здесь я выразил адрес вложенного в базовый каталог каталога static в терминах Питона и сохранил его в одноимённом объекте — static. Вписать эту строчку можно в любое место файла, вот что можно увидеть в окне моего текстового редактора.

wFhwzt0EaC.png

Сохраняю изменения в файл, остаюсь в текстовом редакторе.

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

:!mkdir webapp/static

Вложенный в каталог static каталог с именем css предназначен для хранения файлов с таблицами стилей. Создаю его.

:!mkdir webapp/static/css

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

:!mkdir webapp/static/images

Для сценариев JavaScript я предусмотрю каталог с соответствующим именем — js. Одновременно с ним создаю вложенный в него каталог с именем main, в нём будут храниться сценарии главной подпрограммы web-приложения. Создаю эти каталоги.

:!mkdir -p webapp/static/js/main

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

Дополнительная конфигурация ASGI

Конфигурация созданного в предыдущих выпусках этого блога экземпляра ASGI приложения хранится в "ините" базового каталога. Открываю его в активном на данный момент окне Vim.

:tabnew webapp/__init__.py

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

from starlette.routing import Mount, Route
from starlette.staticfiles import StaticFiles

Кроме этого, мне необходимо подключить к уже существующему экземпляру ASGI приложения инструменты Webassets. Их тоже нужно импортировать.

from webassets import Environment as AssetsEnvironment
from webassets.ext.jinja2 import assets

from .dirs import settings, static, templates

В предыдущем листинге с кодом следует обратить внимание, что из файла .dirs, который я редактировал только что, я импортировал в итоге три объекта.

Инструменты Webassets будут действовать в составе интерпретатора шаблонов Jinja2, его мы подключали в приложение с помощью соответствующего класса J2Templates, внутри его метода _create_env дописываю несколько новых строчек, вот как будет выглядеть этот код в новой версии.

class J2Templates(Jinja2Templates):
    def _create_env(
            self,
            directory: DI, **env_options: typing.Any) -> "jinja2.Environment":
        loader = jinja2.FileSystemLoader(directory)
        assets_env = AssetsEnvironment(static, '/static')
        assets_env.debug = settings.get('ASSETS_DEBUG', cast=bool)
        env_options.setdefault("loader", loader)
        env_options.setdefault("autoescape", True)
        env_options.setdefault("extensions", [assets])
        env = jinja2.Environment(**env_options)
        env.assets_environment = assets_env
        return env

Здесь я создал объект assets_env и с его помощью привязал в окружение только что созданный каталог с именем static; изменил свойство debug этого объекта и привязал его значение к полю ASSETS_DEBUG файла конфигурации приложения. Кроме этого, я добавил в словарь env_options интерпретатора шаблонов ключ extensions с соответствующим значением, это позволит управлять инструментарием Webassets уже на уровне шаблона. И, наконец, добавил окружению интерпретатора шаблонов только что созданное окружение Webassets.

Создать группу url-адресов для статики можно добавлением в список routes в параметрах app ещё одного элемента, который будет экземпляром класса Mount, созданного с соответствующими параметрами.

app = StApp(
    debug=settings.get('DEBUG', cast=bool),
    routes=[
        Route('/', show_index, name='index'),
        Mount('/static', app=StaticFiles(directory=static), name='static')])

И вот как выглядит "инит" базового каталога в своей текущей версии в окне моего текстового редактора, все изменения я выдели красными рамками.

VTA1RLb5fk.png

GRJRBQAGyv.png

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

:split .env

И дописываю в этот файл ещё одну строчку — поле ASSETS_DEBUG.

DEBUG=True
ASSETS_DEBUG=True

M5Utk40Pmh.png

Обращаю ваше внимание, что на текущий момент значением в этом поле включен режим отладки статических файлов. Выключить режим отладки можно изменив это значение с True на False.

Подключаем статику в шаблон

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

:tabnew webapp/templates/main/index.html

В этот шаблон я допишу два новых логических блока. Первый внутри тега <head>, вот как он будет выглядеть.

    {%- assets filters='cssmin', output='generic/css/main/index.css',
              'css/base.css' -%}
      <link rel="stylesheet" href="{{ ASSET_URL }}">
    {%- endassets -%}

В параметрах этого блока я задал имя фильтра — cssmin — эту библиотеку я уже установил в виртуальное окружение. Кроме этого, в параметре output я задал адрес для хранения минимизированного файла, с этим адресом мы будем иметь дело чуть позже при тестировании предложенных изменений. И, наконец, за параметром output я могу разместить список файлов, из которых впоследствии минимизированный файл будет сгенерирован, пока в этом списке один единственный файл — base.css, чуть позже я его создам.

Второй логический блок я создам в самом конце тега <body>, именно туда обычно помещают сценарии JavaScript. Вот как этот блок выглядит.

    {%- assets filters='rjsmin', output='generic/js/main/index.js',
              'js/main/index.js' -%}
      <script src="{{ ASSET_URL }}"></script>
    {%- endassets -%}

Здесь тоже следует обратить внимание на использованный фильтр в параметре filters и адрес минимизированного файла в параметре output. Список использованных для создания минимизированного файла сценариев пока тоже довольно скуден, в нём один единственный файл с именем index.js, и его тоже ещё только предстоит создать.

Вот как эти блоки выглядят в составе шаблона в окне моего текстового редактора.

OZZySbNHUK.png

Создаём статические файлы

Начнём с таблиц стилей. Создаю новый файл с именем base.css следующей командой прямо в текущем окне Vim.

:tabnew webapp/static/css/base.css

Для начала я задам на странице шрифт, его размер и цвет текста.

body {
  font-family: Arial, Helvetica, sans-serif;
  font-size: 14pt;
  color: dimgray;
}

Сохраняю изменения в файл.

Создаю файл с именем index.js.

:split webapp/static/js/main/index.js

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

function setA(ar, len) {
  for (let i = 0; i < len; i++) {
    ar[i] = Math.round(Math.random() * 99 + 1);
  }
}

function sortAr(ar) {
  let box, i, j;
  for (i = 1; i < ar.length; i++) {
    box = ar[i];
    j = i - 1;
    while (j >= 0 && box < ar[j]) {
      ar[j+1] = ar[j];
      j -= 1;
    }
    ar[j+1] = box;
  }
}

let target = new Array();
setA(target, 88);
console.log('Исходный массив:');
console.log(target);
sortAr(target);
console.log('Отсортированный массив:');
console.log(target);

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

0u7E0LAitI.png

Сохраняю изменения всех файлов и покидаю текстовый редактор. Пришла пора протестировать внесённые в конфигурацию web-приложения изменения.

Тестируем

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

$ python runserver.py

Обращаю внимание на выхлоп. Сервер запустился, никаких ошибок в терминал не вывел, работает в штатном режиме.

4HSdPbysm5.png

Запускаю браузер, но пока не спешу переходить по url-адресу стартовой страницы приложения. Остановимся и подумаем... Дело в том, что браузеры обычно кэшируют статические файлы, и это может помешать процессу тестирования и отладки достаточно радикально. Для начала в окне браузера следует отключить кэширование статики. Для этого запускаю инструменты разработчика (ctrl+shift+i), перехожу на вкладку Network и ставлю соответствующую галку, как показано на снимке экрана ниже.

kDLyCHYmI0.png

Перехожу на вкладку Console, на этой вкладке можно проконтролировать исполнение имеющихся на странице сценариев JavaScript.

Чего я ожидаю..?

  1. Я ожидаю что стартовая страница будет иметь заданные в файле base.css шрифт, его размер и цвет текста.

  2. Я ожидаю, что в результате исполнения сценария из файла index.js в консоли браузера появится соответствующий сценарию выхлоп.

Вбиваю в адресной строке браузера адрес стартовой страницы и жму enter.

J7PKdXoi9q.png

Как видно на снимке экрана выше, шрифт на странице, его размер и цвет текста изменились, если сравнивать с соответствующим снимком экрана из предыдущего выпуска этого блога. Кроме этого, на вкладке Console мы видим выхлоп соответствующих отладочных процедур, которые содержатся в конце файла index.js. И самое последнее, на странице зафиксирована ошибка GET запроса по адресу иконки для избранного. Эту же ошибку можно обнаружить в выхлопе отладочного сервера — иконки для избранного у этого web-приложения пока просто нет, я исправлю эту ошибку уже в ближайшем выпуске этого блога.

На что ещё следует обратить внимание? Конечно же обратить особое и пристальное внимание следует на исходный код полученной в итоге в браузере страницы, меня интересуют теги <link> и <script>. Вот как он выглядит.

O6lk9Mkj7F.png

Обращаю внимание на свойство href в теге <link>, и на свойство src в теге <script>, я выделил теги красной рамкой. Ссылки в этих тегах кликабельны, следует пройти по ним и убедиться, что браузер показывает код соответствующих файлов без минимизации.

А теперь я попробую остановить отладочный сервер, отредактировать файл настроек .env, изменить в нём значение в поле ASSETS_DEBUG с True на False, запустить отладочный сервер вновь, обновить страницу в браузере и открыть её исходный код. Вот как он будет выглядеть теперь.

xdeiilwH4Y.png

Как видно на снимке экрана выше, ссылки в указанных тегах изменились в соответствии со значением в параметре output соответствующих "ассетов" шаблона. И если проследовать по этим ссылкам, браузер покажет минимизированный код.

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

rL9jkljfGs.png

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

Подводим промежуточный итог

Процесс разработки web-приложения достаточно сложен, растянут по времени и требует внимания и усидчивости. На текущем этапе проектирования я определил место для хранения статических файлов, и у приложения кроме backend-а, появился ещё и frontend. При этом, развёртывая приложения на сервер, не нужно будет заботиться о минимизации статических файлов, они будут минимизированы автоматически. А в ближайшем выпуске этого блога мы поговорим о некоторых обязательных для современного web-приложения url-адресах и исправим статус код 404 у иконки для избранного.

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