Как стать автором
Обновить
81.02

Эволюция веб-приложения PREMIER: от legacy к современной архитектуре

Уровень сложностиСредний
Время на прочтение12 мин
Количество просмотров1.2K

Может быть, не всё legacy, чему больше года, но у нас и правда был запущенный случай: несколько лет в режиме стартапа над проектом работали разные команды, начиная от аутсорса, заканчивая маленькими инхаус-группами. Мы жили в парадигме «работает — не трогай», но всему есть предел, и в конце-концов техдолг стал слишком сильно блокировать развитие. 

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

Историю эволюции онлайн-кинотеатра PREMIER, перехода от startup-like архитектуры к enterprise-решению, в котором только над фронтендом трудится более 25 специалистов, для вас рассказывают: Дмитрий Борцов, руководитель управления разработки клиентских интерфейсов, и Автушенко Андрей, техлид команды веб-разработки. 

Причины масштабного рефакторинга 

Видеоплатформа PREMIER была запущена в 2018 г. В 2022-м кодовой базе было 5 лет, но не только по этой причине требовался рефакторинг. За время существования проекта сменилось несколько команд разработки, каждая приносила свои инструменты, подходы и стиль. В результате накопился стандартный набор проблем, характерный для проекта, над которым работают в режиме решения исключительно бизнес-задач без возможности выделять ресурсы на рефакторинг и улучшения. Что это были за проблемы? 

1. Отсутствие полноценной дизайн-системы

У дизайнеров не было единого подхода к работе и старые макеты обновлялись под новые фичи только при большой необходимости. Из-за отсутствия атомарности в проекте за несколько лет накопилось большое количество невалидных компонентов. 

Так выглядела папка компонентов веб-приложения PREMIER в 2022-м
Так выглядела папка компонентов веб-приложения PREMIER в 2022-м

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

2. Дублирование логики и спагетти-код

В legacy-проекте «очень удобно» исправлять баги. Например: ловишь баг с модалкой; думаешь, обычное дело, за пару стори-пойнтов разберусь; ищешь, где вызывается функция — находишь в одном из 70+ миксинов, который используется в 100+ файлах; пытаешься предсказать, какой из них взорвётся первым после правки. 

Сложности с масштабированием в свою очередь порождают переусложнённую логику, дублирование компонентов и спагетти-код. 

3. Нет TypeScript и линтинга

О TypeScript мечтала вся команда, но его внедрение требовало ресурсов, о которых ещё только предстояло договориться с бизнесом.  

Дела с линтингом тоже обстояли так себе: Prettier для общего форматирования и на этом всё. Никакой вам типизации, никакой проверки кода — только чистая вера в разработчика!

4. Рост time-to-market и техдолга

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

С точки зрения бизнеса эти проблемы блокировали развитие. Накопившееся к 2022-му legacy мешало решению бизнес-задач: 

  • запуску продуктовых фичей;

  • стабильному показателю time-to-market;

  • найму в команду. 

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

План архитектурных изменений

Все решения по изменениям мы принимали исходя из технических и бизнес-требований, чтобы нововведения не завели нас в новый тупик, а обеспечили искомое развитие. 

Upstream технологического стека: мигрируем с Vue 2 на Vue 3 ради производительности и возможностей новой версии фреймворка; переходим на Nuxt 3, чтобы получить SSR из коробки и отказаться от собственного нестабильного и мешающего SEO-оптимизации решения для генерации html.

Масштабируемость: реализуем гибкую архитектуру, адаптируемую для любых бизнесовых и технических потребностей, с помощью паттерна Composition Root.

Стабильность: внедряем инструменты разработки, призванные автоматически повысить качество кода, такие как TypeScript, Stylelint, Prettier, Eslint + Tslint, Commitlint; договариваемся с дизайнерами внедрить атомарную дизайн-систему, чтобы не плодить лишние компоненты и логику. 

Time-to-market и другие ключевые метрики будем улучшать за счёт масштабируемой архитектуры, которая позволит разработчикам легко переиспользовать код и быстро собирать новые фичи. 

Framework agnostic подход в core-решениях поможет нам: в случае чего справиться с внешними факторами, заменить Vue на любой альтернативный фреймворк, проще нанимать и онбордить новых сотрудников. Для реализации ядра, не зависящего от фреймворка, понадобится два глобальных хранилища: одно для бизнесовых данных, другое — для визуальных, влияющих на UX пользователя.

Реализация изменений

Подробно разберём, как мы воплощали в реальность каждый пункт нашего плана и переходили к новой архитектуре и с какими трудностями столкнулись по дороге.

Nuxt 3 на базе Vue 3

Когда мы начинали переезд, Nuxt 3 ещё находился в бете — это был риск, но нас подкупал его потенциал и обещанные фичи. 

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

Несмотря на все приключения, нам удалось настроить всё так, что Nuxt 3 даже в такой «молодой» версии стал мощной основой для нашего продукта с его крутыми фичами. А заодно прокачали свою адаптивность.  

Примерная структура нашего проекта на Nuxt
Примерная структура нашего проекта на Nuxt

DI и Composition Root

Добиться масштабируемости мы решили с помощью DIP с IoC-контейнером и реализацией Composition Root — чтобы фичи полетели и при этом не ломали соседние части системы. Но давайте обо всём по порядку. 

Dependency Inversion Principle (DIP) — принцип, согласно которому классы должны зависеть от абстракций, а не от конкретных реализаций. Такой подход помогает сделать более устойчивую к изменениям систему и позволяет легко заменять модули, не нарушая высокоуровневую логику. При соблюдении DIP классы напрямую зависят от интерфейса, а реализация передаётся через Dependency Injection. 

Dependency Injection (DI) — механизм передачи зависимостей в классы, может быть нескольких видов:

  • constructor injection — для обязательных зависимостей;

  • property injection — для необязательных зависимостей;

  • method injection — для зависимостей, требуемых только в конкретных методах.

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

Inversion of Control (IoC) — это подход, при котором управление зависимостями передается внешнему контейнеру или фреймворку.

Для управления зависимостями мы выбрали Inversify — библиотеку, которая предоставляет контейнер, который в свою очередь управляет созданием и жизненным циклом объектов. Кроме того, что централизация зависимостей упрощает тестирование и замену модулей, она позволяет реализовать Composition Root

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

Рассмотрим пример реализации этих принципов в коде. Ниже — входная точка в виде плагина Nuxt 3, в которой инициализируется контейнер и регистрируются ключевые модули: транспортный слой со всеми его бэкендами, адаптерами и модулями; сторы; модули вспомогательных функций, логгеры и прочее.

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

Далее нужно описать уникальные имена каждого модуля для правильной работы Inversify: 

Ниже пример применения DI и декораторов из Inversify: App — корень контейнера, в который кладем транспортный слой, фильтры, хранилища. 

Пример одного из бэкендов, в котором регистрируются все его модули, содержащие необходимые методы API: 

Inversify предоставляет два декоратора: 

  • injectable говорит о том, что класс будет доступен в контейнере для дальнейшего внедрения;

  • inject — что вы внедряете конкретный модуль из контейнера (в примере выше — через конструктор).

Разберём подробнее отдельный модуль одного из бэкендов: 

Что здесь происходит: 

  • принимаем в модуль адаптер для запросов, модуль для работы с локальным хранилищем и модуль стора;

  • реализуем метод отвязки устройства logout и другие методы, которые относятся непосредственно к этому модулю.

Пример использования сущности из контейнера: 

Есть $app, в котором есть транспортный слой, в котором есть несколько бэкендов, в котором есть его модули, в котором есть конкретные методы — этакая матрёшка. В данном примере мы получаем подписки и обновляем их в глобальном хранилище.

Feature Sliced Design

Мы не стали ограничиваться базовой структурой папок, которую обеспечивает Nuxt, и серьёзно занялись структурой компонентов — иначе с большой кодовой базой есть риск снова быстро свалиться в мешанину, какая у нас была в прошлой версии.

Методология Feature Sliced Design (FSD) помогает структурировать код и держать его масштабируемым и модульным. Подробно с FSD можно познакомиться здесь, а в этой статье пройдемся только по основным принципам разделения кода и связке с Nuxt.

В FSD используются три уровня абстракции, первый из них — слои:

  • App — инфраструктурный слой, который содержит глобальные настройки приложения и роутинг.

  • Pages — здесь находятся страницы приложения, которые собираются из виджетов и фичей. 

  • Widgets — большие самодостаточные части функциональности, обычно реализующие целые пользовательские сценарии.

  • Features — основные блоки, которые реализуют конкретные бизнес-функции. Фичи могут включать в себя как UI-компоненты, так и логику работы с данными.

  • Entities — основные объекты, которые используются в разных частях системы: модели данных, их обработка и другие бизнес-логические единицы.

  • Shared — слой, где находятся общие компоненты и утилиты, которые могут быть использованы в любом месте приложения.

Второй уровень абстракции FSF — слайсы. Слайсы — это разделение функциональности продукта на более мелкие, самостоятельные части. Это позволяет разработчикам работать над отдельными частями продукта параллельно, повышая скорость и гибкость разработки. Конкретное деление слоёв на слайсы зависит от проекта. Слои App и Shared не имеют слайсов, а сразу состоят из сегментов. 

Третий уровень FSD — сегменты — группируют код по его назначению. Обычно они состоят из следующих сущностей:

  • ui — всё, что связано с отображением: UI-компоненты, стили и т.д.

  • api — взаимодействие с бэкендом: функции запросов, типы данных, мапперы.

  • model — всё, что относится к модели данных: схемы валидации, интерфейсы, хранилища, бизнес-логика.

Так изменилась структура проекта после внедрения FSD. 

Структура веб-проекта PREMIER после внедрения FSD
Структура веб-проекта PREMIER после внедрения FSD

NB: важно не забыть переопределить директории Nuxt в конфиге, чтобы подружить фреймворк и методологию FSD. В фреймворках Nuxt и Next папка pages отвечает за роутинг, а в FSD — за сегмент интерфейса. Поэтому нужно разделить pages и роутинг. 

Вот как это сделали мы: ниже слева на картинке в папке routes находятся страницы Nuxt, которые просто рендерят компонент и добавляют необходимую мета-информацию, а справа — сам сегмент интерфейса, который собирается из различных виджетов и фичей.

Слева в routes страницы Nuxt, справа — сегмент интерфейса
Слева в routes страницы Nuxt, справа — сегмент интерфейса

Framework Agnostic

Несмотря на то, что в этой статье мы рассматриваем примеры из Nuxt, в новой архитектуре мы хотели придерживаться подхода, в котором реализация ядра проекта не привязана к особенностям фреймворка. Принцип framework agnostic призван дать страховку от внешних рисков и упростить онбординг в проект. Но требует иметь два глобальных хранилища, чтобы максимально изолировать глобальные бизнесовые данные и данные, которые нужны для отображения UI, и в будущем переиспользовать ядро как независимую сущность, нанизывая свой визуал со своим фреймворком или UIKit. 

В качестве хранилищ мы выбрали: 

  • MobX для бизнес-данных. Mobx легко интегрируется с React, Vue и другими фреймворками, то есть его можно использовать в любом проекте с минимальными изменениями. 

  • Pinia — для визуальных данных, так как это стандарт во Vue/Nuxt-проектах. 

На иллюстрации слева пример бизнес-данных, связанных с профилем пользователя, в MobX. Также к бизнес-данным можно отнести, например: продукты, подписки, авторизацию и прочее. Справа — пример Pinia и данных отображения: слайдер с флагами, от которых будет зависеть поведение интерфейса. 

Линтинг

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

Чтобы разработчики были сконцентрированы на скорости и не отвлекались на синтаксические холивары, внедрили линтеры:

  • Prettier — для автоматического форматирования кода;

  • ESLint — для проверки JavaScript, чтобы унифицировать подходы к написанию JS-кода и предотвратить потенциальные проблемы на этапе разработки. 

  • Stylelint обеспечивает консистентность стилей, соблюдение командой вёрстки по БЭМ-методологии, валидирует нейминг классов согласно дизайн-системе и подсвечивает неэффективные практики в CSS.

Запускаем линтеры и валидируем код с помощью Husky — инструмента, который позволяет интегрироваться в цепочку git-коммитов. Мы настроили хуки, которые проверяют код на соответствие правилам перед каждым коммитом: 

Если код не соответствует принятым стандартам, husky просто не даёт закоммитить правки.

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

С помощью утилиты Commitizen мы наводим порядок в релизах, побуждая команды писать осмысленные коммиты по commit convention. 

Красивые и понятные сообщения в истории git это: 

  • удобство командной работы и онбординга в проект;

  • инструмент для анализа изменений; 

  • возможность автоматически генерировать changelog. 

TypeScript

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

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

Пример рантайм-валидации: в методе logout, который уже встречался выше, добавляем передачу данных в функцию validateRuntypes

Ниже в описании рантайм-схемы всё просто: импортируем типы из самой библиотеки 'runtypes', из получившегося рантайм-типа получаем обычный TypeScript-интерфейс, который в дальнейшем используем в методах.

Функция для валидации данных принимает схему и сравнивает с данными с бэкенда:

Если данные не совпадут с ожидаемыми, то ещё на этапе разработки будет warning.

Все эти инструменты в совокупности создают мощную экосистему автоматизации, которая гарантирует, что кодовая база соответствует принятым стандартам и содержится в порядке. 

Теперь нужно убедиться, что код соответствует ожиданиям и бизнес-требованиям.

Тестирование

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

  1. TypeScript и статические анализаторы — фундамент.

  2. Unit-тесты —  тестирование отдельных функций или модулей. Мы используем Vitest (основанный на Vite) и по нашему мнению это лучший фреймворк для тестирования фронтенда. 

  3. Скриншотные тесты — подскажут, не «поплыл» ли интерфейс, если в коде компонента что-то поменялось. Отслеживаем визуальные изменения с помощью  Storybook и Loki

  4. Интеграционные тесты — проверяем взаимодействие нескольких модулей в связке.

  5. E2E-тесты — чтобы проверить весь процесс от начала до конца в реальных условиях. С данной задачей отлично справляется Playwright (к сожалению, в Vitest браузерное тестирование ещё сыровато).

Все эти типы тестирования складываются у нас в пирамиду тестирования: чем проще обеспечить тестирование, тем его больше, а ресурсозатратные способы используем выборочно.

Пирамида тестирования
Пирамида тестирования

Принцип пирамиды тестирования: 

  • Не пишем тесты на то, что проверяется типизацией.

  • Много unit-тестов и скриншотных — они быстро пишутся и проверяют целый модуль.

  • Интеграционных тестов меньше.

  • E2E-тестами покрываем ключевые бизнес-процессы, например, авторизацию или оплату подписки. Они хоть и мощные, но медленные, так как требуют поднятия реальной среды, работы браузера и реальных запросов, да ещё и не всегда могут сразу показать, где именно произошла ошибка.

Простые тесты, как и линтеры, запускаются в CI на ранних этапа пайплайна для дополнительной проверки кода. 

Трекаем актуальный code coverage на этапе создания MR в GitLab.

Более того, собираем информацию о текущем coverage в XML c помощью cobertura и передаём в GitLab Artifacts. Дальше GitLab может распарсить этот артефакт, подсветить строчки в MR и наглядно показать, где требуются тесты. 

Результаты и планы

Как изменились проектные метрики после внедрения новой архитектуры:

  • Time-to-market сократился примерно в 2 раза благодаря хорошо организованной архитектуре и четкому разделению всего кода на зоны ответственности. 

  • Как следствие, при том же составе команды сильно вырос объем поставки ценности. 

  • После обновления стека технологий поток кандидатов в найме стал заметно больше. И самое классное — теперь на собеседованиях можно обсуждать архитектуру, методологии и принципы вроде SOLID прямо на уровне нашего проекта. 

  • Новые разработчики быстрее вникают в проект. Теперь для первичного онбординга достаточно чтения вводной документации по архитектуре и style-гайдам, где собраны ссылки на методологии и основные технологии. 

Показатель

Было

Стало

Среднее время реализации фичи 

13,49 дней

6,58 дней

Capacity команды

~80 stp

~170 stp

Онбординг

1 месяц

5 дней

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

  • переход на Nuxt 4;

  • внедрение кодогенерации — чтобы автоматически генерировать интерфейсы, а возможно и весь транспортный слой;

  • Покрытие тестами → 100%.

Если статья была для вас полезна и вам интересна тема карьеры в IT и frontend-разработке, подписывайтесь на telegram-каналы авторов: Дмитрия Борцова, руководителя клиентской разработки PREMIER, и Андрея Автушенко, техлида команды веб-разработки PREMIER.

Также подписывайтесь на Смотри за IT, если хотите знать больше о создании медиасервисов: там инженеры Цифровых активов «Газпром-Медиа Холдинга» таких, как PREMIER, RUTUBE и Yappy делятся своим опытом и тонкостями разработки видеоплатформ.

Теги:
Хабы:
+9
Комментарии3

Публикации

Информация

Сайт
rutube.ru
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия