В экосистеме Webpack одним из ключевых инструментов реализации микрофронтендов является Module Federation. В этой архитектуре отдельные приложения могут выступать как host (контейнер) и remote (удалённый модуль), обмениваясь кодом во время выполнения.
Параметр exposes отвечает за публикацию внутренних
модулей приложения-remote наружу, делая их доступными для загрузки
другими сборками. Это точка, в которой локальная структура проекта
превращается в публичный контракт.
exposes определяет набор модулей, которые будут
экспортированы из текущего бандла. Эти модули становятся доступными по
сетевому имени remote-приложения.
Фактически формируется словарь:
Этот слой абстракции позволяет:
В конфигурации Module Federation параметр exposes
задаётся внутри ModuleFederationPlugin.
new ModuleFederationPlugin({
name: "app_remote",
filename: "remoteEntry.js",
exposes: {
"./Button": "./src/ui/Button",
"./utils": "./src/shared/utils"
}
});
Каждая запись означает:
./Button — публичный путь, по которому модуль будет
запрошен./src/ui/Button — реальный файл внутри
remote-приложенияПри этом host-приложение обращается к модулю через строку:
import("app_remote/Button");
exposes фактически превращается в контракт между
приложениями. В отличие от обычных npm-пакетов, публикация происходит во
время выполнения, а не на этапе сборки.
Основные характеристики такого API:
Наиболее распространённый вариант — экспорт компонентов UI или утилит.
exposes: {
"./Header": "./src/components/Header",
"./formatDate": "./src/utils/formatDate"
}
При росте приложения применяется доменная структура:
exposes: {
"./auth/LoginForm": "./src/auth/LoginForm",
"./auth/useAuth": "./src/auth/useAuth",
"./cart/CartWidget": "./src/cart/CartWidget"
}
Такой подход формирует логическую сегментацию API remote-приложения.
Часто Module Federation используется для UI-микрофронтендов.
Пример экспорта компонента:
// src/components/ProfileCard.jsx
export default function ProfileCard({ user }) {
return (
<div>
{user.name}
</div>
);
}
exposes: {
"./ProfileCard": "./src/components/ProfileCard"
}
Импорт на стороне host:
import ProfileCard from "app_remote/ProfileCard";
Module Federation работает с ES Modules, но поведение зависит от сборки.
Если используется named export:
// utils.js
export const formatDate = (date) => {
return date.toISOString();
};
Экспорт через exposes:
exposes: {
"./utils": "./src/utils"
}
Использование:
import("app_remote/utils").then((module) => {
module.formatDate(new Date());
});
Важный момент: весь модуль экспортируется как единый namespace-объект.
Любой модуль, объявленный через exposes, загружается
лениво:
Пример:
const RemoteButton = React.lazy(() =>
import("app_remote/Button")
);
Это позволяет снижать initial bundle size host-приложения.
При сборке remote-приложения создаётся файл:
remoteEntry.js
Он содержит:
Host-приложение:
Публичное имя модуля ("./Button") не обязано совпадать с
файловой структурой.
Практика:
exposes: {
"./PrimaryButton": "./src/components/ui/buttons/Primary"
}
Это позволяет:
При использовании exposes возникает вопрос версионности:
Пример:
remotes: {
app_remote: "app_remote@https://cdn.site.com/remoteEntry.js"
}
Обновление remote не требует пересборки host, если контракт exposes сохранён.
Remote содержит только дизайн-систему:
exposes: {
"./Button": "./src/ui/Button",
"./Modal": "./src/ui/Modal"
}
exposes: {
"./checkout": "./src/features/checkout",
"./billing": "./src/features/billing"
}
UI + логика в одном remote:
exposes: {
"./components/Cart": "./src/components/Cart",
"./hooks/useCart": "./src/hooks/useCart"
}
Избыточное дробление:
exposes: {
"./a": "./src/a",
"./b": "./src/b",
"./c": "./src/c"
}
Проблемы:
Экспонирование внутренних утилит без стабильного API приводит к:
Плоская структура exposes затрудняет масштабирование.
Exposes становится публичной поверхностью приложения, поэтому:
Хотя exposes отвечает за публикацию модулей, он тесно
связан с shared:
Несогласованность этих частей приводит к:
Любое изменение структуры:
Критическим является именно публичный ключ, а не путь к файлу.
В зрелых системах exposes рассматривается как версионируемый API:
Подход аналогичен REST API контрактам, но на уровне JavaScript runtime.
Для типизации используется декларация модулей:
declare module "app_remote/Button" {
const Button: React.ComponentType<any>;
export default Button;
}
Это позволяет: