Привет, Хабр! Меня зовут Александр, я руковожу веб-разработкой в InfoWatch. Мы занимаемся проектированием решений, которые обеспечивают информационную безопасность для разных компаний.
В этой статье я расскажу, как мы обеспечиваем интеграцию SPA-приложений. Главная цель этого процесса — сохранить стабильность и совместимость приложений, при этом не потеряв по пути скорость поставки функционала. Под катом — о подходах и решениях, которые мы используем, а также пара примеров из практики и немного рекомендаций, если вы по работе занимаетесь подобными вещами.
С чего всё началось
Однажды перед нами встала задача — спроектировать такую архитектуру, которая позволит нашим командам независимо и самостоятельно развивать свои веб-приложения. Но при этом все их нужно было интегрировать в единый UI. В то время про микрофронтенды говорили не так активно (да вообще почти не говорили), так что каких-то устойчивых паттернов на эту тему практически не было.
Так что мы написали своё решение — платформу, базовое приложение, которое несёт в себе общий функционал, необходимый всем нашим продуктам. Продукт в этой схеме — независимый модуль или приложение, которые встраиваются в общую систему, расширяя возможности платформы или принося в неё свою уникальную логику. С точки зрения бизнеса идея выглядит так: клиент докупает необходимые продукты, система расширяется в рантайме. Поэтому важно, чтобы продуктовые команды могли выполнять обновления, установку и иные манипуляции со своими приложениями, причём делать это независимо от общей системы. Мы же со своей стороны должны гарантировать, что подключение таких приложений не нарушит работу существующих продуктов.
Вот как выглядит платформа в базовом решении.

Несколько основных разделов, главный — дашборд с основными виджетами. Когда клиент докупает продукты, система расширяется, а на дашборде появляются соответствующие новые виджеты, готовые к работе.
Первая реализация была вот такой: мы взяли Angular и договорились с другими командами, что используем именно его. Платформу же поделили на два главных модуля — platform.js с основным core и vendor.js с Angular и прочими нашими внешними библиотеками. Bootstrap-модуль в рантайме загружал vendor.js, затем platform.js, а затем уже бандлы продуктов (всё это — с помощью SystemJS). Мы всё это компилировали, строили из полученных сервисов единый инжектор, мерджили роутеры и отпускаем наше веб-приложение. Де-факто получался такой своеобразный разрезанный монолит, для того времени — неплохая заявка на микрофронтенд. К слову, подобные решения иногда можно встретить в разных компаниях и сейчас, в 2026-м.

Работало всё неплохо, команды на самом деле были такими отдельными островками с собственными релизами, сроками и процессами. Клиент получал все обновления быстро, скорость релизов возрастала, да и вообще всё было неплохо.
Но был нюанс. Стабильность наших приложений обеспечивалась ровно до того момента, пока мы все внутри использовали единый стек, пока совпадали версии Angular. Стоило кому-то уйти чуток вперёд и обновиться раньше других — всё, всё разваливалось.
Мы получили довольно болезненный опыт синхронных релизов. Было это вот так — мы как команда платформы время от времени ходили к продуктам и говорили им — а давайте-ка выполним обновление синхронной библиотеки. И получали отказ — мол, у команд свои сроки и свои релизы, и прямо сейчас они не готовы за это браться. Тем более, это не самая тривиальная задача, ведь это и портирование существующего кода, обновление субзависимостей, TypeScript-а, могут отваливаться линтеры и прочее, прочее, прочее. Итого были моменты, когда глобально все уходили не пилить фичи, а именно обновлять библиотеки. В процессе возрастала и нагрузка на тестирование, ведь это был полноценный регресс.
А клиент в это время сидит и терпеливо ждёт, пока ему поставят фичу. А фичу всё не поставляют, клиент ждёт обновления системы.
Шло время, микрофронтенды развивались, у нас появлялось больше команд, больше решений, и мы поняли, что синхронные релизы — это штука, от которой надо отказываться, после чего начали искать решение.

Single-spa — фреймворк, который предлагает написать своё шелл-приложение, выступающее в роли оркестратора. Внутри всё тот же systemsJS, который и будет загружать микрофронтенды. С точки зрения интеграции приложений инструмент очень гибкий, но ряд механик (например, саму оркестрацию и способ расшаривания библиотек) вам придётся описывать самостоятельно. У разных команд в итоге такие решения будут отличаться, а онбординг новых сотрудников будет занимать дополнительное время.
Модульная федерация — всё нужное есть из коробки. И асинхронная загрузка модулей, и способы расшаривания библиотек. В каком-то смысле модульную федерацию в классическом представлении можно назвать пакетом сборки — небольшой конфиг, собираем контейнер, готово.
Айфреймы — вечная классика, о которой не стоит забывать. Минимальный порог входа, просто на одной страничке собираем в мейнфрейме приложение и получаем серьёзную изоляцию, так что вопросы стабильности сами по себе отпадают. Но никуда не уходит один минус — довольно тяжёлый UX, да и механизмы общения между айфреймами достаточно ограничены, у нас window.postMessage или URL как источник истины.
Если вы сейчас на распутье и подумываете о том, чтобы интегрировать несколько spa-приложений, сначала ответьте себе на ряд вопросов.
-
У ваших приложений есть единое состояние и прямая навигация? Если нет, то, возможно, микрофронтенды вам и не нужны. А вот старый добрый подход в плане взять и разнести приложения по разным серверам и поддоменам всё ещё работает. Как по мне, микрофронтенды тут больше интересны бизнесу — когда мы говорим про распределённость масштабируемости команд с точки зрения разработки. Вопрос, конечно, дискуссионный, но в этом случае можно особо ничего и не выиграть. Если ваше приложение — legacy и надо здесь и сейчас создать временный прототип, то айфреймы будут хорошим решением. Но помните, что цена вопросов, которые придётся решать в будущем, может быть существенно выше, чем было бы сразу при выборе более современного подхода к архитектуре.
-
Если у вас есть возможность навязать всем единый стек и синхронные релизы — мне кажется, что модульная федерация будет лучшим выбором. Во-первых, про модульную федерацию не рассказывал только ленивый, и уже есть огромное количество паттернов. Из коробки всё есть, не приходится ломать голову над чем-о ещё. К слову, хоть модульную федерацию изначально и позиционировали для работы с одним приложением, разбитым на модули, практика показывает, что можно без проблем работать и в контексте нескольких spa.
-
Вам нужно поддержать мультиверсию или мультифреймворк? Тогда ваш кейс похож на наш, и в целом даже не так важно, какой инструмент вы выберете — и айфрейм, и singleSPA, всё сработает. Бывают даже комбинированные подходы, когда берут singleSPA и загружают микрофронтенд, построенный на федерации. Только помните — большая вариативность приведёт к тому, что на странице будут работать несколько SPA-приложений (читай — несколько рантаймов), так что вопрос стабильности и совместимости снова будет актуален.

Модульная федерация
Модульная федерация — это плагин для сборки наших приложений, мы собираем их в специальный контейнер. В контексте статьи контейнер — наше приложение (микрофронтенд), с той лишь небольшой разницей, что у нас есть небольшой JS-обвяз, те самые рантайм-функции и механизмы, которые помогают асинхронно загружать модули, инициализировать контейнер, как выполнять код в нём и прочее.
Изначально модульная федерация была ключевой особенностью Webpack, но сейчас есть и другие платформы сборки, находятся даже энтузиасты, которые пишут собственные решения, в общем — на свой вкус и цвет вы точно найдёте платформу.

Mf-manifest.json — классика в истории микрофронтендов, специальный метафайл с информацией о контейнере (имя, точка входа, exposed-пути и другое). Грубо говоря, это данные обо всех частях контейнера, которые смотрят наружу и которые можно переиспользовать, а также мапа с указанием имён и версий библиотек зависимостей, необходимых для работы контейнера. Благодаря этому мы можем организовать shared scope, то есть пошарить наши библиотеки: когда мы загружаем какой-либо контейнер, в глобальной области у нас есть object scope, куда помещаются эти зависимости. И когда мы загрузим ещё один такой контейнер по манифесту, то сможем посмотреть — ага, нам нужна библиотека версии 2.0, такая уже есть в скопе, я не буду тянуть свою версию, поэтому переиспользую существующую.
Наша интеграция и проблемы совместимости
Что именно мы имеем в виду под фразой «Надо интегрировать несколько приложений»? Мы берём компоненты одного приложения (или вообще само приложение) и пытаемся встроить его в другое. Если мы в процессе всех этих действий находимся в одном рантайме (читай — в одном стеке), то всё здорово и никаких проблем нет, просто берём компонент и вставляем, куда надо.

Но мы поддерживаем мультиверсию, поэтому каждое приложение де-факто создаёт свою закрытую систему = свой инжектор, свой контекст. И в таком случае просто взять и вынуть из одной системы компонент, чтобы вставить его в другую, становится нереально. Больше скажу, минорные обновления или патчи наших библиотек несут за собой изменения приватного API, и могут получаться ситуации, в которых мы берём чей-то компонент, который ожидаем конкретный функционал — а предоставить его мы не можем.
Это проблема, и её надо решать. Один из способов — стабильный внешний контракт, который как раз и уберёт все эти сложности и изолирует наши фреймворки. В роли такого контракта у нас выступает веб-компонент, стандартный API для браузера, предлагающий нам создавать свои собственные HTML-теги. Получается, что для внешнего мира это обычный DOM-узел, а вот внутри мы вольны инкапсулировать эту реализацию. Например, это может быть Angular, Vue, React — не так важно, это чистый JS и обычный веб-компонент, но для остальных — это DOM-элемент. Так мы получаем возможность работать с Shadow DOM и настраивать стили так, чтобы они на странице не конфликтовали друг с другом. А ещё события жизненного цикла (mount / unmount), их легко слушать и это полезно, когда компонент вставляется в наше DOM-дерево или удаляется из него.

При работе с веб-компонентом как с обычным HTML-тегом мы можем использовать те же свойства — обращаясь к его атрибутам или свойствам, можем передавать различные входные параметры. Сам веб-компонент в свою очередь создаёт кастомные события, которые мы снаружи слушаем, подписываемся и организуем исходящие события.

Совет — удобно чисто для себя собрать специальный вспомогательный компонент-контейнер, который и будет выполнять эту задачу. Он просто будет получать на вход конфигурацию (нас интересуют тег для нашего веб-компонента и наш remote, контейнер, который его предоставит).
@Input() public config: { tag: "iwc-${string}'; remote: Project; }@Input() public elementInputs: { [prop: string]: unknown };@Input() public elementOutputs: { [event: string]: (event: Event) => void };async ngAfterContentInit() {try {await loadRemote(config.remote);this.element = document.createElement(config.tag);this.setupInputs();this.setupEvents();this.container.nativeElement.appendChild(this.element);}
Так у нас получится на старте при инициализации компонента вызывать функцию load remote, она доступна из коробки модульной федерации и нужна для динамической загрузки контейнера.
Вот что происходит:
-
мы загружаем наш контейнер;
-
он инициализируется внутри как обычное веб-приложение (его точка входа — это main-функция любого приложения на React / Angular);
-
дефайнит нам веб-элементы для системы;
-
и на основе полученного тега мы можем создать наш DOM-узел, всё засетапить, закинуть в него входные данные, повесить обработчики на входящие события и вставить такой веб-компонент в наш DOM.
Во второй части статьи:
-
как без проблем шарить библиотеки
-
Lazy load и его польза
-
изоляция библиотек
-
советы и рекомендации
Александр Посонский
Руководитель веб-разработки InfoWatch
ссылка на оригинал статьи https://habr.com/ru/articles/1053506/