Динамические Remote URL

В архитектуре микрофронтендов на базе Webpack и плагина Module Federation Plugin ключевой проблемой становится управление удалёнными контейнерами (remote) в рантайме. Статическая конфигурация remotes в большинстве случаев быстро упирается в ограничения: фиксированные URL, невозможность переключения окружений без пересборки, сложность A/B-развёртывания и динамического подключения версий.

Обычная конфигурация Module Federation предполагает жёстко заданный remote:

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

Такой подход формирует статическую связку host → remote, которая закрепляется на этапе сборки. Любое изменение адреса требует новой сборки host-приложения.

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

Базовый механизм runtime-remote

Webpack позволяет подменять значение remotes через функцию:

remotes: {
  app2: "promise new Promise(resolve => { ... })"
}

Однако более корректный подход — использовать асинхронную загрузку контейнера через динамический URL.

Основная идея заключается в том, что remote становится не строкой, а функцией, возвращающей контейнер:

const loadRemote = (url, scope) => {
  return async () => {
    await __webpack_init_sharing__("default");

    const container = window[scope];
    if (!container) {
      await new Promise((resolve, reject) => {
        const script = document.createElement("script");
        script.src = url;
        script.onl oad = resolve;
        script.oner ror = reject;
        document.head.appendChild(script);
      });
    }

    await container.init(__webpack_share_scopes__.default);
    return container;
  };
};

Динамическое формирование remoteEntry URL

Ключевой элемент динамического подхода — генерация URL во время выполнения приложения:

const getRemoteUrl = (name) => {
  const base = window.__RUNTIME_CONFIG__.CDN_BASE;
  const version = window.__RUNTIME_CONFIG__.VERSION;

  return `${base}/${name}/${version}/remoteEntry.js`;
};

Такой слой абстракции позволяет:

  • переключать CDN без пересборки
  • менять версии модулей на лету
  • управлять маршрутизацией через конфигурацию окружения
  • реализовывать canary-деплой

Подключение remote через promise-based federation

В конфигурации Module Federation:

remotes: {
  app2: `promise new Promise((resolve) => {
    const url = getRemoteUrl("app2");

    const script = document.createElement("script");
    script.src = url;

    script.onl oad = () => {
      resolve(window.app2);
    };

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

Здесь происходит ключевое изменение: remote становится результатом выполнения promise, который загружает внешний контейнер и регистрирует его в глобальном объекте window.

Универсальный loader для remote контейнеров

Более масштабируемая архитектура строится вокруг централизованного загрузчика:

const remoteRegistry = {
  app2: {
    scope: "app2",
    url: () => getRemoteUrl("app2"),
  },
  app3: {
    scope: "app3",
    url: () => getRemoteUrl("app3"),
  }
};

export const loadRemoteModule = async (name, module) => {
  const remote = remoteRegistry[name];

  if (!remote) {
    throw new Error(`Remote ${name} not found`);
  }

  await loadScript(remote.url(), remote.scope);

  const container = window[remote.scope];

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

  const factory = await container.get(module);
  return factory();
};

Динамическая загрузка script и контроль состояния

Отдельного внимания требует управление загрузкой remoteEntry.js:

const loadScript = (src, scope) => {
  return new Promise((resolve, reject) => {
    if (window[scope]) {
      resolve();
      return;
    }

    const existing = document.querySelector(`script[data-scope="${scope}"]`);
    if (existing) {
      existing.addEventListener("load", resolve);
      existing.addEventListener("error", reject);
      return;
    }

    const script = document.createElement("script");
    script.src = src;
    script.dataset.scope = scope;

    script.onl oad = resolve;
    script.oner ror = reject;

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

Это предотвращает повторную загрузку одного и того же remote и снижает риск конфликтов состояния.

Runtime конфигурация окружений

Часто динамические remote URL привязываются к конфигурации окружения:

window.__RUNTIME_CONFIG__ = {
  ENV: "production",
  CDN_BASE: "https://cdn.production.example.com",
  REMOTES: {
    app2: "v3",
    app3: "v5"
  }
};

На основе этого формируется единая точка принятия решений:

const resolveRemoteVersion = (name) => {
  return window.__RUNTIME_CONFIG__.REMOTES[name] || "latest";
};

Такой подход позволяет:

  • обновлять микрофронтенды независимо
  • фиксировать версии для стабильных релизов
  • внедрять rollback через изменение конфигурации

Manifest-driven архитектура

Более зрелая модель использует manifest.json, загружаемый при старте приложения:

{
  "app2": {
    "url": "https://cdn.example.com/app2/remoteEntry.js",
    "scope": "app2",
    "version": "2.1.0"
  }
}

Загрузка manifest:

const fetchManifest = async () => {
  const res = await fetch("/mf-manifest.json");
  return res.json();
};

Далее remote registry строится динамически:

const buildRegistry = (manifest) => {
  return Object.entries(manifest).reduce((acc, [key, value]) => {
    acc[key] = {
      scope: value.scope,
      url: () => value.url
    };
    return acc;
  }, {});
};

Интеграция с sharing scope

При динамической загрузке важно корректно инициализировать общий scope зависимостей:

await __webpack_init_sharing__("default");

const container = window[scope];

await container.init(__webpack_share_scopes__.default);

Ошибки на этом этапе приводят к дублированию React, lodash и других shared-зависимостей, что критично для runtime-стабильности.

Версионирование и cache invalidation

Динамические URL часто включают версию как часть пути:

/app2/2.3.1/remoteEntry.js

Либо через query-параметр:

remoteEntry.js?v=2.3.1

Это позволяет управлять:

  • инвалидацией CDN-кеша
  • безопасным rollout новых версий
  • параллельным существованием нескольких версий remote

Failover и деградация

При динамическом подключении remote важно учитывать отказоустойчивость:

const loadWithFallback = async (urls) => {
  for (const url of urls) {
    try {
      return await loadRemote(url);
    } catch (e) {
      continue;
    }
  }

  throw new Error("All remote sources failed");
};

Такой механизм позволяет переключаться между CDN или регионами доставки без вмешательства в основной билд.

Изоляция и безопасность динамических remote

Динамические URL увеличивают поверхность атаки, поскольку remote-источник становится конфигурируемым в runtime. В таких системах применяются ограничения:

  • whitelist доменов CDN
  • подпись manifest-файлов
  • контроль integrity (SRI-подход на уровне скриптов)
  • запрет произвольных URL из пользовательского ввода
const isAllowed = (url) => {
  return url.startsWith("https://cdn.example.com/");
};

Связь с архитектурой микрофронтендов

В динамических remote URL формируется переход от compile-time интеграции к runtime-композиции. Host-приложение перестаёт быть жёстко связанным с remote-сборками и начинает функционировать как orchestrator, который:

  • определяет доступные модули через конфигурацию
  • загружает их по требованию
  • управляет версиями и fallback-логикой
  • синхронизирует shared зависимости через Module Federation runtime

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