Webpack Module Federation: как это работает

Курс объясняет, как Module Federation позволяет нескольким приложениям обмениваться модулями во время выполнения. Разберём ключевые понятия, настройку, обмен зависимостями и типовые сценарии использования в microfrontend-архитектуре.

1. Зачем нужен Module Federation и какие задачи решает

Зачем нужен Module Federation и какие задачи решает

Module Federation (MF) — механизм Webpack 5, который позволяет загружать и использовать модули из другого приложения во время выполнения (runtime). То есть один фронтенд может подключить код, собранный и задеплоенный отдельно, без пересборки всего продукта.

Проблема, которую MF решает

В больших SPA со временем появляются типовые боли:

  • Монолитная сборка: любой маленький UI-фикс требует пересобрать и выкатить всё приложение.
  • Долгие сборки и рост бандла: общие зависимости дублируются, стартовая загрузка тяжелеет.
  • Сложная командная работа: много команд правят один репозиторий/пакет, растёт число конфликтов и связность релизов.
  • Миграции “всё или ничего”: сложно постепенно переехать на новый фреймворк/архитектуру.
  • MF предлагает практичный компромисс: разделить продукт на независимые части, но при этом сохранить возможность переиспользовать код и зависимости.

    Какой подход даёт Module Federation

    В терминах MF обычно есть роли:

  • Host (хост/контейнер) — приложение, которое подгружает удалённые модули.
  • Remote (удалёнка/провайдер) — приложение, которое публикует часть своего кода для потребления.
  • Shared dependencies — зависимости, которые можно делить, чтобы не грузить несколько копий (например, один react).
  • Выглядит концептуально так:

    Ключевые задачи, которые решает MF

  • Независимые релизы (decoupled deployments)
  • Команда, отвечающая за “Корзину”, может выкатить обновление без релиза “Каталога” — хост подцепит новый remote при следующем заходе пользователя.

  • Микрофронтенды без жёсткой упаковки
  • MF — один из самых “приземлённых” способов делать microfrontend: интеграция идёт через загрузку реального JS-модуля, а не через iframe или копирование артефактов.

  • Совместное использование зависимостей
  • Можно избежать ситуации, когда на странице одновременно работают две копии одной библиотеки. Это снижает вес и риск конфликтов версий.

  • Переиспользование фич и UI-компонентов между приложениями
  • Общие блоки (хедер, профиль, платежи, дизайн-система) можно публиковать как remote и подключать там, где нужно.

  • Постепенные миграции
  • Позволяет переносить части приложения шаг за шагом: старый хост продолжает жить, а новые фичи приезжают как remotes.

    MF в сравнении с альтернативами

    | Подход | Что даёт | Типичный минус | |---|---|---| | Монолит | Простая интеграция | Релизы связаны, бандл растёт | | Общая библиотека (npm-пакет) | Переиспользование кода | Нужно публиковать/обновлять версии и пересобирать потребителей | | Copy-paste/дублирование | Быстро “прямо сейчас” | Расхождение логики, техдолг | | Module Federation | Runtime-подключение модулей + шаринг зависимостей | Требует дисциплины версий и контрактов между командами |

    <details> <summary>

    Когда Module Federation особенно полезен

    </summary>

  • Несколько команд разрабатывают разные домены (каталог, корзина, аккаунт) и хотят независимые релизы.
  • Есть платформа/экосистема: много приложений, которым нужны общие фичи.
  • Важна скорость вывода изменений без пересборки всех потребителей.
  • Нужно контролируемо уменьшать дубли зависимостей и вес загрузки.
  • </details>

    2. Ключевые термины: host, remote, exposed modules, runtime

    Ключевые термины: host, remote, exposed modules, runtime

    В предыдущей статье уже разобрали, зачем нужен Module Federation. Здесь закрепим базовый словарь — без него сложно понимать конфигурации и отладку.

    Host (хост, container consumer)

    Host — приложение, которое выполняет загрузку чужих модулей во время работы и встраивает их в свой UI/логику.

    Практически host отвечает за:

  • описание удалённых источников (remotes) и их адресов;
  • инициализацию общего пространства зависимостей (share scope), чтобы договориться, какие версии библиотек использовать;
  • запрос конкретного удалённого модуля (например, компонента) в момент, когда он реально нужен.
  • Удобная ментальная модель: host — это «точка сборки страницы в браузере», но сборка идёт не на этапе CI, а в рантайме.

    Remote (удалёнка, container provider)

    Remote — приложение, которое публикует часть своей сборки наружу, чтобы другие могли её подгрузить.

    Remote генерирует специальный файл-«манифест» (часто его называют remoteEntry), который:

  • регистрирует контейнер remote;
  • содержит таблицу соответствий: имя exposed-модуля → как его получить;
  • умеет участвовать в шаринге зависимостей.
  • Remote не «встраивается сам» — его всегда кто-то подхватывает (host или другой remote).

    Exposed modules (экспортируемые / публикуемые модули)

    Exposed module — это явно объявленный модуль, который remote разрешает импортировать извне.

    Важно различать:

  • внутренние модули remote (обычный код приложения);
  • exposed modules — публичная поверхность (контракт), которую можно потреблять.
  • Обычно экспонируют:

  • UI-компоненты (Header, Widget)
  • утилиты/SDK (клиент для API домена)
  • роуты или “страницы”
  • Чем меньше и стабильнее exposed-поверхность, тем проще независимые релизы: изменения внутри remote не должны ломать host, пока контракт exposed-модулей сохраняется.

    Runtime (рантайм Module Federation)

    Runtime — это момент, когда страница уже загружается/работает в браузере, и MF выполняет “склейку” приложений на лету.

    Типовой поток в рантайме выглядит так:

    Ключевая идея: импорт становится двухшаговым — сначала нужно получить «контейнер» remote, затем попросить у него конкретный exposed-модуль.

    <details> <summary>

    Мини-словарь синонимов, которые встречаются в документации

    </summary>

  • Container — техническое название объекта remote, который умеет init (шаринг) и get (выдать модуль).
  • remoteEntry — файл, через который контейнер становится доступен host.
  • consumer/provider — роли host (потребитель) и remote (поставщик).
  • </details>

    3. Базовая настройка: remoteEntry, exposes и remotes

    Базовая настройка: remoteEntry, exposes и remotes

    Эта статья — про «проводку» Module Federation: какой файл публикует remote, что именно он отдаёт наружу, и как host на это подписывается. Термины host/remote/runtime уже были разобраны ранее.

    1) remoteEntry: что это и где он появляется

    remoteEntry.js — это входной файл контейнера remote, который загружается host’ом. Он создаётся плагином ModuleFederationPlugin и обычно лежит рядом с остальными ассетами сборки remote.

    Ключевые практические моменты:

  • Имя можно менять через filename (не обязано быть remoteEntry.js).
  • Файл должен быть доступен по URL (CDN/статика), иначе host не сможет подключить remote.
  • Частая причина ошибок — неверный publicPath у remote: контейнер загрузился, но чанки exposed-модуля ищутся «не там».
  • Мини-визуализация:

    2) exposes: публичная поверхность remote

    exposes — это явный список модулей, которые remote разрешает импортировать извне.

    Пример конфигурации remote (Webpack 5):

    Как это читать:

  • ./Headerпубличный ключ (контракт), который видит потребитель.
  • ./src/ui/Headerреальный путь внутри remote.
  • Правило: ключи в exposes лучше делать стабильными, а внутренние пути можно менять без влияния на host.

    3) remotes: как host “подписывается” на remote

    В host вы описываете, где взять контейнер и как его зовут.

    После этого в коде host становится возможен импорт по контракту:

    Важно: shop слева в remotes и name: 'shop' у remote должны совпадать — это имя контейнера.

    <details> <summary>

    Типовые ошибки и быстрые проверки

    </summary>

  • Cannot read properties of undefined (reading 'get')
  • - host нашёл remoteEntry, но объект контейнера не зарегистрировался: проверьте совпадение name и алиаса в remotes.

  • 404 на чанки после успешной загрузки remoteEntry
  • - проблема в publicPath у remote (особенно при деплое под под-путь).

  • Импорт shop/Header не находится
  • - ключ в exposes должен быть ./Header, а импорт — shop/Header (без точки и слэша в начале).

    </details>

    4. Shared-зависимости: singleton, versioning и избегание дублей

    Shared-зависимости: singleton, versioning и избегание дублей

    Shared-зависимости — это настройка shared в ModuleFederationPlugin, которая позволяет не тащить одну и ту же библиотеку несколько раз и, по возможности, договориться о единой версии, доступной и host, и remote (см. термины про runtime и контейнеры в прошлых статьях).

    Зачем вообще шарить зависимости

    Если host и remote каждый привезёт свой react, на странице окажутся две независимые копии. Это приводит к:

  • увеличению веса загрузки;
  • ошибкам интеграции (особенно для react, react-dom, styled-components, роутеров);
  • «невидимым» багам: контекст/хуки/синглтоны работают в разных инстансах.
  • Визуально:

    singleton: один инстанс на всё приложение

    singleton: true говорит MF: в share scope должна остаться только одна копия пакета.

    Типичные кандидаты на singleton:

  • react, react-dom
  • библиотеки со своим глобальным состоянием/регистрами
  • дизайн-системы, которые ожидают единые контексты/темы
  • Важно: singleton не «магия совместимости». Он лишь заставляет выбрать один экземпляр — поэтому критично, чтобы версии реально были совместимы.

    Versioning: как выбирается версия

    MF использует semver-логику и правила из shared:

  • requiredVersion — какую версию (или диапазон) модуль «ожидает».
  • strictVersionзапрет использовать несовместимую версию (лучше упасть сразу, чем получить странные баги).
  • Практическое правило:

  • Для критичных singleton-зависимостей задавайте requiredVersion.
  • Включайте strictVersion, когда несовместимость опаснее, чем отказ загрузки.
  • Ключевые флаги shared (шпаргалка)

    | Опция | Смысл | Когда применять | |---|---|---| | singleton | один инстанс библиотеки | React/роутер/библиотеки с глобальным состоянием | | requiredVersion | ожидаемый диапазон версий | чтобы разные команды держали контракт по версиям | | strictVersion | не принимать несовместимые версии | для «хрупких» библиотек | | eager | грузить сразу, не лениво | редко; когда нельзя откладывать загрузку shared | | import: false | не бандлить свою копию, только брать из share scope | когда пакет точно должен прийти от host |

    Как реально избегать дублей: рабочие практики

  • Одинаковые версии в lockfile: если версии различаются, MF иногда вынужден держать несколько копий.
  • Singleton-парами: react и react-dom должны быть согласованы.
  • Не шарьте всё подряд: шарьте то, что либо тяжёлое, либо должно быть единым инстансом.
  • Следите за peerDependencies: если библиотека ожидает react как peer, это хороший сигнал шарить react как singleton.
  • <details> <summary>

    Частые симптомы и что проверять

    </summary>

  • "Invalid hook call" / контекст не работает → почти всегда две копии react.
  • Всё настроено, но дубли остались → версии реально разные (например, 18.2.0 vs 18.3.0), либо разные сборки пакета.
  • Падает при инициализации remotestrictVersion выявил несовместимость: это не баг MF, а сигнал выровнять версии.
  • </details>

    5. Практика и частые проблемы: загрузка, ошибки, деплой

    Практика и частые проблемы: загрузка, ошибки, деплой

    Эта статья — про то, что чаще всего ломается в рантайме: загрузка remoteEntry, подкачка чанков exposed-модулей, совместимость окружения и нюансы деплоя. Базовые понятия (remoteEntry, exposes, remotes, shared) уже разобраны в предыдущих материалах.

    Как мыслить про загрузку в браузере

    Удобная ментальная модель — две сетевые стадии:

    Поэтому «remoteEntry загрузился» ещё не означает, что модуль загрузится: вторая стадия чаще всего ломается из‑за путей, кэша или CORS.

    Быстрая диагностическая таблица

    | Симптом | Где смотреть | Типовая причина | Быстрый фикс/проверка | |---|---|---|---| | remoteEntry.js 404 | Network | неправильный URL в remotes, не тот домен/путь | открыть URL remoteEntry напрямую в браузере | | remoteEntry 200, но дальше ChunkLoadError | Network → chunk requests | неверный publicPath у remote, деплой под подпапку | проверить, откуда грузятся чанки (Origin/Path) | | container.get is not a function / undefined | Console | контейнер не зарегистрирован/переопределён глобально | проверить совпадение имён контейнера и алиаса, отсутствие конфликтов имён | | Ошибка инициализации shared | Console | несовместимые версии/настройки shared | временно выключить strictVersion, выровнять версии, проверить singleton-пары | | Работает локально, падает на стенде | Network/Console | CORS, mixed content (http/https), CSP | проверить заголовки CORS, протоколы, CSP script-src |

    Частые “подводные камни” деплоя

  • Кэширование remoteEntry
  • - Если remoteEntry.js закэширован «навечно», host может жить на старом контракте. - Практика: либо короткий TTL для remoteEntry, либо версионированный URL (а host получает актуальный URL через конфиг окружения).

  • Rollback без сюрпризов
  • - Откат remote должен быть совместим с тем, что ожидают текущие host’ы. - Практика: не ломать публичные ключи exposed-модулей; изменения делать совместимыми, пока все host не обновятся.

  • CORS и безопасность
  • - Remote часто лежит на другом домене/CDN. - Нужны корректные CORS-заголовки для загрузки скриптов и политика CSP, разрешающая источник remote.

  • Разные окружения (dev/stage/prod)
  • - Ошибка класса «не там ищем remote» решается не кодом, а конфигурацией. - Практика: адреса remote задавать через переменные окружения/конфиг, а не “хардкодить”.

    Чеклист перед релизом

  • Проверить доступность remoteEntry и всех чанков (не только первого файла).
  • Пройти сценарий с «чистым кэшем» и с «тёплым кэшем».
  • Убедиться, что политика кэша осознанная: что можно кэшировать долго, а что нет.
  • Протестировать кросс-доменную загрузку (CORS/CSP/https).
  • Зафиксировать контракт: какие exposed-модули считаются публичными и как меняются.
  • <details> <summary>

    Практика для отладки “в поле”

    </summary>

  • Логируйте URL remote и фактические запросы чанков: часто проблема видна прямо в Network.
  • Делайте фолбэк в UI на случай недоступности remote (например, заглушка/сообщение), чтобы падение remote не роняло весь host.
  • Если ошибки плавающие — ищите гонки деплоя: CDN обновил remoteEntry, а чанки ещё старые (или наоборот). Решение — атомарный деплой артефактов или версионирование путей.
  • </details>