Главная / Статьи

Состояние URL, состояние сервера и BFF: стек Vite для внутренних React-приложений

Почему аутентифицированная внутренняя React-платформа может получить лучшую поддержку от Vite, TanStack Router, TanStack Query и отдельного BFF, чем от Next.js, и когда это уже не справедливо.

Издатель: REACTAPP.TOP10 мин чтения2310 слов

Источники: Next.js is excellent. That’s exactly why you **shouldn’t** use it everywhere.

Когда создаётся новый проект на React, вопрос «Стоит ли использовать Next.js?» обычно отвечается утвердительно ещё до того, как кто-либо описывает функционал приложения. Для публичных сайтов с большим объёмом контента такой подход часто бывает правильным. Однако для платформы с аутентификацией, где пользователи целый день фильтруют таблицы и редактируют записи после входа, многие из самых сильных функций этой фреймворков решают проблемы, которых у приложения вообще нет. В этой статье рассматривается архитектура, которая действительно необходима такой платформе, показано, как композитный набор React, Vite, TanStack Router, TanStack Query и backend-for-frontend (BFF) соотносится с ней, и в конце приводится чек-лист для определения того, когда Next.js всё же будет более подходящим решением.

Ничто здесь не свидетельствует о слабости Next.js. Это одна из лучших фреймворков React на рынке. Аргумент заключается не в том, чтобы отказаться от него по умолчанию, а в необходимости проверки соответствия его возможностей поставленным требованиям.

Конкретный сценарий

Представим небольшую команду из пяти инженеров, которые разрабатывают внутреннюю платформу с более чем 100 маршрутами, более чем 300 точками входа API и примерно миллионом запросов к API в месяц. Они могли бы создать её с помощью Next.js. Вместо этого они выбирают React с Vite, TanStack Router для управления маршрутами, TanStack Query для работы с данными, а также отдельный BFF между фронтендом и бэкенд-сервисами. Через шесть месяцев разработки этот выбор остается верным, не потому что Next.js оказался неэффективным, а потому что платформа почти не использует те области, в которых Next.js демонстрирует высокую эффективность.

Для более полного сравнения обоих подходов в различных типах проектов см. Next.js или обычный React: подбор фреймворка для вашего проекта. Здесь акцент сделан на более узкой теме: что требуется от каждого слоя для аутентифицированного приложения с большим объемом данных.

Опишите приложение перед выбором фреймворка

Next.js стал стандартным решением, поскольку он объединяет в себе множество функций: маршрутизацию и лейауты на основе файлов, SSR и серверные компоненты, слой кэширования, мидлвары, код бэкенда в одном проекте, оптимизированные изображения и большую экосистему. Для многих продуктов такое объединение является идеальным. Однако более важный первый вопрос заключается в том, какое приложение вы создаете. Два React-приложения могут практически не иметь ничего общего с точки зрения архитектуры.

Публичный интернет-магазин

Типичный публичный интернет-магазин обычно включает:

  • тысячи страниц, доступных для любого пользователя
  • трафик с поисковых систем
  • реальные требования SEO
  • страницы продуктов, содержимое которых улучшается за счёт обработки на сервере
  • URL-адреса, доступные для распространения, и превью в социальных сетях
  • большое количество статического или медленно меняющегося контента

Платформа внутренних операций

Внутренние инструменты обычно включают:

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

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

Пользователи управляют приложением, а не просто читают его

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

SEO больше не является обязательным требованием

Возьмём маршрут вроде /customers/12345, который отображает запись авторизованного клиента с конфиденциальными данными. Вы явно не хотите, чтобы поисковые системы индексировали его. Нет необходимости, чтобы спайдеры находили его, и первый HTML-ответ не должен быть удобным для спайдеров. Страница ценна потому, что с ней взаимодействует авторизованный пользователь, а не потому, что её можно найти. Поэтому одна из основных причин выбора Next.js имеет небольшое значение. Это несоответствие требований, а не недостаток фреймворка.

Где подходят серверные компоненты, а где — нет

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

Customer
├── Profile
├── Loans
├── Payments
├── Transactions
├── Documents
└── Activity

Как только страница загружается, пользователь начинает её изменять. Например, он фильтрует список платежей:

Status: Failed
Type: ACH
Date: Last 30 days

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

Request → Render document → Read document

Он гораздо ближе к циклу взаимодействия:

User interaction
       ↓
Application state
       ↓
Server state
       ↓
API
       ↓
Updated UI

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

Рассмотрение URL как состояния приложения

Предположим, что пользователь операций находится на вкладке платежей конкретного клиента:

/customer/12345/payments

Они применяют набор фильтров:

status=failed
type=ach
amountMin=500
page=2

Если эти фильтры закодированы в адресе, URL будет выглядеть так:

/customer/12345/payments
  ?status=failed
  &type=ach
  &amountMin=500
  &page=2

В этот момент URL уже не является просто средством навигации; он становится частью состояния приложения. Один инженер может скопировать его коллеге, и тот окажется на том же клиенте, в той же вкладке, с теми же фильтрами и на той же странице результатов. URL сохраняется при перезагрузке и обеспечивает правильное поведение кнопки «Назад» в соответствии с ожиданиями пользователей. Именно для решения таких задач на множестве экранов и нужен маршрутизатор с типобезопасностью.

Что вносит TanStack Router

TanStack Router рассматривает маршрутизацию как важнейшую составляющую проектирования. Маршруты, параметры пути и параметры поиска могут быть описаны с использованием системы типов, что критически важно, когда в приложении более сотни маршрутов. При таком объеме маршрутизация уже не сводится к простому вопросу «какой компонент отображается здесь»; это уже часть архитектуры. Например, для такого маршрута:

/customer/$customerId/payments

вам нужно:

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

    Реальным центром внимания является состояние сервера

    В большинстве крупных внутренних приложений речь идет не столько о страницах, сколько о удаленных данных. Объекты, которые приложение постоянно читает и записывает, выглядят так:

    Customers
    Loans
    Payments
    Transactions
    Documents
    Users
    Accounts
    Reports
    

    Для каждого из них фронтенд должен отвечать на одни и те же вопросы:

    • Есть ли эти данные уже в памяти?
    • Не устарели ли они?
    • Следует ли их перезагрузить, и когда?
    • Что отображается в интерфейсе во время загрузки?
    • Что происходит, если запрос не удаётся?
    • Может ли другой компонент использовать этот результат?
    • Что меняется после мутации, и какие запросы необходимо аннулировать?
    • Что должно появиться мгновенно при возврате пользователя назад?
    • Может ли интерфейс обновляться оптимистично?

    Это и есть управление состоянием сервера, и именно для решения этой проблемы создан TanStack Query: кэширование по запросам, фоновое повторное получение данных, удаление дублирующихся запросов, хуки для мутаций и целенаправленное аннулирование данных. Поскольку роутер отвечает за состояние URL, а Query — за серверное состояние, у каждого слоя есть чёткая ответственность, а локальное состояние компонентов ограничивается исключительно вопросами интерфейса, такими как открыта ли диалоговая окно.

    Сохранение BFF в качестве границы API

    В этом сценарии фронтэнд не обращается напрямую к десяткам микросервисов. Он взаимодействует с BFF, который в свою очередь общается со сервисами. Такое разделение позволяет фронтэнду не знать деталей реализации бэкенда, включая то, собирается ли ответ из одного сервиса или пяти. BFF отвечает за контракт API.

    Это также обеспечивает чёткое разделение при развертывании. Фронтэнд представляет собой статические ресурсы, предоставляемые через CDN или статического хоста:

    Frontend
       ↓
    CDN / static hosting
    

    в то время как BFF работает как отдельный сервис перед микросервисами:

    BFF
       ↓
    Microservices
    

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

    Аутентификация остается на стороне сервера

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

    Browser
       │
       │ Sign in
       ▼
     Okta
       │
       │ Callback
       ▼
     BFF / Backend
       │
       ▼
     Redis
    

    React-приложение не является местом реализации архитектуры аутентификации; его задача — обеспечивать работу продукта, в то время как BFF отвечает за безопасность и соблюдение API-контракта. Полезным побочным эффектом является то, что токены доступа никогда не попадают в JavaScript браузера, что снижает риски уязвимостей типа XSS. Использование такого подхода в Next.js не обязательно хуже, но если в организации уже существует подобная архитектура, то сохранение её вне фронтенда делает всё проще и понятнее.

    Да, Next.js может справиться со всем этим

    Он действительно может. Можно создать приложение с использованием Next.js и клиентских компонентов, внутри него применить TanStack Query, настроить аутентификацию, подключить его к BFF и развернуть по своему усмотрению. Проблема не в возможностях фреймворка. Более важный вопрос — насколько большую часть фреймворка вы действительно будете использовать. Если большая часть приложения в итоге будет выглядеть так:

    'use client'
            ↓
    Client-side interactions
            ↓
    API calls
            ↓
    TanStack Query
    

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

    Каждый компонент стека имеет свою задачу

    Для подобных приложений разумным стандартным набором являются React, Vite, TanStack Router и TanStack Query, плюс BFF. Речь не идет о случайном объединении библиотек до тех пор, пока вы не создадите собственную фреймворк-систему. У каждого компонента есть четко определенная роль:

    • Vite обеспечивает инструменты сборки и сервер разработки.
    • React отвечает за отрисовку интерфейса.
    • TanStack Router управляет маршрутизацией и состоянием URL.
    • TanStack Query отвечает за серверное состояние.
    • BFF выполняет функции API и обеспечивает безопасность.

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

    Плюсы и минусы отказа от Next.js

    Использование композитных маршрутов сопряжено с определенными издержками. Вы теряете готовые решения и должны сами принимать решения, которые обычно принимает Next.js: выбор структуры проекта, правила разделения кода, способ обработки клиентских маршрутов статическим хостингом, а также синхронизацию обновлений нескольких библиотек. Требования к проекту также могут измениться. Если позже половина приложения должна стать общедоступной и важность SEO внезапно возрастет, Next.js кажется гораздо более привлекательным выбором. То же самое касается ситуаций, когда требуется интенсивная обработка на сервере или генерация статического контента, когда серверные компоненты приносят реальную пользу вашему дизайну, или когда вы действительно хотите, чтобы код фронтенда и сервера были тесно связаны в одном выпуске приложения. Следовательно, выбор не заключается в «Next.js или React» — речь идет о том, какая архитектура лучше всего подходит для конкретного приложения.

    Чек-лист для выбора

    Перед принятием решения ответьте на следующие вопросы:

    • Имеет ли приложение значительную публичную часть? Это сильная сторона Next.js.
    • Важен ли SEO? Тогда Next.js кажется более привлекательным вариантом.
    • Нужна ли серверная обработка или статическое генерирование контента? В этом аспекте тоже побеждает Next.js.
    • Способны ли серверные компоненты или действия сервера существенно улучшить дизайн? Тогда стоит серьезно рассмотреть Next.js.
    • Является ли приложение в основном аутентифицированным и высокоинтерактивным? В таком случае лучше выбрать React с Vite.
    • Использует ли организация уже слой BFF или API? Стоит рассмотреть возможность развертывания отдельного клиентского приложения.
    • Является ли состояние сервера ключевой проблемой? Тогда TanStack Query становится очень привлекательным решением.
    • Важны ли состояние URL и сложное маршрутизирование? В этом случае TanStack Router становится очень привлекательным выбором.

    Из этих ответов следует простое правило. Если у приложения большая публичная часть, стоит серьезно рассмотреть Next.js. Если приложение в основном требует аутентификации, особенно это внутренняя или административная платформа, начните с React и Vite, добавляя только необходимые компоненты. В целом ни один из этих стеков не лучше другого; каждый более подходит для конкретной задачи, причем это различие легко теряется, когда тот или иной фреймворк становится стандартным решением.

    Заключение

    Лучшая архитектура — это не та, у которой самый длинный список функций, а та, чьи функции решают реальные проблемы. Для инструмента операций с входом под проверкой, основанного на собственном слое бэкенда, с большим объемом серверного состояния, интенсивными взаимодействиями и минимальным количеством публичного контента, использование React с Vite, TanStack Router и TanStack Query — это не компромисс, а осознанный выбор.

    • Сначала определите категорию приложения: сайт с публичным контентом или инструмент с аутентификацией.
    • Рассматривайте URL и данные сервера как два основных хранилища состояния, предоставив для каждого отдельный инструмент управления.
    • Сохраняйте границы безопасности и API там, где они уже установлены в вашей организации.
    • Пересмотрите принятое решение, если публичные страницы, SEO или серверная отрисовка станут реальными требованиями.