Привет, Хабр! Меня зовут Данил, я Frontend-разработчик в Samokat.tech. Недавно мы с командой распилили монолит на Angular и перешли к микрофронтендам на Vue.
Наш опыт я постарался упаковать в пошаговый гайд – надеюсь, этот материал поможет тем, кто только начинает свой путь в мире микрофронтендов. По ходу статьи мы с вами вместе пройдём от полного незнания до понимания принципов и ответа на заветный вопрос — нужно ли всё это вам. На практике мы сделаем небольшую демонстрацию, которую вы сможете использовать в своём проекте. Поехали!
Джентльменский набор: проблемы легаси-проекта
Наша команда разрабатывает Личный кабинет мерчанта (ЛКМ) — это интерфейс для продавцов, которые приходят торговать на маркетплейс. В ЛКМ огромное количество разделов: склады, ассортимент, заказы, промо и другие. Это довольно большое приложение в стадии активного развития: нужно не только поддерживать кодовую базу, но и регулярно добавлять новые фичи. В плане технологий сначала мы использовали Angular и ориентировались на монолитную структуру, сейчас перешли на Vue и микрофронтенды.
Изначально всем этим занималась одна небольшая команда и всё было хорошо. Но когда бизнес вырос, мы начали задумываться о масштабировании команды и для начала сформулировали текущие проблемы.
1. Много легаси-кода
Большая часть кода уже устарела, в команде зрели настроения отрефакторить приложение и переписать всё на другой фреймворк (с Angular на Vue). Идея — класс, но вы, конечно, понимаете, что просто пойти и переписать все приложения с нуля мы не можем, создание новых фич никто не отменял.
2. Быстрорастущая команда
К нам присоединялись новые люди, формировались новые команды, но мы не всегда могли аккуратно разграничить зоны ответственности и исчерпывающе рассказать про зависимости внутри проекта.
3. Процесс редизайна и ребрендинга
Наступала пора ребрендингов. Изменение дизайн-гайдов, логотипов, изображений, цветов и прочего во всём проекте – ставило перед нами немало задач в разработке.
4. Жажда новых технологий
Не упомянуть такой фактор было бы нечестно: мы как разработчики любим использовать новейшие инструменты и технологии. Однако применять что-то новое в устаревшем проекте очень сложно.
Посмотрев на существующие проблемы, мы сформулировали задачу – «упрощение и разделение кода, переход на новый фреймворк». Мы решили отрефакторить приложение, чтобы упростить работу с кодом, избавиться от легаси и подготовиться к редизайну, запустить процесс переезда на новый фреймворк и явно распределить функциональные блоки между командами, чтобы каждый понимал свою зону ответственности.
Теория
Для начала давайте схематично разберём понятия «упрощение / разделение кода».
Разделим наше приложение на отдельные независимые модули. Мы использовали подход «1 раздел – 1 модуль» и добавляли в список большие визуальные элементы (навигация, шапка, страницы регистрации и остальные).
Идея: вынести эти блоки в отдельные репозитории и распределить по командам (упрощение/разделение), а потом на этапе сборки и деплоя соединить это приложение в одно.
Реализация: пусть у нас будет один пустой HTML-файл, который будем отдавать пользователю. Потом мы загрузим наши модули и вмонтируем в HTML.
Тут появляются вопросы: «Как определить, какой модуль грузить?», «Как эти модули переключать на основе действий пользователя?», «Как пошарить данные?».
Давайте разбираться по порядку: у всех модулей есть что-то общее — адресная строка. С помощью неё мы можем определять, какие модули грузить и какие модули сейчас активны. Пользователь ходит по страницам, мы это отлавливаем и загружаем то, что нужно.
А как быть с общими данными?
Мы можем создавать собственные события и подписываться на них в каждом модуле, либо иметь какие-то общие сервисы, которые будут прокинуты в модули при их инициализации.
Но пока это всё теоретические догадки, давайте переходить к практике.
Практика
Начнём с составления схемы. В первую очередь мы должны создать корневой проект, в котором будет описана вся логика по управлению модулями. Назовём его router.
Пока в нём будет два файла: (1) HTML, который отдаётся пользователю, (2) JS, содержащий всю логику, связанную с получением модулей по сети и их монтированию.
Теперь перейдём к самим модулям. Наш микрофронтенд — это, по сути, обычное одностраничное приложение (SPA), за исключением пары деталей:
в отличие от обычных SPA, микрофронтенды не идут со своим HTML, а монтируются в уже существующую разметку;
микрофронтенды должны уметь шарить данные между собой.
Итого у нас получается: много модулей, похожих на SPA, но с парой нюансов, и router, который эти модули грузит и запускает для монтирования в свой HTML.
Выглядит немного страшно, но, к счастью, пилить свой велосипед мы не будем. Есть замечательная библиотека single-spa, которая применяет именно такой подход и реализует большинство нужных нам вещей.
Либа популярная, дока шикарная, есть ещё и поддержка всех фреймворков – считаю это просто подарком.
Итак, перейдём от схем к делу.
Первый шаг – реализация роутера. Напоминаю, что роутер является мозгом нашей архитектуры.
Этап 1: закладываем базу и реализуем роутер
Начнём с создания HTML. Это простой HTML-код, ничего необычного. За единственным исключением: нам нужно где-то установить якорь, куда будут монтироваться наши микрофронтенды. Допустим, это будет div с id=”content”
Далее, у нас есть идет index.js.
Здесь мы используем инструменты single-spa:
start – старт приложения;
registerApplication – регистрация модуля.
Закидываем пока что один модуль в массив и регистрируем его, указывая нужные параметры:
name – имя модуля;
app – метод, с помощью которого мы будем загружать модуль по сети;
activeWhen – метод, который будет определять активен ли модуль (вспоминаем адресную строку).
Давайте напишем реализацию для метода «app». Здесь нам помогут два инструмента: importmap и SystemJs.
Importmap – это JSON-объект, который позволяет импортировать внешние модули. Обычно для этого мы подключаем сборщики, например, Gulp или Webpack, но они полезны в большей мере в рамках одного проекта и на этапе сборке, нам же нужна вещь для runtime и которая будет работать сразу во всех модулях.
Синтаксис у импортмапов очень простой: ключ – название модуля, значение – ссылка на его местоположение.
Вот пример подключения Vue через CDN.
Теперь поговорим про System.js. Это загрузчик модулей, который позволяет загружать код во время выполнения программы (runtime), когда как Webpack и Gulp работают только во время разработки.
К тому же он является полифилом для импортмапов. Догоняем двух зайцев за одну вылазку.
Давайте всё это дело настраивать. Подключаем SystemJS и берём файл для поддержки amd-модулей (такой синтаксис будет на выходе у наших микрофронтендов).
А теперь указываем импортмапы и путь к нашему первому микрофронтенду (который пока будет лежать на локальном Dev-сервере).
Дополняем наш index.js.
В качестве параметров мы передаём строку home (на которую мы завязались в наших импортмапах). Мы, конечно, можем сейчас обойтись и без них (просто указать ссылку, а не имя модуля), но они нам пригодятся в дальнейшем, поэтому решил сразу вас погрузить в логику работы.
Вишенка на торте — настройка вебпака, чтобы наше приложение запустить. Тут базовые вещи, на них я останавливаться не буду.
Работает, но пока ничего интересного, давайте добавлять сюда микрофронтенды.
Этап 2: создаем микрофронтенды
Как мы помним, микрофронтенды — это обычные SPA-приложения, за исключением некоторых особенностей.
Сначала я использую обычную cli-команду для создания Vue-проекта «vue-create micro-home».
Затем мы устанавливаем два пакета: single-spa-vue (для интеграции с single-spa) и systemjs-webpack-interop (чтобы настроить наши ассеты).
И дорабатываем наш вебпак. Нам нужно, чтобы модуль был amd, файл назывался main.js и лежал на порту 4401 (так как именно такие требования сейчас у роутера).
Теперь про systemjs-webpack-interop. Это либа, которая поможет настроить правильные пути до картинок наших модулей. Ссылки к ассетам мы указываем в рамках самого микрофронтенда, а чтобы все работало нужно «путь к микрофронтенду + путь к ассету». А кто знает путь до нашего микрофронтенда? Правильно, импортмапы и systemjs. Поэтому пишем такую конструкцию и ссылаемся на home.
Напоследок допилим наш файл main.js. Подключаем файл, где мы использовали systemjs-webpack-interop, а затем пользуемся мощностями single-spa-vue.
Главное, что нужно не забыть – указать место для монтирования нашего модуля (параметр el), в него мы и запишем наш якорь в HTML, который мы размечали в самом начале.
Также экспортируем наружу три хука, с помощью них наш роутер вместе с single-spa будет управлять монтированием и демонтированием модуля.
Запускаем приложения и вуаля — два приложения, которые лежат в разных папках (а потом в репах) соединились в одно. Это именно то, что мы и хотели.
Чтобы показать, насколько просто масштабировать такую архитектуру, я создам ещё один микрофронтенд (orders).
В нём мы должны поменять public-path:
Указать новый порт.
А также добавить в импортмапы роутера.
И в массив модулей index.
Но как микрофронтенды будут взаимодействовать друг с другом, ведь у них общий якорь? Они же перезапишут друг друга? Если ничего не менять, то да, но как мы помним, у нас есть замечательный метод activeWhen, который как раз отвечает, кто сейчас активен.
Мы сделаем простую проверку названия модуля и адресной строки.
Запускаем и получаем: разные урлы, разные модули и всё это в одном приложении.
Этап 3: шаринг данных
У нас была ещё одна проблема — «как шарить данные между модулями». На самом деле нам ничего здесь придумывать не нужно, single-spa предоставляет решение этой проблемы. Единственно, что мы тут сделаем – обернём всё это в удобный интерфейс.
Мы создаём классы, которые имеют состояние, геттеры и сеттеры, а потом экземпляры этих классов (синглтоны) пробрасываем через single-spa во все модули. Так, мы получим возможность менять общее состояние из любого места нашей архитектуры.
Если же мы захотим подписываться на изменения (например, обновление счётчика в корзине), то можно использовать API браузера и кастомные события или же использовать свой бэкенд и, например, вебсокеты.
Напишем один такой класс:
Передадим его экземпляр с помощью параметра customProps:
В микрофронтенде мы сможем получить его через handleInstance и прокинуть дальше в приложение:
Этап 4: разбираемся с общими зависимостями
На этом этапе ещё не заметно, но у нас появилась проблема — дублируются зависимости микрофронтендов. Если раньше (с монолитом) у нас всего было по одному экземпляру, то сейчас каждый микрофронтенд тянет за собой вагончик своих пакетов. Тут есть решение, но оно имеет подвох, о котором я расскажу, когда будем говорить о минусах микрофронтендов.
Итак, чтобы решить проблему, воспользуемся нашими любимыми ипортмапами. Мы можем все повторяющиеся пакеты вынести в роутер, указать их в импортампе и удалить при сборке из микрофронтендов.
Перейдём к реализации. Сначала добавляем пакеты в импортмапы, которую вынесем в отдельный файл (для удобства).
И подключим внутри HTML. Чтобы избежать кэширования, добавим дополнительный параметр в query-ссылки:
А сам этот параметр будем проставлять в вебпаке:
И под конец перейдём в микрофронтенды и удалим пакеты после сборки:
Отлично, теперь дублирования нет.
Этап 5: Решаем остаточные проблемы (кеширование)
Что, опять? К сожалению — да. В реализации выше есть проблема с кэшированием. Мы назвали все выходные файлы наших сборок main.js и вскоре возникнет ситуация, когда разработчик внесёт какие-то правки в проект, но изменений итоговый клиент не увидит, так как браузер всё закэширует (названия-то не меняются).
В идеале хочется, чтобы кэширование осталось, но только в случае, когда изменений в клиенте не было. Если же код менялся — клиент обязан получить всё новое.
Быстрое решение — добавить динамический хэш в название файла после сборки. Но как мы теперь из роутера будем определять ссылку до нашего микрофронтенда?
Можем попробовать изменять импортмап роутера после релиза в микрофронтенде. Но тут тоже не всё гладко: роутер будет зависеть от микрофронтендов, соответственно, будут зависимости команд между собой, а нам это не нужно, так как хочется деплоится, когда хочешь и не блокировать других.
Какое же тогда решение нам выбрать? Давайте так: создадим манифест нашего модуля, где будет лежать новое название микрофронтенда, и именно манифест будем запрашивать в роутере и вытягивать нужную ссылку. Название манифеста мы менять не будем, а чтобы он не кэшировался, добавим query-параметры для запроса.
В итоге получается, что мы кэшируем микрофронтенды, когда изменений нет, а когда что-то новое появилось, то заново всё запрашиваем (так как название файла изменилось).
Настраиваем вебпак наших микрофронтендов:
И меняем логику загрузки микрофронтенда: сначала манифест, потом сам микрофронтенд. Для удобства вынесем всё это в отдельный сервис:
И подключаем его в index.js:
Это, в общем-то, всё. Мы успешно прошли через все этапы нашего роадмапа и получили готовое приложение, построенное на архитектуре микрофронтендов, которое решает все наши проблемы.
Самое время рассмотреть минусы такой архитектуры.
Минусы микрофронтендов
Дублирование зависимостей
«Так мы же решали такую проблему?». Да, но не до конца. Проблема решается, если в проектах используется одна и та же версия пакета, но если она отличается, то придётся тащить все. Как показывает практика, очень накладно обновлять пакеты во всех проектах (у нас их больше 30), поэтому итоговое приложение весит больше, чем хотелось бы.
Сложность архитектуры
Хоть мы и написали рабочую демку, но это только база, потом нам в любом случае придётся накручивать кастомную логику в зависимости от потребностей. А это время и усилия.
Дублирование кода
Если в монолите мы можем легко переиспользовать код, то в микрофронтендах это делать сложнее. Конечно, есть решения: сделать библиотеку компонентов (как это сделали мы), вынести все утилиты в одно место, написать шаблоны, Но всё равно встречаются места, которые идентичны во всех проектах (например, конфиг) и когда наступит время это править, может быть больно.
Баланс между минусами и плюсами я бы подвёл вот так:
ПЛЮСЫ | МИНУСЫ |
Можно легко изменить стек: каждый микрофронтенд может быть написан на любом фреймворке | Много лишних зависимостей |
Каждое приложение не зависит от других: кейсы, когда твой код может сломать функциональность другой команды, становятся менее частыми | Сложность архитектуры |
Быстрое развёртывание и частые релизы: релизный цикл команды не зависит от других + деплоить нужно одну часть приложения, а не всё разом | Дублирование кода |
Простое масштабирование: создавай хоть 1000 проектов, всё равно будешь начинать с чистого листа | |
Удобная локальная разработка: работаешь в рамках одного микрофронтенда |
Итоги
Для небольших компаний, стартапов или проектов, которые разрабатываются небольшим количеством людей, архитектура из микрофронтендов на мой взгляд — это перебор. Она добавляет сложности, которые не всегда оправданы. В таких случаях проще, удобнее и быстрее работать в одной репе.
Однако, если у вас несколько команд и большое приложение, микрофронтенды – крутое решение, которое заметно упростит вам жизнь.
Оставлю здесь ссылки на код:
🤜 Роутер
🤜 Главная
🤜 Заказы