Современный фронтенд – больше, чем просто формы и стили. Это сложные модели, композитные компоненты, графики, интерактивные редакторы, системы локализации на несколько языков и многое другое.
Для развития и поддержки такого левиафана требуется много разработчиков — чтобы писали ещё больше кода. Крупнеет команда, растёт кодовая база – работать с монолитом становится всё сложнее и сложнее. Казалось бы, выхода нет — сиди и страдай. Но мы смогли выпутаться из этой непростой ситуации.
Меня зовут Влад Коротун, я ведущий фронтенд-разработчик в одной из продуктовых команд 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. Быстрее видишь результат — быстрее принимаешь решения.
Нет толкучки на релизе, когда из-за конфликта в задачах весь поезд встаёт до выяснения ситуации.
Автотесты проходят быстро. На основном проекте это чуть больше часа, а в сервисе прогоняем только те тесты, что затрагивают фрагменты со встроенными прокси-компонентами или страницами.
На этом у меня всё.
Задавайте вопросы в комментариях, рассказывайте, как вы распиливаете ваш монолит.
Приятных майских!
i360u
Немного не понял, какой смысл в SSR, если потом результат нужно тащить в Shadow DOM через тот-же JS?
brianconnoly Автор
В shadowDom тащим для того чтобы запустить css/js, голая верстка для индексации попадает в DOM
Но вы верно подметили, поэтому мы и решили в итоге от shadowDom отказаться сделав верстку прокси-компонентов безопасной с помощью префиксирования
Однако использование shadowRoot было важным этапом в становлении нашего решения — мы смогли начать распиливать монолит несмотря на то что код сервисов конфликтовал с основным приложением
Чуть позже, когда схему без shadowRoot обкатаем, сделаю небольшой апдейт