Всем привет! Меня зовут Иван Ситкин, я бэкенд-разработчик в Едадиле. Сегодня я хочу поделиться с вами историей написания очередной панели администрирования и как из этого мы собрали подходящие подходы и практики. Для начала давайте вспомним, что же это за панели. Панель администрирования (или админка) — это приложение, которое используется для управления и настройки приложения. То есть это такой продукт для продукта. Панели администрирования нужны для различных целей, например, для создания и редактирования контента, настройки параметров продукта или управления пользователями. Но иногда в проектах важна скорость и ресурсов на создание админки с привлечением команды фронтенда откровенно не хватает. И тогда бэкендеру приходится брать процесс в свои руки. А теперь вы готовы погрузиться в эту кроличью нору. Внимание! Эта статья не является призывом отменить существующие методологии разработки приложений на стыке фронтенда и бэкенда. Это статья о том, как мы пришли к нестандартной организации проектов, которая помогает нам быстро создавать и сопровождать наши элементы управления продуктом. Наша команда в большинстве случаев использует 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, то отказ от транспиляции и сборки приложения в бандл, кажется логичным. То есть как в нулевых: При этом, конечно, никакой myframework.js мы писать не хотим. Мы хотим подобрать такой, который может работать без транспиляции. А app.js — это не минифицированный код нашей админки. Таким образом наш бекенд на Python сможет раздавать приложение как обычную статику, а вместо минификации просто будем раздавать gzip-файлы. Однако есть небольшая проблема: в современной индустрии фронтенда так не принято. Поразмыслив над всем этим, мы изобрели (может, и не мы) обратный путь. Встречайте – 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 и начали раздавать его бэкендом. Выглядел он примерно вот так: Как можно заметить, мы вставляли компоненты прямо в 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 и решили попробовать на нём. Теперь сборка зависимостей выглядит следующим образом: Управляем зависимостями стандартным образом — у нас yarn (но можно, при желании и npm). Указываем нужные нам объекты в файле vendor.js. Собираем его через esbuild, сжимаем gzip и так же храним в репозитории. Повторяем всё с шага 1, когда обновляем зависимости. Последний пункт на практике нужен довольно редко: разработчик 98% времени пишет саму админку, а не зовёт yarn, npm или webpack, как это было бы при стандартном подходе. Лишь иногда отдельным PR обновляется vendor и коммитится в репозиторий. Более того, у новичков может быть даже не установлен ни npm, ни yarn, ни даже node — просто делаешь: и пишешь формы, потому что все зависимости к тебе уже приехали собранными. В итоге концептуально всё осталось прежним: тонкий клиент, который раздаётся бэкендом напрямую без сборки после каждого Merge Request. Однако есть несколько изменений: Мы не сильно проиграли в скорости работы на наших небольших SPA. Убедились в этом, измерив в песочнице. Используем современные модули, которые даже визуально чище старых, а также позволяют управлять областью видимости тех или иных объектов. Теперь не нужно поддерживать топологическую сортировку, ведь она сама размотается через import или export.  В итоге мы сохранили все плюсы и получили парочку бенефитов. На текущий момент мы используем именно этот вариант для наших админок. Если вам интересно пощупать это нечто, пример этого монстра в зародыше есть вот тут, а свою песочницу поднять можно через cookiecutter-шаблон. А если вам интересна отдельная статья на тему того, как стартовать проект, используя наш шаблон, не забудьте написать об этом в комментариях. Само собой, мы регулярно оглядываемся на то, что происходит в индустрии. Мы не отменяем сборку как идею, скорее мы нашли способ организации проекта и взаимодействия между элементами, который позволяет нам быстро и без большой боли собирать инструмент для управления продуктом. Если у вас схожи потребности, то, может быть, такой подход поможет и вам. Так как всё же заставить бэкендера писать фронтенд? Краткий ответ звучит так — никак. Критически важно избегать ситуации, когда добавление простой формы приводит к панике и необходимости быстро освоить десяток новых инструментов. Вместо этого необходимо сделать процесс создания админок настолько простым, насколько это вообще возможно. Чтобы человек мог оставаться в своей роли в команде и жил по принципу: «Я так-то бэкендер, а написание формочек это так, для души».