Подключаем статические файлы в web-приложение на Python и Starlette
programming
Web-приложение на Python и Starlette, разработку которого мы исследуем в этом цикле статей, будет иметь код, состоящий из двух составляющих: код исполняемый на сервере (backend), и код исполняемый на клиенте (frontend). На текущий момент в структуре каталогов разрабатываемого приложения уже предусмотрены каталоги для хранения серверного кода, а вот с клиентским кодом пока всё плохо, его пока положить некуда. Этот недочёт я и буду исправлять в этой демонстрации — мне предстоит создать структуру для хранения статических файлов и обеспечить статические файлы адекватными url-адресами. Об этом далее в мелких деталях, с пояснениями и картинками, как мы любим.
Об особенностях статических файлов
Статические файлы называют так потому, что они остаются неизменными на всём протяжении времени исполнения web-приложения, в отличие от HTML страниц, которые генерируются сервером и могут меняться от запроса к запросу. Статика обычно делится на два типа:
-
Бинарные файлы, в эту группу падают файлы изображений;
-
Тестовые файлы, к этой группе относятся файлы с таблицами стилей и сценариев JavaScript.
С бинарными статическими файлами всё понятно — они обычно сжаты и оптимизированы. А вот с текстовыми статическими файлами есть небольшая проблема. С одной стороны, они должны быть удобными для чтения разработчиком и могу содержать комментарии, отступы, пустые строки и прочие мелкие технические детали полезные разработчику. С другой, сервер их каждый раз передаёт с каждым своим ответом каждому клиенту, и, чтобы сократить трафик и нагрузку на сервер, очень хочется их минимизировать, то есть убрать из них все лишнее, предназначенное только для удобного чтения разработчиком, и совершенно ненужное клиенту. Следовательно текстовые статические файлы должны существовать в двух версиях. Первая версия для разработчика и сопровождающего проект, вторая для клиентов, тех самых web-браузеров пользователей, которые посылают запросы на сервер, получают от сервера ответы, рендерят HTML-страницы ответов сервера и исполняют присланные в них сценарии.
Держать и контролировать две версии одного и того же файла достаточно накладно, чревато ошибками и сбоями, поэтому очень хочется создание минимизированных версий статических файлов автоматизировать и доверить серверу. С этой задачей призвана справиться ещё одна сторонняя библиотека Python, которую я буду использовать в составе серверного кода, — Webassets.
Установка Webassets
Как и любую другую стороннюю библиотеку, Webassets необходимо установить в виртуальное окружение проекта. Для этого запускаю терминал, вхожу в корневой каталог приложения и запускаю его виртуальное окружение. У Webassets есть две необязательные зависимости, с помощью которых осуществляется минимизация кода каждого типа, это cssmin — для минимизации .css
файлов, и rjsmin — для минимизации .js
файлов. Находясь в терминале с активным виртуальным окружением я могу установить в него все три библиотеки одной простой командой.
$ pip install webassets cssmin rjsmin
Эта команда установит перечисленные пакеты и отзовётся обильным выводом в терминал. Обращаю внимание на последнюю строчку выхлопа, если она выглядит также, как на снимке экрана ниже, то можно двигаться дальше.
Каталог для хранения статических файлов
Статические файлы web-приложения хранятся на сервере, именно он их обслуживает. Клиент получает доступ к статическим файлам посредством запросов по соответствующим url-адресам. То есть, для каждого конкретного статического файла в составе приложения должен существовать url-адрес, к которому клиент может обратиться и получить актуальную версию этого файла. Это значит, что мне необходимо адрес файла в файловой системе сервера сгруппировать с соответствующим этому файлу url-адресом.
Начать следует с создания в структуре приложения каталогов для хранения статических файлов каждого типа. Открываю в текстовом редакторе файл dirs.py
из базового каталога приложения.
$ vim webapp/dirs.py
В этот файл я впишу одну единственную строчку. Вот как она выглядит.
static = os.path.join(base, 'static')
Здесь я выразил адрес вложенного в базовый каталог каталога static
в терминах Питона и сохранил его в одноимённом объекте — static
. Вписать эту строчку можно в любое место файла, вот что можно увидеть в окне моего текстового редактора.
Сохраняю изменения в файл, остаюсь в текстовом редакторе.
Мне необходимо создать каталог с именем 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')])
И вот как выглядит "инит" базового каталога в своей текущей версии в окне моего текстового редактора, все изменения я выдели красными рамками.
Поскольку текущая конфигурация ссылается на ещё одно поле файла настроек, в него это поле следует добавить. Открываю файл настроек приложения.
:split .env
И дописываю в этот файл ещё одну строчку — поле ASSETS_DEBUG
.
DEBUG=True
ASSETS_DEBUG=True
Обращаю ваше внимание, что на текущий момент значением в этом поле включен режим отладки статических файлов. Выключить режим отладки можно изменив это значение с 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
, и его тоже ещё только предстоит создать.
Вот как эти блоки выглядят в составе шаблона в окне моего текстового редактора.
Создаём статические файлы
Начнём с таблиц стилей. Создаю новый файл с именем 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);
Вот как только что созданные файлы выглядят в окне моего текстового редактора.
Сохраняю изменения всех файлов и покидаю текстовый редактор. Пришла пора протестировать внесённые в конфигурацию web-приложения изменения.
Тестируем
Первое, чего я ожидаю в результате всех предпринятых действий и внесённых в код приложения правок, это запуск отладочного сервера без сбоев и отчётов об ошибках в выхлопе. Запускаю.
$ python runserver.py
Обращаю внимание на выхлоп. Сервер запустился, никаких ошибок в терминал не вывел, работает в штатном режиме.
Запускаю браузер, но пока не спешу переходить по url-адресу стартовой страницы приложения. Остановимся и подумаем... Дело в том, что браузеры обычно кэшируют статические файлы, и это может помешать процессу тестирования и отладки достаточно радикально. Для начала в окне браузера следует отключить кэширование статики. Для этого запускаю инструменты разработчика (ctrl+shift+i
), перехожу на вкладку Network
и ставлю соответствующую галку, как показано на снимке экрана ниже.
Перехожу на вкладку Console
, на этой вкладке можно проконтролировать исполнение имеющихся на странице сценариев JavaScript.
Чего я ожидаю..?
-
Я ожидаю что стартовая страница будет иметь заданные в файле
base.css
шрифт, его размер и цвет текста. -
Я ожидаю, что в результате исполнения сценария из файла
index.js
в консоли браузера появится соответствующий сценарию выхлоп.
Вбиваю в адресной строке браузера адрес стартовой страницы и жму enter
.
Как видно на снимке экрана выше, шрифт на странице, его размер и цвет текста изменились, если сравнивать с соответствующим снимком экрана из предыдущего выпуска этого блога. Кроме этого, на вкладке Console
мы видим выхлоп соответствующих отладочных процедур, которые содержатся в конце файла index.js
. И самое последнее, на странице зафиксирована ошибка GET запроса по адресу иконки для избранного. Эту же ошибку можно обнаружить в выхлопе отладочного сервера — иконки для избранного у этого web-приложения пока просто нет, я исправлю эту ошибку уже в ближайшем выпуске этого блога.
На что ещё следует обратить внимание? Конечно же обратить особое и пристальное внимание следует на исходный код полученной в итоге в браузере страницы, меня интересуют теги <link>
и <script>
. Вот как он выглядит.
Обращаю внимание на свойство href
в теге <link>
, и на свойство src
в теге <script>
, я выделил теги красной рамкой. Ссылки в этих тегах кликабельны, следует пройти по ним и убедиться, что браузер показывает код соответствующих файлов без минимизации.
А теперь я попробую остановить отладочный сервер, отредактировать файл настроек .env
, изменить в нём значение в поле ASSETS_DEBUG
с True
на False
, запустить отладочный сервер вновь, обновить страницу в браузере и открыть её исходный код. Вот как он будет выглядеть теперь.
Как видно на снимке экрана выше, ссылки в указанных тегах изменились в соответствии со значением в параметре output
соответствующих "ассетов" шаблона. И если проследовать по этим ссылкам, браузер покажет минимизированный код.
В качестве вишенки на торте, давайте посмотрим на структуру каталогов приложения после всех проделанных действий, вот как она выглядит теперь.
В этой структуре появились два новых каталога, я их пометил красными рамками на снимке экрана выше. Эти каталоги были созданы автоматически инструментами Webassets после отключения отладчика. Кроме этого, в структуре приложения появилось место статическим файлам, а все файлы классифицированы по типам и хранятся в соответствующих каталогах, и если у программы сменится разработчик, ему будет достаточно просто разобраться в этой структуре и понять, откуда что растёт.
Подводим промежуточный итог
Процесс разработки web-приложения достаточно сложен, растянут по времени и требует внимания и усидчивости. На текущем этапе проектирования я определил место для хранения статических файлов, и у приложения кроме backend-а, появился ещё и frontend. При этом, развёртывая приложения на сервер, не нужно будет заботиться о минимизации статических файлов, они будут минимизированы автоматически. А в ближайшем выпуске этого блога мы поговорим о некоторых обязательных для современного web-приложения url-адресах и исправим статус код 404
у иконки для избранного.
В рамках этого цикла статей я планирую показать поэтапно разработку системы авторизации пользователей этого web-приложения, для этого мне потребуется подключить в приложение много других полезных инструментов. Насколько далеко я зайду в реализации этого плана, зависит от активности в блоге — ваши посещения, подписки, лайки, комментарии, донаты служат главным мотивирующим меня фактором. Все статьи цикла доступны в хронологическом порядке по метке webapp. Не оставайтесь в стороне, продолжение следует, будет интересно...