remotes: подключение удалённых модулей

Архитектура Module Federation в Webpack основана на разделении приложения на независимые сборки, способные загружать код друг друга во время выполнения. В этой модели термин remote обозначает приложение или сборку, которая экспортирует модули для использования другими приложениями (host).

Remote-сборка публикует специальный файл remoteEntry.js, содержащий метаданные, реестр модулей и логику загрузки. Host-приложение подключает этот файл и получает доступ к экспортируемым сущностям так, будто они являются частью локального кода.


Базовая структура remote-сборки

Remote-приложение конфигурируется через ModuleFederationPlugin, где определяется имя контейнера и список экспортируемых модулей.

new ModuleFederationPlugin({
  name: 'app1',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/components/Button',
    './utils': './src/utils/index'
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true }
  }
});

Ключевые элементы конфигурации

name

  • Уникальное имя remote-контейнера
  • Используется host-приложением как идентификатор

filename

  • Имя файла манифеста federation
  • Обычно remoteEntry.js
  • Загружается динамически во время выполнения

exposes

  • Карта экспортируемых модулей
  • Ключ — публичный путь
  • Значение — локальный путь к модулю

shared

  • Общие зависимости между host и remote
  • Используется для предотвращения дублирования библиотек

Подключение remote в host-приложении

Host-приложение объявляет удалённые источники через секцию remotes.

new ModuleFederationPlugin({
  name: 'host',
  remotes: {
    app1: 'app1@http://localhost:3001/remoteEntry.js'
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true }
  }
});

Формат строки remote

<name>@<url>
  • name — идентификатор remote-контейнера
  • url — путь до remoteEntry.js

Механизм загрузки remoteEntry

При обращении к remote-модулю Webpack выполняет несколько шагов:

  1. Загружает remoteEntry.js через <script> или dynamic import
  2. Инициализирует контейнер с указанным именем
  3. Регистрирует фабрики модулей
  4. Возвращает экспорт требуемого модуля

Пример импорта:

import('app1/Button').then((module) => {
  const Button = module.default;
});

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

Статическая конфигурация подходит не всегда. В реальных системах часто требуется динамическая подгрузка remote URL.

remotes: {
  app1: `app1@${process.env.APP1_URL}/remoteEntry.js`
}

Более гибкий вариант — использование promise-based remotes:

remotes: {
  app1: `promise new Promise((resolve) => {
    const url = window.RUNTIME_CONFIG.app1Url;
    const script = document.createElement('script');

    script.src = url;
    script.onl oad = () => {
      resolve(window.app1);
    };

    document.head.appendChild(script);
  })`
}

Особенности динамической загрузки

  • Позволяет менять remote без пересборки host
  • Требует runtime-логики загрузки
  • Часто используется в микрофронтендах

Инициализация контейнера remote

После загрузки remoteEntry.js контейнер должен быть инициализирован:

await __webpack_init_sharing__('default');
await container.init(__webpack_share_scopes__.default);

Где:

  • __webpack_init_sharing__ — инициализация shared scope
  • __webpack_share_scopes__ — глобальный реестр зависимостей

Shared зависимости и их влияние на remote

Remote и host могут использовать одинаковые библиотеки. Чтобы избежать конфликтов:

shared: {
  react: {
    singleton: true,
    requiredVersion: '^18.0.0'
  }
}

Поведение shared пакетов

  • singleton: true — только один экземпляр библиотеки
  • requiredVersion — контроль совместимости версий
  • eager: true — немедленная загрузка

Неправильная настройка shared приводит к:

  • дублированию React-контекста
  • ошибкам hooks
  • несовместимости состояния

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

Remote-система не требует строгой версии API, но поддерживает стратегию:

Версионирование через URL

app1@https://cdn.example.com/app1/v1/remoteEntry.js
app1@https://cdn.example.com/app1/v2/remoteEntry.js

Версионирование через exposes

exposes: {
  './ButtonV1': './v1/Button',
  './ButtonV2': './v2/Button'
}

Семантика совместимости

  • backward-compatible изменения не требуют нового remote
  • breaking changes требуют нового endpoint

Ошибки загрузки remote

Remote может быть недоступен, и это требует обработки:

import('app1/Button')
  .then((m) => m.default)
  .catch(() => {
    return FallbackButton;
  });

Основные причины ошибок

  • недоступность CDN
  • CORS ограничения
  • неверный URL remoteEntry
  • несовместимая версия Webpack runtime

CORS и remoteEntry

Так как remoteEntry загружается как внешний скрипт, сервер обязан отдавать корректные заголовки:

Access-Control-Allow-Origin: *

Без этого браузер блокирует загрузку контейнера.


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

RemoteEntry часто кэшируется агрессивно через CDN:

Cache-Control: max-age=31536000, immutable

Проблема возникает при обновлении API, поэтому используют:

  • хеширование имени файла (remoteEntry.[hash].js)
  • версионированные URL
  • runtime-redirect через manifest

Ленивая загрузка remote-модулей

Remote подключается только при необходимости:

const loadWidget = async () => {
  const module = await import('app1/Widget');
  return module.default;
};

Это снижает:

  • initial bundle size
  • время первого рендера
  • нагрузку на сеть

Typings для remote-модулей

TypeScript не знает структуру remote, поэтому требуется декларация:

declare module 'app1/Button' {
  const Button: React.ComponentType<any>;
  export default Button;
}

Для крупных систем создаются отдельные d.ts пакеты, синхронизируемые с exposes.


Контракт между host и remote

Remote становится частью распределённой системы, где важен контракт:

  • стабильные пути exposes
  • предсказуемые типы API
  • согласованные shared зависимости
  • единый runtime Webpack federation

Нарушение контракта приводит к runtime-ошибкам, которые не выявляются на этапе сборки.


Remote как независимый деплой

Remote-сборка обычно разворачивается отдельно:

  • отдельный CI/CD pipeline
  • независимая публикация
  • возможность hot deployment

Host не пересобирается при изменении remote при условии стабильного интерфейса.


Множественные remote-источники

Host может подключать несколько remotes:

remotes: {
  app1: 'app1@https://cdn/a/remoteEntry.js',
  app2: 'app2@https://cdn/b/remoteEntry.js',
  uiKit: 'uiKit@https://cdn/ui/remoteEntry.js'
}

Это позволяет строить композицию микрофронтендов, где каждая часть системы изолирована и независимо обновляется.


Ограничения remote-модели

  • зависимость от runtime загрузчика Webpack
  • необходимость строгого контроля версий shared
  • увеличение сложности дебага
  • сетевые задержки при первом доступе
  • риск runtime-несовместимости модулей