Микрофронтенды: разделяй и властвуй
Всем привет! Меня зовут Аня, я фронтенд-разработчик в Delivery Club. Хочу рассказать про использование микрофронтендов. Поговорим о том, что же это за зверь такой — микрофронтенд, почему мы решили использовать этот подход в своих проектах и с какими проблемами столкнулись при внедрении.
Для чего они нам понадобились
Delivery Club — это не только пользовательское приложение для заказы еды. Наши команды работают над сайтом, приложениями для курьеров и партнёров (ресторанов, магазинов, аптек и т.д.), занимаются прогнозированием времени, оптимизацией доставки и другими сложными задачами. Моя команда создаёт админку для работы с партнёрами. В ней можно управлять меню, создавать акции, отвечать на отзывы, общаться со службой поддержки и делать много других полезных для партнёров вещей. Но мы не единственная команда, которая занимается админкой. Отсюда возникало множество проблем при разработке:
- Сложно жить с монолитом. Быстро писать код мешали устаревание подходов, фреймворков и зависимостей, а также связанность логики.
- Совместная разработка. Долго решать конфликты, противоречивость изменений.
- Проблема с тестированием. Когда прилетали баги от других задач и команд, было непонятно, в чём причины.
- Развёртывание было зависимое и блокирующее.
Поэтому нам нужна была возможность:
- использовать любые инструменты и технологии, снижая риск устаревания;
- разрабатывать и деплоить независимо;
- изолировать реализации.
Устройство проекта
Для начала расскажу, как сейчас устроен наш проект.
- Основное старое приложение на AngularJS, к которому мы планируем подключать новые микроприложения.
- Dashboard-приложение на Angular 6, подключенное через iframe (но оно со временем разрослось и от описанных выше проблем не избавило). К нему подключаются приложения, здесь хранятся старые страницы.
- Приложения на VueJS, которые используют самописную библиотеку компонентов на VueJS.
Мы поняли, что ограничения тормозят развитие проекта. Поэтому сформулировали возможные пути:
- Разделение приложения на страницы по маршрутам.
- iframe.
- Микрофронтенды.
Мы сразу отказались от варианта с iframe (почему мы так решили, читайте ниже). У разделения страниц по маршрутам есть один большой недостаток: дублирование логики в каждом приложении (типа навигации) и вообще всего, кроме специфики страницы. Поддерживать такую систему в актуальном состоянии очень сложно. Поэтому мы выбрали микрофронтенды.
Что такое микрофронтенды
По сути, это микросервисы в браузере. В бэкенд-разработке микросервисы используют повсеместно, а в мире фронтенда этот подход встречается редко. Вот что можно почитать и посмотреть о микрофронтендах:
Сейчас наш проект только переходит на микрофронтенды, наша команда стала первопроходцем. И мы столкнулись с рядом проблем.
Проблемы внедрения микрофронтендов
1. Ещё один iframe? Может, уже хватит?
К великому разочарованию, наше dashboard-приложение открывается внутри iframe (исторически так сложилось), и нам очень не хотелось делать один iframe внутри другого. Однако это был запасной план. Если вдруг что-то пойдёт не так, то просто заворачиваем микрофронтенд в iframe — и готово рабочее приложение.
Мы видели несколько недостатков:
- Неудобная навигация. Каждый раз для редиректа на внешнюю ссылку нужно использовать
window.postMessage
. - Сложно верстать в iframe.
К счастью, нам удалось этого всего этого избежать, и микрофрентенд мы подключили как веб-компонент с shadow dom:
. Такое решение выгодно с точки зрения изоляции кода и стилей. Веб-компонент мы сделали с помощью модифицированного vue-web-component-wrapper
. Почитать подробнее о нём можно здесь.Что мы сделали:
- Написали скрипт, который добавляет ссылку на сборку микрофронтенда в разделе
head
страницы при переходе на соответствующий маршрут. - Добавили конфигурацию для микрофронтенда.
- Добавили в
window.customElements
тегreview-ui-app
. - Подключили
review-ui-app
в dashboard-приложение.
Так мы сразу убили несколько зайцев. Во-первых, приложение представляет собой кастомный тег, как мы привыкли — компонент. Во-вторых, кастомный тег принимает данные как свойства, следит за ними и выбрасывает события, которые слушает обёртка. Ну и в-третьих, избавились от iframe внутри iframe:)
2. А где стили?
Ещё одна неприятная проблема нас ждала дальше. Компоненты в микрофронтенде работали, только вот стили не отображались. Мы придумали несколько решений:
- Первое: импортировать все стили в один файл и передать его во
vue-wrapper
(но это слишком топорно и пришлось бы добавлять вручную каждый новый файл со стилями). - Второе: подключить стили с помощью CSS-модулей. Для этого пришлось подкрутить
webcomponents-loader.js
, чтобы он вшивал собранный CSS в shadow dom. Но это лучше, чем вручную добавлять новые CSS-файлы :)
3. Теперь про иконки забыли!
Затем мы подключили внутреннюю библиотеку, но в ней не отображались SVG.
С иконками нам пришлось повозиться подольше. Для их подключения нам пришлось сделать несколько итераций:
- Сначала мы попытались подрубить спрайт так же, как и стили, через
appendChild
. Они подключились, но всё равно не отображались. - Затем мы решили подключить через
sprite.mount(this.shadowRoot)
. Добавили в вебпаке вsvg-sprite-loader
опциюspriteModule: path.resolve(__dirname, './src/renderers/sprite.js’)
. Внутри sprite.js экспортировалиBrowserSprite
, и иконки начали отображаться! Мы, счастливые, подумали, что победили, но не тут-то было. Да, иконки отображались, и мы даже выкатились с этой версией в прод. Но потом нашли один неприятный баг: иконки пропадали, если походить по вкладкам dashboard-приложения. - Наконец, во vue-wrapper мы подключили
DcIconComponent
(библиотечный компонент, позволяющий работать с библиотечными иконками) и в нём подключили иконки из нашего проекта. Получили отображение без багов :)
4. Без авторизации никуда!
Ну и завершающая проблема, без которой наш микрофронтенд не смог бы существовать на проде, — это, конечно же, взаимодействие с API. Главная проблема заключалась в передаче заголовка авторизации и запроса на обновление этого заголовка, если он протух. Сделали мы это таким способом:
- Токен с авторизацией передаём с помощью свойств веб-компонента.
- С помощью
AuthRequestInterceptor
подключаем токен-запросы для API. - Используем токен, пока он не протухнет. После протухания ловим ошибку 401 и кидаем в dashboard-приложение событие «обнови токен, пожалуйста» (ошибка обрабатывается в
AuthResponseInterceptor
). - Dashboard-приложение обновляет токен. Следим за его изменением внутри main-сервиса, и когда токен обновился, заворачиваем его в промис и подписываемся на обновления токена в
AuthResponseInterceptor
. - Дождавшись обновления ещё раз повторяем упавший запрос, но уже с новым токеном.
5. Нас волнуют зависимости
Проблема выглядела так: с каждым подключённым микрофронтом тянется бандл. И сколько микрофронтов подключено, столько и бандлов подтянуто. Чтобы избежать этого, мы применили общие зависимости. Вот как мы это реализовали:
- В микрофронтенд-приложении указываем в webpack.config.prod.js в разделе
externals
те зависимости, которые хотим вынести:module.exports = { externals: { vue: ‘Vue’ },
Здесь мы указываем, что под именемVue
вwindow
можно будет найти зависимостьvue
. - В рамках оболочки (в нашем случаем в dashboard-приложении) выполняем
npm install vue
(и другиеnpm install
-зависимости). - В dashboard-приложении импортируем все зависимости:
import Vue from ‘vue’ (window as any).Vue = Vue;
- Получаем удовольствие.
6. Разные окружения
Мы имеем три окружения: локальное, тестовое и прод. Сначала у нас была проблема при переходе с тестового окружения на прод: для микрофронтенда приходилось явно прописывать в проекте ссылку.
Решили мы это следующим образом:
- Добавили в микрофронтенд файл, в котором определяем конфигурацию для приложения в runtime браузера. Также добавили в Docker системный пакет, который предоставляет команду
envsubst
. Она подставляет значения в env.js, который тянет микрофронтенд-приложение, и эти переменные пишутся вwindow['${APP_NAME}_envConfig']
. - Добавили переменные окружения отдельно для прода и отдельно для тестового окружения.
Так мы решили несколько проблем:
- настроили локальные моки (раньше приходилось вручную включать их локально);
- настроили тестовое окружение без дополнительных ручных переключений на прод и обратно.
Выводы
Реализация микрофронтендов, их подключение и решение проблем при работе с ними тянулись довольно долго, но это того стоило. Микрофронтенды помогли нам отделить релизы команд друг от друга, разделить кодовую базу и избавиться от долгих решений конфликтов при
git merge
.Думаю, со временем всё больше проектов будут отказываться писать монолит и переходить на более универсальные подходы. Одним из таких подходов могут стать и микрофронтенды. Надеюсь, эта статья поможет кому-то в реализации :).