Работа с React без мета-фреймворка: скрытые издержки Python-бэкенда
Почему использование React вместе с Vite на не-JavaScript-бэкенде избавляет от дискуссий о Next.js, какие функции фреймворков приходится переписывать, и как справляться с проблемами интеграции.
Большинство дискуссий о фреймворках React предполагают наличие лишь двух вариантов: оставаться с Next.js или перейти на тот инструмент, который в настоящее время рекламируется как его преемник — будь то Astro, Remix или TanStack Start. Однако значительное количество реальных приложений на React не выбирает ни один из этих путей. Они используют Vite для отрисовки одностраничного клиента и взаимодействуют с бэкендом, написанным на Python, Ruby или PHP, который уже содержит базу данных, бизнес-правила и API. В этой статье честно рассматривается третий вариант: какие претензии к мета-фреймворкам вообще не имеют смысла, какие обязанности тихо переходят на ваш собственный код и как сохранять созданный таким образом фреймворк в рабочем состоянии.
Конкретный пример третьего варианта
Представьте себе систему общественного здравоохранения, которая отслеживает процесс обогащения пищевых продуктов питательными веществами. Фронтенд реализован на React и TypeScript с использованием библиотеки компонентов Ant Design. Серверная часть — это Frappe, Python-фреймворк, лежащий в основе ERPNext. Frappe определяет модели данных в виде «doctypes», управляет базой данных и предоставляет методы на Python из списка разрешенных как API-эндпоинты. В производственной среде отсутствуют движок Node.js, маршрутизатор приложений и компоненты сервера, отвечающие за определение того, какие части интерфейса могут взаимодействовать с базой данных. React отвечает за отрисовку экрана; всё остальное выполняет Frappe.
Для команды, находящейся в такой ситуации, многие из самых громких аргументов против Next.js оказываются нерелевантными. Зависимость от хостинга Vercel не имеет значения, когда там ничего не развернуто. Медленная замена модулей в режиме реального времени под действием маршрутизатора приложений также не является проблемой, поскольку сервер разработки запускается с использованием Vite, что позволяет быстро перезагружать контент, из-за чего об этом даже не задумываются.
Та же архитектура применяется во всех случаях, когда React интегрируется с Django, Rails, Laravel или любой другой стеком, уже имеющим собственную серверную часть. Однако остальные претензии к мета-фреймворкам по-прежнему существуют. Они просто возникают из другого источника.
Вы по-прежнему создаете фреймворк, по одной утилите за раз
Игнорирование мета-фреймворка не устраняет проблем, которые он решает. Оно лишь перемещает их в ваш кодовый базис, где вы сами решаете их, обычно постепенно и без четкого названия того, что вы создаете. Три примера из приложения fortification иллюстрируют эту схему.
Формы, основанные на схемах, вместо стандартов фреймворка
Команда создала компонент DynamicForm, который отображает формы непосредственно на основе определений doctype в формате JSON, предоставляемых бэкендом. Он считывает схему, определяет, какие поля существуют и какой у них тип, отмечает обязательные поля, обрабатывает поля, видимость или значение которых зависит от другого поля, и отображает полную форму. Для добавления нового doctype больше не требуется вручную писать форму.
Это пример применения конвенций вместо настройки — основного принципа каждой мета-рамки, реализованного внутри проекта. Это также не сработало с первой попытки: компонент прошел три полных переписывания, прежде чем стал стабильным. Обычная рамка позаботилась бы о таких итерациях от вашего имени среди тысяч пользователей.
Пагинация загрузки без слоя данных
В отчете говорится о необходимости загружать строки пачками по мере прокрутки пользователя. Без getServerSideProps, инструмента для загрузки маршрутов или любого другого средством фреймворка для получения данных, решение было создано из отдельных компонентов: IntersectionObserver, отслеживающего элемент в нижней части списка, вручную управляемого курсора страницы и состояния загрузки, поведение которого пришлось разрабатывать с нуля. Следует ли показывать индикатор загрузки внизу или новые строки должны просто появляться постепенно? Не имея возможности опираться на рекомендации фреймворка, один разработчик принимает это решение в обычный рабочий день.
В среде React-plus-Vite библиотека вроде TanStack Query может обеспечить большую часть необходимого (бесконечные запросы, курсоры, флаги загрузки), и стоит рассмотреть её использование прежде чем писать логику самостоятельно. Основная идея остаётся прежней: кто-то должен выбрать и взять на себя ответственность за слой данных.
Слой адаптеров для неоднородных данных бэкенда
В выпадающих списках примерно десяти типов данных необходимо было отображать читаемые человеком метки, но каждый тип данных называл своё поле с меткой по-разному. Некоторые использовали label, другие — title, а один устаревший тип данных использовал display_name. Небольшая утилита mapOptions нормализует их все до единого формата, чтобы каждый компонент выпадающего списка мог игнорировать эти несоответствия.
Никто бы не назвал это архитектурой, однако именно такая нормализация обычно внедряется с помощью стандартов фреймворков. Каждая команда в конечном итоге пишет ту самую структуру, которую предоставил бы мета-фреймворк. Единственный выбор заключается в том, заплатите ли вы за это, принимая точку зрения фреймворка, или потратите собственное время разработчика.
Когда самодельный фреймворк становится проблемой
Настоящая стоимость проявляется позже. Через несколько лет компонент DynamicForm был отмечен SonarQube как имеющий чрезмерную когнитивную сложность. Это не была ошибка библиотеки или несовместимость версий — это было обычное развитие компонента: один файл тихо превратился одновременно в слой маршрутизации, слой форм и слой проверки данных приложения.
Рефакторинг показал, насколько запутанной стала структура кода. Перенос логики проверки в отдельную функцию выявил четыре независимых пути выполнения кода, которые зависели от побочных эффектов, скрытых внутри компонента. Это та «налоговая нагрузка», которую редко видишь в счете-фактуре, когда пренебрегаешь мета-фреймворком: вы сами создаете инфраструктуру и одновременно становитесь её единственной документацией. Не существует популярных инструментов для отслеживания проблем, где кто-то уже столкнулся с аналогичной проблемой и опубликовал решение.
Решение было основано на хорошо знакомом принципе декомпозиции:
- Перенести обработку зависимостей между полями в отдельный пользовательский хук.
- Разместить логику проверки в отдельном модуле, который не имеет никакого представления о том, как отображается форма.
- Позволить самому компоненту отвечать исключительно за её отображение.
После разделения сложность кода снизилась, и компонент стал легче для восприятия. Более общий урок заключается в том, что принцип разделения ответственностей, обеспечиваемый мета-фреймворком по умолчанию, в пользовательской стек-технологии должен соблюдаться строго каждый раз. Никакой инструмент проверки кода не предупредит вас о том, что вы неправильно перестраиваете слой данных фреймворка. Полезными привычками здесь являются периодические отчеты о сложности кода, ревью кода с вопросом «Превращается ли этот компонент в инфраструктуру?», а также краткая внутренняя документация для любых утилит, от которых зависят более нескольких экранов.
Баги, существующие на стыке двух экосистем
Фреймворк вроде Next.js управляет как серверной, так и клиентской частью, поэтому граница между ними рассматривается как едина система. Когда React используется с Python-бэкендом, эту границу необходимо определять самостоятельно, что приводит к появлению особого класса багов.
Один пример: ошибка MySQL 1292, устранение которой заняло почти целый день. Эта ошибка указывает на недействительное или неправильно сформатированное значение даты или времени. Форма в React отправляла данные в одном формате, тогда как ORM Frappe ожидал другой. Ни фронтенд, ни бэкенд по отдельности не были неправы; проблема возникла из-за несоответствия форматов между двумя экосистемами, которые никогда не разрабатывались вместе и были связаны лишь общепринятыми правилами, придуманными месяцы назад и частично забытыми.
Другой пример: неожиданная активация проверки силы пароля внутри хука after_insert бэкенда, вызванная форматом данных, который отправляла форма и который никто не тестировал. В чатлоге ни одной фреймворков нет информации об этой взаимодействии, поскольку ни один мета-фреймворк не знает о существовании Frappe. Для отладки необходимо изучать как ваш код, так и исходный код бэкенд-фреймворка, пока обе стороны проблемы не совпадут.
Некоторые практические способы защиты от ошибок, связанных с формами:
- Сериализуйте даты и временные метки ровно в одном месте на стороне клиента, в формате, указанном на серверной части, а не для каждой отдельной формы.
- Рассматривайте данные запроса как контракт: оформите их на TypeScript и, по возможности, проверяйте их на обоих концах связи.
- Пишите тесты интеграции, которые отправляют реальные данные форм в реальную серверную часть, включая нестандартные форматы, поскольку скрипты на стороне сервера реагируют на данные, которых вы можете не ожидать.
- Фиксируйте сырые данные запроса при неудачной проверке на сервере, чтобы непостоянства становились сразу заметными.
Если ваш бэкенд также написан на JavaScript, совместная схема валидации устраняет большую часть этих трудностей; см. использование одной схемы Zod как в React-фронтенде, так и в Node-бэкенде. При использовании бэкенда на Python код нельзя совместно использовать, но контракт можно всё равно общий.
Правильное формулирование решения
Дискуссии о «каком мета-фреймворке» предполагают, что бэкенд можно изменить по соглашению. Для многих команд это не так. Если у вас уже есть приложение на Django, Rails, Laravel или Frappe, которое управляет сервером, то использование React вместе с Vite — это не компромисс, а естественная архитектура. Настоящий вопрос заключается в том, сколько функций фреймворка вы готовы разрабатывать и обслуживать сами на основе уже существующего бэкенда.
Взамен вы можете выбрать бэкенд, соответствующий вашей сфере деятельности. Для системы, построенной на doctypes и Python-хуках, такая свобода имеет большую ценность. Однако за неё приходится платить, принимая все последствия, которые возникают в результате такого выбора.
Основные выводы
- Аргументы о привязке к Vercel и необходимости миграции на app router действительно существуют, но они не касаются клиентских частей на React, работающих с бэкендом, отличным от JavaScript.
- Даже без мета-фреймворка вам всё равно придётся создавать слои для форм, загрузки данных и их нормализации; вы просто делаете это сами и остаётесь ответственными за них на постоянной основе.
- Инфраструктура, разработанная самостоятельно, склонна превращаться в запутанные компоненты; сознательно соблюдайте принцип разделения ответственностей и документируйте общие утилиты.
- Ответственность за границу между клиентом и сервером лежит на вас: централизуйте процесс сериализации, определите структуру данных и тестируйте всё на реальном бэкенде.