В модульной экосистеме JavaScript под singleton-зависимостью понимается экземпляр модуля, который должен существовать в приложении в единственном числе во время выполнения. В условиях классического bundling-подхода это не гарантируется автоматически: один и тот же пакет может быть включён в сборку несколько раз, если он встречается в разных графах зависимостей или если разные части системы используют разные версии.
В Webpack это становится особенно критично при переходе к микрофронтендам и динамической загрузке чанков, где границы сборок перестают совпадать с границами рантайма.
Ключевая проблема singleton-зависимостей заключается в том, что JavaScript-модули в разных бандлах перестают быть идентичными ссылочно. Даже если код одинаковый, разные инстансы модуля приводят к разным состояниям, разным контекстам и несовместимости внутренних кэшей.
Дублирование зависимостей в Webpack возникает не случайно, а как следствие особенностей резолва модулей:
Даже в монолитной сборке возможно появление двух экземпляров одной библиотеки, если Webpack не может доказать их идентичность на этапе dependency graph optimization.
Особенно критичны библиотеки, содержащие глобальное состояние:
Webpack 5 выполняет анализ зависимостей и частичную дедупликацию, однако не может гарантировать singleton-семантику на уровне рантайма.
При сборке происходит следующее:
В результате:
Singleton-зависимость в Webpack не является автоматически обеспечиваемым свойством. Это контракт между разработчиками разных частей системы.
Он предполагает:
В противном случае даже простое сравнение instanceof
может начать возвращать ложные результаты, поскольку классы будут
загружены из разных контекстов.
В Webpack 5 проблема singleton-зависимостей решается через Module Federation. Основной механизм — shared dependencies.
Конфигурация shared модуля позволяет определить, что конкретная библиотека должна быть разделена между host и remote.
Пример:
new ModuleFederationPlugin({
name: "app",
remotes: {
remoteApp: "remoteApp@http://localhost:3001/remoteEntry.js"
},
shared: {
react: {
singleton: true,
requiredVersion: "^18.0.0"
},
"react-dom": {
singleton: true,
requiredVersion: "^18.0.0"
}
}
});
Здесь ключевое значение имеет параметр singleton: true,
который указывает, что Webpack должен обеспечить единственный экземпляр
зависимости в рамках share scope.
Share scope представляет собой рантайм-реестр модулей, доступных для совместного использования между контейнерами.
Процесс выглядит следующим образом:
Таким образом, share scope становится механизмом координации singleton-экземпляров между независимыми сборками.
Версионирование играет критическую роль, поскольку singleton не означает «любая версия подходит». Несовместимые версии могут нарушить контракт API.
В Webpack используются несколько стратегий контроля версий:
Параметр requiredVersion задаёт допустимый диапазон
версии:
shared: {
lodash: {
singleton: true,
requiredVersion: "^4.17.0"
}
}
Webpack проверяет соответствие версии и принимает решение:
Параметр strictVersion усиливает контроль:
shared: {
react: {
singleton: true,
strictVersion: true,
requiredVersion: "18.2.0"
}
}
При strictVersion: true несовпадение версии приводит к
ошибке загрузки или отказу от использования shared модуля.
SemVer в singleton-контексте работает не как рекомендация, а как механизм совместимости runtime.
Основные риски:
Особенно опасны ситуации, когда:
Даже небольшие различия приводят к нарушению hook-цепочек и контекста.
Конфликты возникают при невозможности согласовать версию модуля между несколькими участниками share scope.
Типовые сценарии:
Если один remote требует ^17.0.0, а другой
^18.0.0, Webpack не может выбрать единую версию.
Если один модуль объявляет зависимость как singleton, а другой нет, возникает дублирование экземпляров.
Monorepo и независимые сборки могут по-разному интерпретировать зависимости, особенно при hoisting.
Физическая структура зависимостей в node_modules напрямую влияет на Webpack resolution.
Особенности:
Эти различия приводят к тому, что Webpack может видеть один и тот же пакет как:
В результате singleton-логика может работать нестабильно без явного контроля через Module Federation.
Для предсказуемого поведения применяются следующие подходы:
Все микрофронтенды используют одинаковые версии критических библиотек.
Host управляет основными singleton-зависимостями, remote только подключаются.
Каждая критическая зависимость помечается
singleton: true, исключая случайное дублирование.
Peer dependencies фиксируют ожидание внешнего предоставления зависимости:
{
"peerDependencies": {
"react": "^18.0.0"
}
}
Это переносит ответственность за версию на верхний уровень системы.
Ошибки singleton-зависимостей часто проявляются не сразу, а на уровне runtime:
Такие ошибки сложно диагностировать, поскольку код внешне остаётся корректным, а проблема возникает только при наличии двух инстансов одного модуля.
Webpack использует resolution strategy, включающую:
Однако ни один из этих механизмов не гарантирует singleton без участия Module Federation. Resolver работает на этапе сборки, тогда как singleton требует контроля на уровне runtime.
После загрузки бандла Webpack формирует module cache. Singleton-семантика должна быть согласована между несколькими независимыми cache.
Module Federation решает это через общий share scope, который действует как внешний registry поверх локальных кешей.
Без него:
Singleton-зависимость в Webpack формируется как комбинация трёх слоёв:
Версионирование при этом выступает как фильтр совместимости, определяющий возможность совместного использования одного экземпляра модуля.
Стабильная архитектура достигается только при строгом контроле всех трёх уровней одновременно, поскольку любой разрыв в цепочке приводит к появлению дублирующих инстансов и нарушению контрактов выполнения.