30 августа 2021 года на GitHub прошел первый релиз исходного кода фреймворка Tramvai. При этом свою историю фреймворк начал гораздо раньше и долгое время был внутренней разработкой компании.

Tramvai предназначен для создания универсальных (SSR) React-приложений наряду с Next.js, Remix и SvelteKit. Фреймворк служит основой для десятков приложений и решает проблемы наших разработчиков с помощью более чем 150 библиотек и модулей, разработанных специально для tramvai-приложений.

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

Внедрение зависимостей

Ключевая особенность фреймворка — использование принципа внедрения зависимостей. Dependency Injection yдивительно хорошо подходит для создания SSR-приложений и позволяет делать даже самые большие приложения гибкими и модульными.

DI — неотъемлемая часть многих фреймворков на других языках программирования. Angular и Nest.js — самые яркие примеры таких фреймворков во фронтенд-экосистеме.

Серверный и клиентский код SSR-приложений имеет много общих абстракций, которые работают по-разному на обеих платформах. Пример такой абстракции — cookies. DI позволяет создать общий интерфейс сервиса для работы с cookies и добавить две разные реализации для сервера и браузера. Благодаря общему интерфейсу такой сервис можно будет использовать в любом коде приложения, не задумываясь о текущем окружении. Один из вариантов использования -— любой React-компонент приложения:

Для SSR-приложений важно учитывать, что на сервере одно и то же приложение обрабатывает много запросов. Такие singleton-объекты, как LRU-кэши для HTTP-клиентов, сам веб-сервер fastify и его плагины, создаются только один раз и сохраняются в главном DI-контейнере приложения.

В обработчике запроса от пользователя на каждый запрос от Root DI создается вложенный DI-контейнер, и будет содержать все уникальные для пользователя сущности:

  • объекты request и response;

  • снапшот данных, которые надо передать с сервера на клиент для корректной гидрации;

  • экземпляры таких сервисов, как CookieService.

Визуальное представление этой иерархии DI-контейнеров
Визуальное представление этой иерархии DI-контейнеров

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

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

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

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

Действия на каждом этапе выполняются параллельно, так мы легко можем сгруппировать на одном этапе множество запросов к API и выполнять линию на каждый запрос максимально быстро.

Упрощенная визуализация линии CommandLineRunner для пользовательского запроса на сервере
Упрощенная визуализация линии CommandLineRunner для пользовательского запроса на сервере

Для клиентских SPA-переходов стоит рассматривать линию CommandLineRunner в связке с визуализацией флоу роутинга: документация для CommandLineRunner и документация для модуля роутинга.

Модули другая важная концепция Tramvai. Любое tramvai-приложение собирается из набора модулей, каждый из которых отвечает за конкретные функции. Вот список базовых модулей, названия которых говорят сами за себя: ServerModule, RenderModule, RouterModule, LogModule, CookieModule.

Модули работают как связующий слой между различными функциями и DI. На примере CookieService из DI модуль CookieModule будет экспортировать уникальный токен с интерфейсом сервиса и два модуля: для серверного и клиентского кода. Каждый из модулей регистрирует соответствующую реализацию токена в DI, которая будет доступна в приложении. При этом для реализации могут использоваться любые библиотеки для работы с cookies.

Схема зависимостей сервиса для работы с cookies в разных окружениях
Схема зависимостей сервиса для работы с cookies в разных окружениях

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

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

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

Особенность экшенов — в ограничении времени выполнения на стороне сервера. Эти лимиты позволяют отдавать HTML пользователю как можно раньше. При этом каждый экшен, которые не успел выполниться на сервере, будет запущен заново на клиенте. Например, если для страницы зарегистрировано три экшена и запрос к API в одном из них завис и вышел за лимиты, на клиенте будет перезапущен только этот единственный экшен.

Схема работы Actions на сервере и клиенте
Схема работы Actions на сервере и клиенте

Возможности Tramvai

Создание React-приложений. Они предоставляют собой гибкий механизм для построения макета страниц, хуки для работы с DI, экшенами, роутингом и глобальным стейтом и поддерживают hot reloading компонентов без потери локального состояния с помощью Fast Refresh.

Full-stack-фреймворк, который предоставляет API-роуты. Эти роуты позволяют создать полноценный бэкенд, или Backend for Frontend (BFF), для группировки или трансформации запросов к основному API.

Тесно интегрированное с приложением решение для микрофронтендов с поддержкой SSR, DI и Module Federation — Child Apps. Child Apps позволяют строить страницы приложения из независимых блоков с собственным релизным циклом и отдельной командой разработчиков.

Благодаря поддержке DI и переиспользованию CommandLineRunner разработка Child Apps практически не отличается от разработки tramvai-приложений.

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

Управление состоянием. Также используется собственное решение с API, похожим на Redux в связке с redux-act, но с поддержкой независимых атомарных сторов, что значительно повышает производительность подписок на сторы через useSelector и useStore.

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

Для unit и интеграционного тестирования разработаны библиотеки с максимально близкими к компонентам приложения абстракциями, использующие Jest, Testing-library и Playwright.

Для обеспечения качественной и бесперебойной работы tramvai -приложений мы предоставляем модули со следующими возможностями:

  • обработчиками для неперехваченных исключений;

  • метриками состояния сервера для Prometheus;

  • отправкой ошибок в Sentry на клиенте и сервере;

  • Health checks и graceful shutdown для успешных деплоев;

  • автоматическим логированием ошибок запросов;

  • Request Limiter;

  • кэшированием DNS.

Документация, инструментарий, примеры

Для сборки приложений создана консольная утилита @tramvai/cli. Помимо development- и production-сборки CLI отвечает за генерацию новых приложений или компонентов приложения, несколько режимов анализа сборки, обновление и установку tramvai -зависимостей.

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

Документация проекта расположена на сайте tramvai.dev. Мы следим за качеством и актуальностью документации, улучшать TT нам помогает большое сообщество разработчиков tramvai-приложений внутри Т-Банка.

Для знакомства с фреймворком рекомендуем пройти туториал по созданию приложения Pokedex.

Узнать больше про внутреннее устройство и основные концепции можно в разделе Concepts.

Самый простой способ запустить tramvai-приложение — это базовый шаблон на Codesandbox.

Более комплексный пример — todo-приложение, разработанное по методологии feature-sliced.

Посмотреть на результат работы tramvai-приложений можно на https://www.tbank.ru/.

Заключение

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

Наши цели — это максимальный комфорт в разработке и эффективно и быстро работающие веб-приложения.

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

Welcome!

Комментарии (7)


  1. sumdy-c
    21.06.2024 12:09
    +1

    Максимальный комфорт веб-приложений, когда рендером страниц занимается клиент, а не сервер. Нет, вы не подумайте, я сам люблю в Remix потыкать хобби ради, но если что-то продуктовое, то пожалуй оставлю связку React ( любой возьмите ) + Nest.js ( любой возьмите ). Под задачи быстродействия всего вот этого очень много лет строилось обвязка, производителями браузеров, поставщиками веб-серверов, фреймворков.
    А так спасибо за SSR фреймворк, все будут юзать Next.js, как и юзали до этого.


    1. DarthVictor
      21.06.2024 12:09

      Ну, справедливости ради, Next.js на полноценный Fullstack фреймворк не тянет. Это именно что SSR фреймворк, для бэка в нём вообще ничего нет. Да и для фронта некоторые решения в нём сомнительны.


    1. SuperOleg39ru Автор
      21.06.2024 12:09

      Под разные задачи свои инструменты)


  1. GeorgeDydyrko
    21.06.2024 12:09
    +1

    «Посмотреть на результат работы tramvai-приложений можно на https://www.tbank.ru/.»: формирование страницы сервером — более трети секунды. В сравнении с временем формирования страниц у приличных сайтов, где оно исчисляется единицами-десятками миллисекунд — неприлично долго.
    То есть быстродействие этого примера системы — ну уж очень низкое.


    1. SuperOleg39ru Автор
      21.06.2024 12:09
      +1

      Мне сложно сказать, так как не знаю что и с чем сравниваете, но предполагаю два варианта:
      - приличный сайт это статичный блог
      - или это клиентский рендеринг

      Сравнивать отдачу статичного HTML из CDN с динамической страницей не очень корректно.

      Если говорим про CSR - более валидной метрикой будет LCP, сложно назвать полноценным ответом быстрый показ двухсекундного лоадера для пользователя.


  1. xztv
    21.06.2024 12:09

    Прикольно видеть синтаксис модулей Angular и jsx в одном фреймворке, крутая работа! :)

    Вопрос про экшены: если идет запрос на какой-то "секретный" API, который может отрабатывать дольше лимита и который не хотим светить клиенту, как в таком случае происходит перезапрос?


    1. SuperOleg39ru Автор
      21.06.2024 12:09
      +1

      Спасибо за фидбек!

      Для такого вида запросов механизм перезапроса не будет применяться, защита тут возможна на нескольких уровнях:
      - указать экшену что он должен запускаться onlyServer
      - в env переменной API указать что на клиент ее передавать не надо
      - вообще код экшена в клиентский бандл не включать

      Простой альтернативы Server Actions некста например у нас нет, можно создать API роут и его как BFF юзать но это надо делать вручную.