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

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

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

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

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

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

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

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

MF предлагает практичный компромисс: разделить продукт на независимые части, но при этом сохранить возможность переиспользовать код и зависимости.

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

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

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

Выглядит концептуально так:

[ Host (Shell) ]  --->  подгружает  --->  [ Remote A: Header ]
       |                                  [ Remote B: Checkout ]
       └------------------------------->  [ Shared: react, ui-kit ]

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

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

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

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

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

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

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

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

<details> <summary>

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

</summary>

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

</details>

Ключевые термины: 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 — публичная поверхность (контракт), которую можно потреблять.

Обычно экспонируют:

  1. UI-компоненты (Header, Widget)
  2. утилиты/SDK (клиент для API домена)
  3. роуты или “страницы”

Чем меньше и стабильнее exposed-поверхность, тем проще независимые релизы: изменения внутри remote не должны ломать host, пока контракт exposed-модулей сохраняется.

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

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

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

[Host runtime]
  1) Загружает remoteEntry (JS)
  2) Инициализирует share scope (общие зависимости)
  3) Запрашивает exposed-модуль по ключу
  4) Получает factory-функцию и исполняет её → модуль готов

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

<details> <summary>

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

</summary>

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

</details>

Базовая настройка: 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-модуля ищутся «не там».

Мини-визуализация:

Host загружает:
  https://remote-app.com/assets/remoteEntry.js

А remoteEntry уже подгружает:
  остальные чанки remote (по publicPath)

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

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

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

// webpack.config.js (remote)
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shop',
      filename: 'remoteEntry.js',
      exposes: {
        './Header': './src/ui/Header',
        './CheckoutApi': './src/api/checkout',
      },
    }),
  ],
};

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

  • ./Headerпубличный ключ (контракт), который видит потребитель.
  • ./src/ui/Headerреальный путь внутри remote.

Правило: ключи в exposes лучше делать стабильными, а внутренние пути можно менять без влияния на host.

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

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

// webpack.config.js (host)
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      remotes: {
        shop: 'shop@https://remote-app.com/assets/remoteEntry.js',
      },
    }),
  ],
};

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

import Header from 'shop/Header';

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

<details> <summary>

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

</summary>

  1. Cannot read properties of undefined (reading 'get')

    • host нашёл remoteEntry, но объект контейнера не зарегистрировался: проверьте совпадение name и алиаса в remotes.
  2. 404 на чанки после успешной загрузки remoteEntry

    • проблема в publicPath у remote (особенно при деплое под под-путь).
  3. Импорт shop/Header не находится

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

</details>

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

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

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

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

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

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

Визуально:

До shared:
  Host: react@18 + Remote: react@18 (два инстанса)
После shared:
  Host/Remote -> общий react@18 (один инстанс)

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

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

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

  • react, react-dom
  • библиотеки со своим глобальным состоянием/регистрами
  • дизайн-системы, которые ожидают единые контексты/темы

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

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

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

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

Практическое правило:

  1. Для критичных singleton-зависимостей задавайте requiredVersion.
  2. Включайте strictVersion, когда несовместимость опаснее, чем отказ загрузки.

Ключевые флаги shared (шпаргалка)

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

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

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

<details> <summary>

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

</summary>

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

</details>

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

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

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

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

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

1) Host → грузит remoteEntry.js (контейнер)
2) Container → грузит чанки конкретного exposed-модуля (по publicPath)

Поэтому «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

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

  1. Кэширование remoteEntry

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

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

    • Remote часто лежит на другом домене/CDN.
    • Нужны корректные CORS-заголовки для загрузки скриптов и политика CSP, разрешающая источник remote.
  4. Разные окружения (dev/stage/prod)

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

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

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

<details> <summary>

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

</summary>

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

</details>