Host и Remote: роли приложений

Базовая модель взаимодействия

В архитектуре Module Federation каждое приложение получает одну из двух ключевых ролей: Host или Remote. Эти роли определяют, кто потребляет код, а кто его предоставляет, но в реальных системах граница между ними часто размывается, поскольку одно приложение может одновременно выступать в обеих ролях.

Host-приложение — это контейнер, который загружает и использует удалённые модули из других сборок. Remote-приложение — это сборка, которая экспортирует свои модули для использования внешними приложениями.

Ключевая идея заключается в том, что зависимости между приложениями становятся динамическими и исполняемыми в рантайме, а не фиксируются на этапе сборки.


Роль Remote: предоставление модулей

Remote-приложение в Module Federation — это поставщик функциональности. Оно определяет набор модулей, которые могут быть импортированы извне.

В конфигурации Webpack Remote описывается через ModuleFederationPlugin, где указывается имя приложения и список экспонируемых модулей:

new ModuleFederationPlugin({
  name: 'auth_app',
  filename: 'remoteEntry.js',
  exposes: {
    './LoginForm': './src/LoginForm',
    './AuthService': './src/AuthService'
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true }
  }
});

Ключевые характеристики Remote:

1. Экспорт модулей через exposes Каждый модуль явно объявляется как доступный извне. Это предотвращает случайное раскрытие внутренней структуры приложения.

2. Генерация remoteEntry файла Файл remoteEntry.js является точкой входа, через которую Host получает доступ к модулям Remote-приложения.

3. Изоляция внутренней структуры Remote контролирует, какие части системы доступны. Внутренние зависимости и архитектура остаются скрытыми.

4. Управление зависимостями через shared Remote может объявлять общие зависимости, чтобы избежать дублирования библиотек.


Роль Host: потребление удалённых модулей

Host-приложение является потребителем функциональности. Оно не знает заранее, где физически находится код, но знает контракт доступа к нему.

Пример конфигурации Host:

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

Основные особенности Host:

1. Динамическая загрузка модулей Host получает код Remote во время выполнения через загрузку remoteEntry.js.

2. Абстракция источника кода Host работает с модулями как с локальными, хотя они физически находятся в другой сборке.

3. Управление несколькими Remote Одно Host-приложение может подключать множество Remote одновременно, формируя распределённую систему.

const LoginForm = React.lazy(() => import('auth_app/LoginForm'));

4. Ленивое подключение функциональности Модули могут загружаться только при необходимости, снижая стартовый вес приложения.


Взаимодействие Host и Remote

Связь между Host и Remote строится через три слоя:

1. Runtime-слой загрузки

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

2. Контейнер модулей

Каждое приложение в федерации имеет контейнер, который хранит фабрики модулей. Host обращается к этим фабрикам для получения кода.

3. Общие зависимости

Если Host и Remote используют одну и ту же библиотеку (например React), Webpack пытается синхронизировать версии через механизм shared.


Конфигурация shared и влияние на роли

Хотя роли Host и Remote логически разделены, поведение сильно зависит от конфигурации shared-зависимостей.

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

Singleton-режим гарантирует, что в рантайме будет использован только один экземпляр библиотеки. Это особенно важно для React, где наличие двух копий приводит к ошибкам контекста.


Гибридные сценарии: Host как Remote

Приложение может одновременно:

  • импортировать модули других приложений (Host)
  • экспортировать собственные модули (Remote)

Пример гибридной конфигурации:

new ModuleFederationPlugin({
  name: 'profile_app',
  filename: 'remoteEntry.js',
  remotes: {
    auth_app: 'auth_app@http://localhost:3001/remoteEntry.js'
  },
  exposes: {
    './ProfilePage': './src/ProfilePage'
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true }
  }
});

Такой подход позволяет строить многоуровневые системы, где один Remote становится зависимостью другого Host.


Цепочки федерации

В сложных системах формируются цепочки:

  • Shell (Host верхнего уровня)
  • Feature App (одновременно Host и Remote)
  • Micro Frontend (Remote)

Каждый уровень может:

  • потреблять модули сверху
  • экспортировать функциональность вниз

Это создаёт иерархию, в которой приложение становится узлом распределённой графовой структуры.


Динамическая подмена Remote

Одним из ключевых преимуществ архитектуры является возможность менять Remote без пересборки Host.

remotes: {
  auth_app: 'auth_app@https://cdn.example.com/remoteEntry.js'
}

Host не зависит от конкретного окружения. Достаточно изменить URL, чтобы переключить реализацию.


Ошибки и проблемы в связке Host–Remote

Несовпадение версий зависимостей

Если Host и Remote используют разные версии библиотек без корректного shared-конфига, возникает дублирование и конфликты состояния.

Недоступность remoteEntry

Если remoteEntry.js недоступен, Host теряет возможность загрузки модулей. В таких случаях требуется fallback-логика.

Нарушение контрактов модулей

Если Remote изменяет экспорт без обратной совместимости, Host может получить runtime-ошибки.


Контрактность взаимодействия

Несмотря на динамичность, взаимодействие Host и Remote требует строгого контрактного подхода.

Контракт включает:

  • имена экспортируемых модулей
  • ожидаемые props и API компонентов
  • версии shared-зависимостей
  • соглашения о структуре данных

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


Масштабирование архитектуры через роли

Разделение на Host и Remote позволяет:

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

При этом архитектурная сложность переносится с уровня сборки на уровень исполнения, где управление становится более гибким, но требует дисциплины в проектировании контрактов и зависимостей.