Всем привет! Меня зовут Иван Ситкин, я бэкенд-разработчик в Едадиле. Сегодня я хочу поделиться с вами историей написания очередной панели администрирования и как из этого мы собрали подходящие подходы и практики.

Для начала давайте вспомним, что же это за панели. Панель администрирования (или админка) — это приложение, которое используется для управления и настройки приложения. То есть это такой продукт для продукта. Панели администрирования нужны для различных целей, например, для создания и редактирования контента, настройки параметров продукта или управления пользователями.

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

А теперь вы готовы погрузиться в эту кроличью нору.

Внимание!

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

Точка отсчёта

Наша команда в большинстве случаев использует Python, поэтому неудивительно, что в своё время для админок был выбран Flask вместе с Flask-Admin. Это лёгкие и доступные инструменты с достаточно богатым набором примеров, документацией, сообществом, а также с открытым исходным кодом.

Админки в нашем понимании — это набор страниц, которые формируют интерфейс для выполнения типичных операций над объектами, то есть создание, чтение, обновление и удаление (CRUD). Но со временем стало заметно, что такие проекты команде стало дорого сопровождать, так как требовалось прикладывать намного больше усилий для составления логики поведения страницы. Например, добавлять поле с выбором объектов из нового API. Тогда мы решили посмотреть, как такую проблему решают другие команды и комьюнити.

В интернете пишут, что правильно вот так

Вы наверняка знаете, что есть такой подход к проектированию веб-приложений — API First . Если коротко, то это означает, что сначала разработчики формируют набор контрактов, или какую-то спецификацию API, а потом все вместе реализуют её как на бэкенде, так и на фронтенде.

Разумеется, такой подход имеет ряд преимуществ:

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

  • Возможность предоставить доступ к API третьим лицам. Например, разработчикам сторонних продуктов для интеграции с вашим приложением.

Однако, у этого подхода есть ряд серьёзных ограничений:

  • Очень сложно менять контракт и спецификацию API.

  • Если понадобится, придётся поддерживать несколько версий API, чтобы не нарушить существующие интеграции.

  • Необходимо поддерживать и развивать две отдельные части — бэкенд и фронтенд.

Именно из-за этих ограничений цена разработки сильно возрастает. Ниже попробую показать почему.

Давайте пофантазируем о том, как именно это бывает

Есть две команды: фронтенд и бэкенд. Давайте, для нашего карикатурного примера назовём их «Хищники» и «Травоядные Чужие». Работают они по SCRUM, используют API First. Так как основной продукт — приложение на iOS, Android и Web, этот подход отлично работает. Зачем нам изобретать велосипед — давайте и админки писать точно так же.

Итак, команде «Чужих» ставим задачу на спринт: сделать спецификацию API. В особо удачных случаях команда дизайна успела нарисовать макет и согласовать его у ответственного за UX.

Разработчик целую неделю проектирует на Swagger. Смотрит в СУБД проекта, а там не хватает пару полей. «Чужой» заводит задачу на миграцию в следующий спринт (этот-то уже битком). Потом делает спецификацию API, отдаёт её на ревью своей команде. Разумеется, очень опытный коллега находит, скажем, шесть мест, где могут возникнуть проблемы с безопасностью. После пары часов обсуждений задачу закрывают, но заводят ещё три задачи в бэклог: на починку потенциальных и сугубо теоретических проблем в безопасности ещё нереализованного API и собственно на реализацию самого API.

Прошёл спринт, задачи закрыты, все молодцы. Отдаём Swagger команде «Хищников». Разработчик с их стороны через пару дней закончил писать абстрактный класс клиента и тесты к нему. Наконец, можно открывать макет и начинать верстать!

Не буду тут ударяться в подробности, скажу лишь, что в Swagger-API, например, нет поля, по которому фронтендер будет сортировать результаты. Ну и обязательно найдутся ещё какие-нибудь неочевидные штуки.

И что же мы имеем по истечении двух недель:

  • задачи на безопасность, миграцию данных, само API и «ещё что-нибудь» в бэклоге «Чужих»; 

  • задачи на сборку фронта и деплой ноды у «Хищников»;

  • макет от дизайнеров;

  • пачка задач от главного за UX.

И главное: бэкенд не готов, фронтенд не готов.

Если серьёзно, то пример выдуман для подчёркивания проблем, так что любые совпадения с реальными персонажами — чистая случайность. Но вы можете написать в комментариях, было у вас что-то такое или нет.

Что же тут пошло не так

Всё же требования к основному продукту значительно отличаются от требований к админке, как минимум в таких критериях:

  • Сроки релиза. Надо сделать админку намного быстрее, чем основной продукт.

  • Требования по поддержке платформ. Можно забить на IE9, а в особо запущенных случаях вообще сказать всем сотрудникам поставить определённый браузер только для этой админки, или привезти им «правильный браузер» через AD-профиль. На бизнес это никак не повлияет, но мы делать этого, конечно же, не будем.

  • Требования к качеству. Если не работает кнопка сортировки, клиент не уйдёт к конкурентам (потому что чаще всего это наш сотрудник).

  • Требования к поддержке. В случае основного продукта /api/v1/ надо поддерживать до тех пор, пока есть хотя бы один клиент, использующий его. Но в случае админок нужен только самый актуальный клиент.

Как исправить ситуацию

Так как мы всё-таки инженеры, давайте сформируем требования и критерии успеха. Да и вообще, что мы будем делать и как оценивать результат.

  • Выкатывать на прод новую форму админки в среднем за день.

  • Приложение корректно работает в современных браузерах: Safari, Firefox и весь Chrome-based.

  • Та часть админки на Python, с которой взаимодействует JS, покрыта тестами.

  • В JS-части должно быть минимум логики, тесты на JS мы писать не хотим (в идеале, конечно, совсем не писать на JS, но это по ситуации).

  • Можно быстро ломать API, но при этом не ломать клиент.

Наш идеальный мир — это отдельный микросервис, который раздаёт JS-код, а он, в свою очередь, и является его админкой. То есть первый критерий успеха — когда коды клиента и бэкенда мирно сосуществуют в одном репозитории (это необязательно, но было бы приятным бонусом).

Так как бэкенд у нас на Python, а фронтенд — на JS, то отказ от транспиляции и сборки приложения в бандл, кажется логичным. То есть как в нулевых:

<script src="/static/myframework.js"></script>

<script src="/static/app.js"></script> 

При этом, конечно, никакой myframework.js мы писать не хотим. Мы хотим подобрать такой, который может работать без транспиляции. А app.js — это не минифицированный код нашей админки. Таким образом наш бекенд на Python сможет раздавать приложение как обычную статику, а вместо минификации просто будем раздавать gzip-файлы.

Однако есть небольшая проблема: в современной индустрии фронтенда так не принято.

API Last

Поразмыслив над всем этим, мы изобрели (может, и не мы) обратный путь. Встречайте – API Last. Вот что мы вкладываем в этот подход:

  • Клиент останется только один. Для нас это Дункан МакКлауд JS-клиент в браузере.

  • Если нам понадобится второй клиент, напишем для него отдельный микросервис. Спойлер: в случае админок такого ещё не случалось.

  • Мы не публикуем контракт, а пишем UI-часть вместе с бэкенд-частью. В идеале прямо в одном репозитории.

Так как мы не хотим использовать полноценную сборку, значит, фреймворк-дистрибутив должен быть собран под браузер и иметь возможность использовать бо́льшую часть фичей фреймворка в рантайме, например, шаблоны.

На дворе был 2019 год. На тот момент было три популярных фреймворка: React, Angular и Vue. 

  • React можно собрать под браузер, а в коде приложений использовать не JSX, а VDOM API. Но код админки будет выглядеть сложным и трудночитаемым.

  • В Angular все примеры в документации были на TS, а рантайм интерпретатора нет. Да и через 4 года мало что изменилось.

  • У Vue есть сборка под браузер. Можно использовать как VDOM API, так и компилировать шаблоны в рантайме. Как раз то, что нам и нужно.

Первый полёт

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

Мы взяли уже подготовленный Vue c unpkg, добавили его в index.html и начали раздавать его бэкендом. Выглядел он примерно вот так:

<head>
  <script src="https://unpkg.com/vue@2.7.14/dist/vue.js"></script>
  {% for component in components %}
    {{ component }}
  {% endfor %}
</head>

<body>
  <div id="app"></div>
</body>

Как можно заметить, мы вставляли компоненты прямо в index.html, используя Jinja. Но мы быстро поняли, что для компонент нужна топологическая сортировка, так как они могут быть зависимыми друг от друга в неочевидном порядке.

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

Затем пришло понимание, что нам мало одного Vue. Не вопрос, добавили ещё один скрипт из unpkg. Но чтобы не тянуть сорок мульёнов зависимостей, пришёл мой коллега @orlovdl и написал скрипт, который качает из unpkg библиотеки и пакует в один жирный vendor.js. А так как набор библиотек меняется не так часто, то и vendor.js (а в последствии vendor.js.gz), мы решили хранить прямо в репозитории.

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

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

Кому интересны детали, милости просим ворваться вот сюда. В этом репозитории представлены аналоги наших первых прототипов.

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

Конечно, можно использовать обычный REST API в формате json, а уже на стороне клиента вызывать API, десериализовать json в объекты и обрабатывать ошибки. Но наш подход – если можно не писать JS, то мы и не будем. А для aiohttp есть полноценный пакет, который мы и решили использовать — wsrpc-aiohttp.

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

Промежуточные результаты

Теперь можно проверить, каких целей, которые мы обозначили для себя выше, удалось достичь:

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

  • Приложение корректно работает в современных браузерах.

  • Часть на Python, с которой взаимодействует JS, покрыта тестами, как и любой другой наш API.

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

  • Можно быстро ломать API и тут же чинить клиент или дописывать новый RPC-вызов, а старый просто убрать. И теперь нет нужды поддерживать устаревший код.

Но, конечно, не всё так радужно. Есть и менее приятные моменты:

  • Нужно писать компоненты в старом стиле. (такие модули используют особенность языка, где создается замыкание и тут же вызывается для изменения глобальной области видимости)

  • Нужно поддерживать топологическую сортировку руками.

  • Не все библиотеки собраны под браузер.

  • Область видимости модулей доступна прямо из консоли.

Если вам интересно, поднять свою песочницу можно через этот шаблон.

Также, но по-новому

Во второй половине 2020 года вышел Vue3. И, конечно же, мы захотели на него перейти.

В один из дней мозгового штурма пришёл @dizballanze и рассказал про появление нескольких CDN: например, esm.sh, которая предоставляет библиотеки аналогично unpkg, но в формате esm. А его вполне поддерживают современные браузеры. Ко всему прочему, новая версия Vue уже шла со сборкой в этом формате. Нормальные импорты слишком сильно манили нас в очередное подземелье, но не тут-то было...

Мы обнаружили главную проблему – Vue Single-File компоненты требуют определённой предобработки. Фактически нам нужно повторить наш загрузчик, который разбивал SFC на блоки: шаблон, стиль, скрипт. Но, само собой, кастомный fetch браузеры не поддерживают и мы взяли перерыв на очередные исследования.

В качестве решения мы нашли es-module-shims, который расширяет поддержку в браузерах, а также в режиме shim позволяет использовать кастомный fetch. Это дало нам возможность переиспользовать загрузчик и продолжать использовать Single-File компоненты (ну, нравятся они нам).

Теперь дело за малым: собрать vendor в нужном формате, так как тянуть библиотеки с CDN через importmap не даёт нужного управления кэшем и контентом. В очередной раз мы пошли шерстить интернет в поисках подходящего инструмента. Наткнулись на быстрый-модный-молодежный esbuild и решили попробовать на нём.

Теперь сборка зависимостей выглядит следующим образом:

  1. Управляем зависимостями стандартным образом — у нас yarn (но можно, при желании и npm).

  2. Указываем нужные нам объекты в файле vendor.js.

  3. Собираем его через esbuild, сжимаем gzip и так же храним в репозитории.

  4. Повторяем всё с шага 1, когда обновляем зависимости.

Последний пункт на практике нужен довольно редко: разработчик 98% времени пишет саму админку, а не зовёт yarn, npm или webpack, как это было бы при стандартном подходе. Лишь иногда отдельным PR обновляется vendor и коммитится в репозиторий. Более того, у новичков может быть даже не установлен ни npm, ни yarn, ни даже node — просто делаешь:

 git clone $projectUrl

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

В итоге концептуально всё осталось прежним: тонкий клиент, который раздаётся бэкендом напрямую без сборки после каждого Merge Request. Однако есть несколько изменений:

  • Мы не сильно проиграли в скорости работы на наших небольших SPA. Убедились в этом, измерив в песочнице.

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

  • Теперь не нужно поддерживать топологическую сортировку, ведь она сама размотается через import или export

В итоге мы сохранили все плюсы и получили парочку бенефитов.

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

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

Вместо заключения

Так как всё же заставить бэкендера писать фронтенд? Краткий ответ звучит так — никак.

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

Вместо этого необходимо сделать процесс создания админок настолько простым, насколько это вообще возможно. Чтобы человек мог оставаться в своей роли в команде и жил по принципу: «Я так-то бэкендер, а написание формочек это так, для души».