Настройка ModuleFederationPlugin

Module Federation Plugin — механизм, позволяющий разделять код между независимыми приложениями во время выполнения, а не сборки. Это основа архитектуры микрофронтендов, где отдельные приложения могут динамически загружать модули друг друга, оставаясь автономными по сборке и деплою.


Базовая идея Module Federation

Module Federation решает задачу разделения приложения на независимые части, которые:

  • собираются отдельно
  • деплоятся независимо
  • могут использовать общий код без дублирования
  • загружают зависимости во время выполнения

Ключевые сущности:

  • Host (Shell) — приложение, которое потребляет удалённые модули
  • Remote — приложение, которое экспортирует модули
  • Shared — общие зависимости (например, React, Lodash)

Базовая конфигурация ModuleFederationPlugin

Минимальная настройка выполняется через webpack.config.js:

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app_shell",
      remotes: {
        app_remote: "app_remote@http://localhost:3001/remoteEntry.js",
      },
      shared: {},
    }),
  ],
};

Здесь:

  • name — имя текущего контейнера
  • remotes — подключаемые удалённые приложения
  • remoteEntry.js — точка входа федерации

Конфигурация Remote-приложения

Remote-приложение экспортирует модули:

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app_remote",
      filename: "remoteEntry.js",
      exposes: {
        "./Button": "./src/components/Button",
        "./utils": "./src/utils/index",
      },
      shared: {},
    }),
  ],
};

Основные поля:

  • name — имя remote-контейнера
  • filename — файл манифеста федерации
  • exposes — публичные модули

Использование remote-модулей

В host-приложении импорт выглядит как динамический:

import("app_remote/Button").then((module) => {
  const Button = module.default;
});

Или в React:

const RemoteButton = React.lazy(() => import("app_remote/Button"));

Shared зависимости

Shared позволяет избежать дублирования библиотек:

shared: {
  react: {
    singleton: true,
    requiredVersion: "^18.0.0",
  },
  "react-dom": {
    singleton: true,
  },
}

Ключевые параметры shared:

  • singleton — гарантирует один экземпляр библиотеки
  • requiredVersion — контроль совместимости версий
  • eager — загрузка на этапе initial bundle
  • strictVersion — жёсткая проверка версии

Singleton и проблемы дублирования

Если react не помечен как singleton, возможны:

  • два React-контекста
  • ошибки хуков
  • некорректная работа state management

Правильная настройка:

shared: {
  react: {
    singleton: true,
    strictVersion: true,
  },
}

Версионирование shared модулей

Module Federation поддерживает согласование версий:

shared: {
  lodash: {
    requiredVersion: "^4.17.0",
  },
}

Если версии несовместимы, Webpack:

  • либо загрузит локальную версию
  • либо выбросит ошибку (при strictVersion)

Динамические remote

Remote можно определять во время выполнения:

__webpack_init_sharing__("default");

const container = window.app_remote;
await container.init(__webpack_share_scopes__.default);

const factory = await container.get("./Button");
const Module = factory();

Это позволяет:

  • менять remote URL без пересборки host
  • подгружать окружения (staging/prod)

Runtime поведение Module Federation

При сборке создаются:

  • remoteEntry.js — манифест модулей
  • chunk-файлы remote-приложения
  • runtime-обвязка Webpack

Процесс загрузки:

  1. Host загружает remoteEntry.js
  2. Инициализирует share scope
  3. Запрашивает модуль через get
  4. Выполняет фабрику модуля

Изоляция и границы контекстов

Каждый remote работает в своём scope:

  • собственный Webpack runtime
  • независимая сборка
  • разделение chunk graph

Важно понимать:

  • это не monorepo runtime
  • это federation контейнеров

Типовые архитектуры

Microfrontend shell

  • shell управляет маршрутизацией
  • remotes — страницы или виджеты

Feature-based federation

  • один remote = одна бизнес-фича
  • например: checkout, profile, dashboard

Shared design system

  • отдельный remote с UI-kit
  • используется всеми приложениями

Lazy loading и performance

Module Federation работает с ленивой загрузкой:

  • уменьшает initial bundle
  • ускоряет first paint
  • переносит загрузку на runtime

Оптимизация:

remotes: {
  app_remote: "app_remote@https://cdn.example.com/remoteEntry.js",
}

Использование CDN снижает latency.


Error handling при загрузке remote

Типичная проблема — недоступный remote:

const loadRemote = async () => {
  try {
    return await import("app_remote/Button");
  } catch (e) {
    return import("./FallbackButton");
  }
};

TypeScript и Module Federation

Для типизации используются декларации:

declare module "app_remote/Button" {
  const Button: React.ComponentType;
  export default Button;
}

Можно также генерировать типы через federation plugins.


Частые ошибки конфигурации

Несовпадение версий React

  • ошибка hooks
  • “Invalid hook call”

Отсутствие remoteEntry.js

  • 404 при загрузке контейнера

Неправильный publicPath

output: {
  publicPath: "auto",
}

Без этого возможны ошибки загрузки chunk-файлов.


Безопасность Module Federation

Риски:

  • выполнение внешнего JS-кода
  • отсутствие sandbox-изоляции

Меры:

  • доверенные CDN
  • подпись артефактов
  • ограничение exposes

Runtime конфигурация через env

Можно управлять remote через переменные:

const remoteUrl = process.env.REMOTE_URL;

remotes: {
  app_remote: `app_remote@${remoteUrl}/remoteEntry.js`,
}

Производственная конфигурация

Рекомендуемые настройки:

new ModuleFederationPlugin({
  name: "shell",
  remotes: {
    remote_app: "remote_app@https://cdn.site.com/remoteEntry.js",
  },
  shared: {
    react: { singleton: true },
    "react-dom": { singleton: true },
  },
});

Интеграция с CI/CD

При использовании federation важно:

  • деплоить remotes независимо
  • версионировать remoteEntry.js
  • избегать breaking changes в exposes

Поведение при обновлении remote

Host всегда получает:

  • актуальную версию remoteEntry
  • новые exposed модули без пересборки

Однако:

  • кеширование CDN может задерживать обновления
  • требуется cache-busting стратегия