Module Federation — это подход, при котором можно разделить приложение на небольшие отдельные модули и в рантайме объединять в единое приложение. Это одно из немногих рабочих решений для разработки микрофронтендов. Почему оно лучше, чем остальные, какие ещё решения для микрофронтендов существуют, что вообще такое микрофронтенды и зачем они нам нужны, расскажу в статье. В конце поделюсь полезными ссылками на статьи, видео и документацию, которые помогут углубиться в тематику Module Federation.
Так же будет немного практики. Настоятельно рекомендую самостоятельно опробовать технологию чтобы материал хорошо закрепился в памяти. Практика наше всё! Поехали!
Немного об авторе:
Алексей Вечканов
FrontEnd Tech Lead направления «Онбординг», 8 лет в вебе, 4 — в Альфа-Банке. В основном пишет фронт, но увлекается DevOps. GitHub автора
Содержание статьи:
Проблема монолитного приложения.
Что такое микросервисы?
Что такое микрофронтенды
Существующие решения.
Описание Module Federation.
Архитектура Module Federation.
Демо.
Проблемы.
Полезные ссылки.
Проблема монолитного приложения
Несколько команд разрабатывают одно большое приложение, что приводит к конфликтам и долгой доставке приложения в продакшн.
Один и тот же компонент нужен сразу в нескольких приложениях.
Вынести общие компоненты в отдельную библиотеку, конечно, можно, но дорого и долго обновлять во всех приложениях после обновления библиотеки.
Перезагрузка страницы при переходе между приложениями довольно дорога.
Большой размер приложения после итоговой сборки.
Решение? Распиливаем фронт на части.
Некоторые преимущества которые мы получим:
Сможем независимо, отдельными командами, разрабатывать отдельные части приложения.
Автоматически обновлять код сразу в нескольких приложениях.
Уменьшим размер основного бандла.
Получим общие компоненты и быстрый доступ к ним.
Ускорим поставки в продакшн.
Концепция разделения монолита на части пришла к нам из бэкенда — от микросервисов.
Что такое микросервисы?
Микросервисы — это популярный архитектурный паттерн для разработки. Используют в бэкенде уже достаточно много лет. Сам термин «микросервисы» озвучили в 2012 году на конференции 33rd Degree Conference в докладе Джеймса Льюиса «Micro Services – Java, the Unix Way»
Но сам подход появился еще раньше. Например, одна из философий Unix звучит как:
«Пишите программы, которые делают что-то одно и делают это хорошо», а Unix уже очень много лет. Оттуда же, кстати, пошёл и термин «монолит».
Если простыми словами, то мы берём наш монолит и делим его на более мелкие процессы. Каждый процесс запускаем в отдельном контейнере (мы используем Docker для запуска наших процессов). Процесс работает и никому не мешает, а когда нужен — мы к нему обращаемся.
Приведу аналогию. Это как если бы вся документация разработчиков, QA, аналитиков, DevOps-инженеров, продактов, аналитиков, дизайнеров лежала бы в одном огромном документе на 1000 страниц. Неудобно. Поэтому мы этот монолит «распилим»: документацию для разработчиков в один документ, для продактов в другой, для QA в третий, а на их месте оставляем ссылки, чтобы их «вызывать».
Микросервисы — это низкая связность компонентов системы между собой, что даёт высокую автономность командам. Визуально выглядят так: каждое приложение — отдельный сервис, в основном, в Docker. Сервисы общаются друг с другом по протоколам, чаще это HTTP, а с запросами клиентов и его разбивкой на разные сервисы работает обычно API Gateway.
Что такое микрофронтенды?
Это аналогичный архитектурный паттерн, но для фронтенда: независимо доставляемые клиентские компоненты в браузере объединяются в единое целое. Здесь мы представляем веб-приложение как набор функций, за каждую из которых отвечает отдельная команда.
Что дают микрофронтенды?
Упрощение координации задач. Команды больше сфокусированы на своих предметных областях, у каждой чёткая зона ответственности. Например, одна команда может делать только блок меню, а другая только футер приложения.
Возможность независимого развертывания. Позволяет командам быть автономнее — отдельно развёртывать разные части приложения.
Сокращение цикла поставки. Каждая команда быстрее доставляет свой код в продакшн. Более быстрая сборка и тесты, как нового функционала, так и исправления ошибок.
Снижение сложности. Отдельные части меньше и легче для понимания, чем большой сложный монолит.
Изоляция ошибок. Проще изолировать сбои в отдельных частях приложения, пока другие части работают. Если в одном сервисе ошибка, то она будет влиять на остальные меньше, чем если бы это был монолит.
С одной стороны микрофронтенды похожи на микросервисы, с другой — есть коренные отличия.
Микросервисы vs микрофронтенды | |
Очень похожи в разработке |
Не похожи в рантайме |
Разные репозитории |
Общий DOM |
Разные CI/CD |
Общий event loop |
Общая адресная строка |
|
Общие globals (window) |
Зрелость методов и технологий гораздо ниже, чем у микросервисов, потому что подход стал использоваться позже, чем в бэкенде. Но некоторые решения всё же появились.
Существующие решения
Server Side Fragment Composition. Очень старая технология, когда веб-сервер собирает из разных блоков HTML (генерируемых разными сервисами) единую страницу. По сути — это SSR. Сервер собирает итоговую страницу HTML из нескольких отдельных страниц прямо на сервере и отдаёт в браузер.
Это не наш вариант.
Полезная ссылка: статья «Server-side page fragment composition»
Iframes. Наверно, с ними многие знакомы. Работает на клиенте — позволяет вставлять блоки ваших приложений по URL как отдельные страницы (интерфейсы могут общаться посредством postMessage), а приложения изолированы.
Но много минусов:
Боль с модальными окнами, когда CSS у окон не работает как нужно.
Проблема выпадающих меню.
Проблема SEO для поисковиков.
Проблема с производительностью: одни и те же библиотеки загружаются повторно в каждый iframe. Здесь грузится фактически несколько сайтов одновременно.
Web Components — веб-стандарт 2011-2013 годов.
Веб-компоненты — это набор различных технологийЮ которые позволяют создавать повторно используемые настраиваемые элементы с их функциональностью, инкапсулированной отдельно от остальной части вашего кода, и использовать их в ваших веб-приложениях.
Звучит неплохо, но тоже не наш вариант. Эта технология никак не помогает создавать архитектуру для построения приложения в рантайме, не помогает сделать единое SPA-приложение.
Полезная ссылка: документация по веб-компонентам.
Linked Pages & SPAs. Технология, когда создаются два очень похожих приложения, у них общее меню, дизайн, и они развернуты на двух одинаковых доменах. Различается только basePath (иногда еще называют contextRoot) — небольшая часть URL сразу после доменного имени. Балансировщик нагрузки понимает к какому приложению идёт запрос и перенаправляет его на то или иное SPA-приложение. Роутинг внутри SPA-приложения дешевый и быстрый.
Этот подход работает, и работает у нас прямо сейчас в некоторых системах. Но есть проблема, из-за которой мы хотим избавиться от Linked Pages & SPAs. Она в том, что для клиента ничего не меняется, но приложение грузится повторно.
Полезная ссылка: доклад Александра Китова про атомарные SPA, нашего разработчика, где он рассказывает, как работает эта технология.
Single SPA. Один из самых популярных фреймворков для SPA (основан в 2016). Это фреймворк, который позволяет разметить страницу и загружать приложения в отдельные участки. Можно объединять разные приложения, можно объединить React, Angular, Svelte и другие фреймворки. Он позволяет выгружать или загружать эти приложения в любой момент.
Но из-за минусов не подходит для нашей задачи:
У него нет реализации css, fonts, images, приходится что-то изобретать с загрузкой ресурсов.
Одни и те же библиотеки загружаются несколько раз.
Полезная ссылка: single-spa.js.org.
Свой велосипед. Это свобода решений. У нас в банке есть свои велосипеды. Но их дорого поддерживать, потому что комьюнити решает, когда много людей развивают одну технологию. Без комьюнити развивать и поддерживать придётся вам самостоятельно.
Теперь подходим к тому, ради чего затевалась статья — к Module Federation.
Module Federation: что умеет и для чего используется
Module Federation — «новая» киллер фича в Webpack 5. Позволяет точечно подключать модули из другой webpack-сборки, которая расположена на другом хосте.
Разработчик Module Federation (далее MF) — Zack Jackson. Работал он, конечно, не один — с командой Webpack — но Зака можно считать идейным вдохновителем и основным разработчиком.
Полезная ссылка: GitHub Зака.
Этапы разработки. Ресёрч и прототипирование проекта началось ещё в середине 2017.
Первый анонс в виде статьи «Micro-FE Architecture: Webpack 5, Module Federation, and custom startup code» на блоге Medium— в октябре 2019 года. В октябре 2020 проект зарелизили как core-плагин к Webpack 5. Поэтому, если захотите использовать MF, то без Webpack 5 не обойтись.
Цели. MF разрабатывали не просто так, а с конкретными целями:
Нет перезагрузкам страниц при переходе между модулями.
Не грузить vendor code, который уже предоставлен другой Webpack-сборкой, например, React.
Каждый MF-модуль может быть standalone — без внешних зависимостей.
Не пересобирать основное приложение, если поменялся shared-модуль. Например, когда шарится общая кнопка и мы поменяли её цвет, поставляем только шаренный модуль, и он автоматически раздаёт новую реализацию этой кнопки.
Оркестрация должна происходить на стороне пользователя, позволяя загружать части без «умного сервера», чтобы спокойно раздаваться с CDN и не только.
Скрин целей из GitHub (ссылка выше), с которыми Зак подходил к разработке.
Получилось ли? Ещё как получилось!
Что умеет MF? Module Federation позволяет одному webpack-приложению динамически подгружать код из другого webpack-приложения. Это webpack-plugin, он импортирует чанки из стороннего webpack bundle в рантайме: CSS, images, fonts, любые модули, что собирает Webpack. Всё, что может сбандлить Webpack с MF, может быть зашарено между микрофронтендами.
Грубо говоря, MF позволяет объединить в рантайме два Webpack манифеста, и заставить их работать вместе так, будто вы их скомпилировали с самого начала.
Также:
MF может шарить между собой общие зависимости, если совпадает их версия. Например, если React уже загружен, то он не будет повторно грузиться со стороннего webpack-приложения.
MF могут быть развёрнуты на разных доменах и деплоиться независимо.
«Сборка» происходит на лету при запуске приложения в браузере. Браузер подтягивает «запчасти» и собирает единое приложение.
Мини-резюме. У каждого микрофронтенда может быть свой репозиторий, независимые сборки и деплои. Микрофронтенд может быть запущен как standalone SPA, а в браузере всё работает как монолит.
Кажется, что это именно то, что нам нужно!
Архитектура: как работает Module Federation
Сначала терминология, чтобы мы были в одном контексте.
Host (consumers) — это бандл, который первый инициализировался во время загрузки страницы. Это и есть наше корневое приложение, которое подтягивает другие части.
Remote (consumable) — другой бандл, чьи некоторые части может импортировать host. Host запрашивает у remote шареные компоненты.
Omnidirectional host — это бандл, который одновременно может быть и host, и remote. Он может быть и SPA-приложением, а может просто раздавать общие компоненты.
Exposed modules. Модули, которые будут доступны другим приложением для импорта, например, компоненты, картинки или стили.
Shared modules. Модули, которые могут быть общими для всего приложения, например, React.
Как Webpack собирает модули? Чтобы понять как работает MF, важно уяснить как Webpack работает с модулями после сборки. Для этого я подготовил небольшой пример. Вы можете скачать репозиторий и самостоятельно ознакомиться с итогом сборки.
Пример. У нас имеется корневой файл, который импортирует функцию action из модуля, получает новое значение, а затем экспортирует полученное значение.
import action from './module';
const value = action();
export default value;
Выполним сборку Webpack. В итоговом файле мы видим несколько вызовов eval. Это потому что Webpack хранит наш код в виде строки (только в режиме разработки). Но если убрать все лишнее мы получим следующее:
Условно, Webpack добавляет наш модуль в modules scope (не путать с областью видимости) в специальный массив __webpack_exports__
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, { "default": () => (action) });
function action() {return 'value';};
А вот тут Webpack успешно достает наш модуль из modules scope и присваивает переменной _module__WEBPACK_IMPORTED_MODULE_0__
для дальнейшего использования.
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"default": () => (__WEBPACK_DEFAULT_EXPORT__)
});
var _module__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/module.js\");
const value = (0,_module__WEBPACK_IMPORTED_MODULE_0__["default"])();
const __WEBPACK_DEFAULT_EXPORT__ = (value);
Единственное, что нужно уяснить из приведенного примера — это то что webpack собирает все наши модули в некий массив modules scope.
Схема работы MF. Теперь давайте взглянем на следующую схему работы MF.
Слева желтым — host, то приложение которое будет получать общие модули.
Справа оранжевым — Remote контейнер, который раздаёт общие модули.
Посередине — share scope. Это как раз наш modules scope.
Когда host запросит у контейнера шареные компоненты, они попадут в тот самый скоуп, о котором чуть выше говорил (в примере). Webpack даже не будет знать, что эти компоненты были откуда-то загружены, и будет использовать так, будто они всегда были в основной сборке.
Конечно, приведенный выше пример очень условный, в реальности все намного сложнее, но главное понять принцип.
Загружаются Remote модули, обычно, с помощью динамического import() — прямо в рантайме.
Полезные ссылки. Подробнее узнать как работает WMF можно из расширенных гайдов:
Видео о том, как не грузить vendor code, который уже предоставлен другой Webpack-сборкой, например, React: «A look at the source code and how it works»
Демо.
Чтобы лучше понять суть, я подготовил еще немного практики. Ниже ссылки на 2 репозитория. В одном приложение Host, во втором Remote. Host загружает у Remote общий компонент.
Скачайте репозитории, установите зависимости, запустите оба сервера и посмотрите как они работают в связке. Экспериментируйте - отключите Remote приложение, посмотрите как будет работать Host, создайте свои общие компоненты.
Сборку я намеренно упростил, чтобы понять принцип и не усложнять контекст. Демонстрация работы с репозиториями есть на видео:
Проблемы Module Federation
Их довольно много, но их можно обойти.
Возможен vendor lock. Например, два приложения подтягивают общую компоненту. Если они завязаны на определенную версию библиотеки, то можно оказаться в зависимости от этой версии: не сможем обновиться на Host-приложении.
Слёзы и нервы при разработке/дебаге/поставке: у нас два разных компонента, которые нужно ставить в продакшн, иногда одновременно, иногда отдельно. Нужны новые подходы с деплоем
С тестированием пока всё сложно. Если поменять интерфейс выставленного модуля, то приложение сломается. Последствия изменений такие же, как если бы мы поменяли схему своего REST API. Например, если мы поменяли React-компонент, изменили API, то и Host-приложение может сломаться, потому что завязано на старое API.
Пока схема тестирования такая:
В модули выносятся только те компоненты, которые можно протестировать самостоятельно, вне иного контекста.
Каждый модуль тестируется либо unit-тестом, либо Cypress тестом (Cypress фреймворк для Ent-to-End тестирования).
Также есть интеграционные тесты которые охватывают сразу все приложения и тестируют загружаемые модули.
Можно увлечься и наимпортировать вагон интерфейсов и библиотек. Когда мы импортируем шареный модуль, все импорты, которые там используются, тоже попадают в импорт. Можно попасть в такую ситуацию, когда у нас наимпортирована гора библиотек из Remote-приложения.
SSR есть, но его трудно реализовать. По SSR уже есть React 18 альфа-версия, которая поддерживает асинхронную гидрацию.
Есть проблемы с асинхронным запуском. Это bootstrap file, к фреймворку не имеет отношения. Если захотите поэкспериментировать, то прикладываю видео, где Зак рассказывает как его реализовать.
Полезные ссылки
Видео о том, как в случае недоступности шареных компонентов сделать fallback на npm пакете — «How to build a resilient. shared Header/Footer using Module Federation».
Статья как готовить шареные модули. Она 2020 года, но ещё актуальна.
Дашборд (dashboard-plugin), который делает сам Зак. Помогает видеть в UI, как и откуда ваши модули импортируют компоненты. Но ещё в разработке.
На этом всё. Надеюсь, статья была полезна, вы изучите дополнительные материалы и рассмотрите возможность применения Webpack Module Federation.
Эта статья подготовлена на основе доклада Алексея на митапе Alfa JS Meet UP. Если было интересна, приходите на JS Meet UP 1 июня в 19:00. Поговорим о проблемах и решениях во фронтенде, карьерном пути и работе в команде. Ссылка для регистрации. Подробная программа в анонсе.
Также рекомендуем почитать.
Подписывайтесь на Alfa Digital Jobs — там мы интересно и весело рассказываем про нашу работу, делимся новостями и полезными советами, иногда даже шутим. Приходите к нам в команды, сейчас у нас открыты несколько вакансий.
Комментарии (7)
Kirill_Aksenov
27.05.2022 14:56+2Спасибо за полезную статью, мне, как человеку с небольшим опытом очень интересно почитать про подходы, паттерны и как изнутри устроены сборщики
script3004
27.05.2022 15:28Если зависимость в мфе имеет другую версию, её можно положить в другой скоп, и очистить при онмаунте мфе. На самом деле wmf имеет больше минусов и есть среди них более существенные
noodles
27.05.2022 18:28+2Linked Pages & SPAs. Технология, когда создаются два очень похожих приложения, у них общее меню, дизайн, и они развернуты на двух одинаковых доменах. Различается только basePath
Всегда так делали.. не знал что у этого есть официальное определение..)
По ощущениям одни плюсы - можно каждую страничку-спа делать на разном стеке; плюс также удобно разносить ответственность\задачи постранично на разных людей.
Условно огромное приложение не ощущается тормознутым монстром, т.к. на каждой страничке свой небольшой бандл.. и при переходе между страничками за счёт настоящей браузерной перезагрузки всё "подчищается\обновляется" и начинается с чистого листа.
Для тех кто придумал "перезагрузка страниц - это плохо" - какая вам разница где наблюдать спинер.. внутри приложения, или во вкладке браузера?..) в гитлабе вон почти каждая страничка перезагружается.. сильно плохо что-ли?Но есть проблема, из-за которой мы хотим избавиться от Linked Pages & SPAs. Она в том, что для клиента ничего не меняется, но приложение грузится повторно.
Почему это проблема? Зашли на страницу-1 - загрузился скажем реакт-бандл.js, в меню нажали на другой раздел - перешли на страницу-2 - загрузился ангуляр-бандл.js. Нажимаем назад - происходит переход на предыдущую страничку - реакт-бандл.js берётся из кеша (никакой повторной загрузки). Причём за счёт такой изолированности и малого объёма бандлов - приложение в целом ощущается быстрее.
BlezPaskal
Из статьи я так понял, что вы пока не перекатиились на MF? в чем сложности?
Hydrock
Сложности очень часто бывают из-за инфраструктуры. Зачастую очень сложно согласовать несколько разных решений в одном. Мы уже активно используем MF в нескольких проектах. Под это дело даже пишется обертка, некий конструктор, который позволит динамически набирать любые страницы. Надеюсь в будущем об этом расскажу тоже.
BlezPaskal
Хотелось бы узнать об этом на ваших примерах