Современный фронтенд – больше, чем просто формы и стили. Это сложные модели, композитные компоненты, графики, интерактивные редакторы, системы локализации на несколько языков и многое другое. Для развития и поддержки такого левиафана требуется много разработчиков — чтобы писали ещё больше кода. Крупнеет команда, растёт кодовая база – работать с монолитом становится всё сложнее и сложнее. Казалось бы, выхода нет — сиди и страдай. Но мы смогли выпутаться из этой непростой ситуации. Меня зовут Влад Коротун, я ведущий фронтенд-разработчик в одной из продуктовых команд hh.ru. В этой статье расскажу о нашем пути от большого монолита до так называемых "микрофронтендов". Для начала давайте разберем основные проблемы, с которыми мы сталкивались при работе с монолитом: Множество команд одновременно пишут в разные части проекта. Сложно определить ответственного за тот или иной код. В монолите сложно проводить рефакторинги и внедрять новые технологии. Сборка монолита с каждым годом занимает всё больше времени. При отладке очень неприятно ждать по полминуты, пока проект сбилдится, и ты сможешь увидеть результат своих правок. Происходит толкучка на релизе, зачастую возникают конфликты. Релизные автотесты могут занимать до двух часов, что тоже вносит неприятные коррективы в ход работы. В монолите обычно скапливается много legacy-кода который никто не хочет трогать, но его приходится поддерживать. Наш фронтенд имеет более 500 страниц, миллионы строк кода, так что мы обо всём этом знаем не понаслышке. И мы нашли выход из сложившейся ситуации — выносить части проекта в отдельные микросервисы. Не так давно перед нами встала задача — добавить веб-интерфейс чатика между соискателями и работодателями. Он по своей природе отличается от других страниц в hh.ru, потому что заполняет собой всё доступное пространство, имеет боковую и верхнюю панели и выглядит как самостоятельное веб-приложение. Вполне логично выделить этот интерфейс в отдельный сервис, чтобы не упираться в устоявшиеся ограничения основной кодовой базы. Поскольку у нас уже был опыт встраивания сторонних ресурсов с помощью iframe, мы решили применить уже знакомый подход — разместили чатик на отдельном домене и встроили его в основной сайт с помощью iframe. Это также позволило открывать чатик на отдельной вкладке без обвязки основного сайта — удобно для пользователей, которые много времени проводят в переписках. Коммуникация с основным сайтом осуществлялась посредством postMessage-сообщений. Ввиду того, что чат получил свою независимую кодовую базу — он стал нашим первым сервисом на 17 Реакте. Работа с iframe не всегда удобна. В случае с чатом, который является скорее веб-приложением, чем сверстанной страницей, iframe проблем не вызывал. Чат открывается в оверлее и растягивается на всё выделенное ему место. Но когда дело касается встраивания страницы, возникают проблемы с управлением высотой iframe. Содержимое хоть и может менять высоту, но это никак не повлияет на высоту самого iframe. Конечно, проблему можно решить с помощью JS — проверять высоту содержимого по таймеру и подстраивать высоту контейнера, но это весьма затратно с точки зрения производительности. Поиск подходящего решения привел нас к системе встраивания целой страницы в обвязку основного сайта, когда все скрипты, стили и сама верстка грузятся в основной документ. Встраиваемые сервисы используют css-модули для собственных классов, поэтому пересечений стилей между обвязкой и содержимым встраиваемой страницы не возникает. Но и здесь есть проблема — стили нашей переиспользуемой библиотеки компонентов. Обычно сервисы релизятся независимо друг от друга, поэтому в них могут использоваться разные версии нашей библиотеки компонентов. И поскольку её стили помимо реакта используются и в legacy-xsl коде, их имена не хешируются. Из-за расхождений между используемой версии основным сайтом и встраиваемой страницей могут возникать конфликты. С другой стороны, в случае встраивания целой страницы — от основного сайта используются только хидер и футер, которые имеют собственную верстку, а значит конфликты стилей наших переиспользуемых компонентов маловероятны. Иногда хочется унести в отдельный сервис не всю страницу целиком, а какой-то конкретный компонент. Это может быть виджет на главной или другой блок, который встраивается в верстку существующей страницы. Поэтому следующим витком эволюции наших микрофронтендов стали прокси-компоненты. Прокси-компоненты, как и прокси-страницы, используют нашу библиотеку компонентов. Поскольку прокси-компонент встраивается в существующие страницы сайта, где тоже используется эта библиотека — повышается вероятность появления конфликтов в стилях. Чтобы избежать конфликтов, мы решили изолировать DOM прокси-компонента с помощью shadowRoot. Весь код встраиваемого компонента, включая верстку, подготовленную SSR, стили и скрипты мы помещаем в изолированный контейнер shadowRoot. Как бонус – shadowRoot наследует стили своего контейнера, например, цвет текста. Это позволило нам без дополнительных доработок размещать компоненты в блоки, которые могут быть отображены с черным фоном и белым текстом, либо наоборот – с белым фоном и черным текстом. Цвет текста в любом случае будет унаследован от родителя. Для ускорения отображения страницы у пользователя и для улучшения индексации сайта мы применяем Server-Side-Rendering. Первая версия интеграции прокси-компонентов выполняла загрузку кода прямо в браузере пользователя. Соответственно, первый приходящий с сервера рендер вообще не включал в себя прокси-компоненты. Нам хотелось добавить верстку сервисов в результат первого рендера, чтобы содержимое индексировалось и в браузере сразу было видно всё содержимое страницы. И вот что мы сделали: Инициализацию прокси-компонентов мы перенесли из браузера в наш BackendForFrontend на Python. При необходимости отрендерить прокси-компонент хендлер ходил в сервис и клал результат в специальную ноду. При сборке страницы результат, полученный из сервиса, встраивался в нужное место шаблонизатором. Далее на фронте полученные данные перемещались в shadowRoot и происходил hydrate — процесс оживления верстки реактом, который сопоставляет полученную с сервера верстку с результатом первого рендера. Единственный недостаток такой работы с shadowRoot заключается в том, что первые пару секунд пользователи видят фрагменты страницы без стилей. Почему так происходит? Стили прокси-компонента могут конфликтовать с основным документом, поэтому при первом рендере мы не кладем их в DOM. Стили появляются только тогда, когда весь код перемещен в shadowRoot. Планируем перевести нашу библиотеку компонентов на css-модули. Поскольку её код поставляется “несбилженным” и собирается самим сервисом, имена классов получат уникальные префиксы, а в сборку попадут только те стили, которые в нем используются. Как только это будет сделано, мы сможем отказаться от shadowRoot и грузить стили и верстку сразу в основной документ. Ведь необходимость в изолировании содержимого полностью пропадет. Мы живем с этой схемой уже некоторое время, поэтому вскрылись небольшие неудобства использования прокси-сервисов. Например, зачастую приходится делать две задачи сразу: разрабатывать новый компонент и встраивать его в основной сайт. А если меняется контракт, приходится создавать обратно-совместимые релизы, так как микрофронтенды и основной сайт релизятся в разное время. Еще становится сложнее управлять зависимостями —  их приходится обновлять отдельно в каждом сервисе. И поскольку зависимости обновляются одновременно, зачастую происходит расхождение в версиях от сервиса к сервису. Это может привести к различиям в стилях наших переиспользуемых компонентов. К примеру, мы перекрасили кнопки в библиотеке компонентов, и пока каждый сервис не обновит эту зависимость, его встраиваемые страницы и компоненты будут выводить кнопки, отличающиеся от аналогичных на основном сайте. Также при возникновении уязвимостей в используемых пакетах приходится поднимать версии во всех проектах, а их количество скоро перевалит за десятку. Однако преимуществ для нас больше: Сервис собирается гораздо быстрее, чем монолит — 1-2 секунды против 15. Быстрее видишь результат — быстрее принимаешь решения. Нет толкучки на релизе, когда из-за конфликта в задачах весь поезд встаёт до выяснения ситуации. Автотесты проходят быстро. На основном проекте это чуть больше часа, а в сервисе прогоняем только те тесты, что затрагивают фрагменты со встроенными прокси-компонентами или страницами. На этом у меня всё. Задавайте вопросы в комментариях, рассказывайте, как вы распиливаете ваш монолит. Приятных майских!