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

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

Меня зовут Влад Коротун, я ведущий фронтенд-разработчик в одной из продуктовых команд hh.ru. В этой статье расскажу о нашем пути от большого монолита до так называемых "микрофронтендов".

Главные проблемы

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

  • Множество команд одновременно пишут в разные части проекта. Сложно определить ответственного за тот или иной код.

  • В монолите сложно проводить рефакторинги и внедрять новые технологии.

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

  • Происходит толкучка на релизе, зачастую возникают конфликты.

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

  • В монолите обычно скапливается много legacy-кода который никто не хочет трогать, но его приходится поддерживать.

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

Проба пера

Не так давно перед нами встала задача — добавить веб-интерфейс чатика между соискателями и работодателями.

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

Поскольку у нас уже был опыт встраивания сторонних ресурсов с помощью iframe, мы решили применить уже знакомый подход — разместили чатик на отдельном домене и встроили его в основной сайт с помощью iframe. Это также позволило открывать чатик на отдельной вкладке без обвязки основного сайта — удобно для пользователей, которые много времени проводят в переписках. Коммуникация с основным сайтом осуществлялась посредством postMessage-сообщений.

Ввиду того, что чат получил свою независимую кодовую базу — он стал нашим первым сервисом на 17 Реакте.

Прокси-страницы

Работа с iframe не всегда удобна. В случае с чатом, который является скорее веб-приложением, чем сверстанной страницей, iframe проблем не вызывал. Чат открывается в оверлее и растягивается на всё выделенное ему место.

Но когда дело касается встраивания страницы, возникают проблемы с управлением высотой iframe. Содержимое хоть и может менять высоту, но это никак не повлияет на высоту самого iframe. Конечно, проблему можно решить с помощью JS — проверять высоту содержимого по таймеру и подстраивать высоту контейнера, но это весьма затратно с точки зрения производительности.

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

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

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

Прокси-компоненты

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

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

Чтобы избежать конфликтов, мы решили изолировать DOM прокси-компонента с помощью shadowRoot. Весь код встраиваемого компонента, включая верстку, подготовленную SSR, стили и скрипты мы помещаем в изолированный контейнер shadowRoot. Как бонус – shadowRoot наследует стили своего контейнера, например, цвет текста.

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

А что с SSR?

Для ускорения отображения страницы у пользователя и для улучшения индексации сайта мы применяем Server-Side-Rendering. Первая версия интеграции прокси-компонентов выполняла загрузку кода прямо в браузере пользователя. Соответственно, первый приходящий с сервера рендер вообще не включал в себя прокси-компоненты.

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

И вот что мы сделали:

Инициализацию прокси-компонентов мы перенесли из браузера в наш BackendForFrontend на Python. При необходимости отрендерить прокси-компонент хендлер ходил в сервис и клал результат в специальную ноду. При сборке страницы результат, полученный из сервиса, встраивался в нужное место шаблонизатором. Далее на фронте полученные данные перемещались в shadowRoot и происходил hydrate — процесс оживления верстки реактом, который сопоставляет полученную с сервера верстку с результатом первого рендера.

Недостатки

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

Почему так происходит?

Стили прокси-компонента могут конфликтовать с основным документом, поэтому при первом рендере мы не кладем их в DOM. Стили появляются только тогда, когда весь код перемещен в shadowRoot.

Что будем с этим делать

Планируем перевести нашу библиотеку компонентов на css-модули. Поскольку её код поставляется “несбилженным” и собирается самим сервисом, имена классов получат уникальные префиксы, а в сборку попадут только те стили, которые в нем используются.

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

Итоги

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

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

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

Однако преимуществ для нас больше:

  • Сервис собирается гораздо быстрее, чем монолит — 1-2 секунды против 15. Быстрее видишь результат — быстрее принимаешь решения.

  • Нет толкучки на релизе, когда из-за конфликта в задачах весь поезд встаёт до выяснения ситуации.

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

На этом у меня всё.

Задавайте вопросы в комментариях, рассказывайте, как вы распиливаете ваш монолит.

Приятных майских! 

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


  1. i360u
    05.05.2022 11:35

    Немного не понял, какой смысл в SSR, если потом результат нужно тащить в Shadow DOM через тот-же JS?


    1. brianconnoly Автор
      05.05.2022 12:16

      В shadowDom тащим для того чтобы запустить css/js, голая верстка для индексации попадает в DOM

      Но вы верно подметили, поэтому мы и решили в итоге от shadowDom отказаться сделав верстку прокси-компонентов безопасной с помощью префиксирования

      Однако использование shadowRoot было важным этапом в становлении нашего решения — мы смогли начать распиливать монолит несмотря на то что код сервисов конфликтовал с основным приложением

      Чуть позже, когда схему без shadowRoot обкатаем, сделаю небольшой апдейт