Микрофронтенды и наблюдаемость: Module Federation, Sentry, RUM
Это опциональный урок серии. Он про два вопроса, которые появляются не у каждого проекта, но неизбежно появляются у каждого выросшего проекта.
Первый: что делать, когда над одним интерфейсом работают пять команд и каждый релиз превращается в очередь из чужих веток. Ответ индустрии, микрофронтенды: распилить UI так, чтобы команды деплоились независимо.
Второй: как узнать, что у пользователей всё плохо, раньше, чем об этом напишут в поддержку. Ответ, наблюдаемость фронтенда: ошибки, метрики и сессии реальных людей, собранные в одно место. DevTools показывает твой ноутбук; RUM показывает телефон твоего пользователя в метро.
Темы связаны теснее, чем кажется: микрофронтенды без наблюдаемости рассыпаются первыми, потому что ошибка на стыке двух приложений не видна ни одной из команд.
1. Когда нужны микрофронтенды (и когда нет)
Микрофронтенды это не про технологию, а про организацию. Распил монолитного SPA оправдан, когда совпали три условия:
- Команд больше четырёх-пяти, и они мешают друг другу в одном репозитории: очередь на релиз, конфликты в общих файлах, чужие тесты валят CI.
- Домены реально независимы: каталог, корзина и личный кабинет могут жить отдельными релизными циклами без ежедневной синхронизации.
- Стоимость координации уже выше стоимости инфраструктуры: команда готова платить за общий контракт, реестр версий и интеграционные стенды.
Если команда одна или две, микрофронтенды дают все минусы распределённой системы без единого плюса: дубль зависимостей, зоопарк версий, сложный деплой. Для них достаточно модульного монолита, который мы строили в уроке про архитектуру фронта: жёсткие границы слайсов внутри одного репозитория и одной сборки. Это прямой родственник модульного монолита с бекенда, и мост туда же: границы по командам это линза team topologies из раздела 09.
2. Четыре способа композиции
Все варианты отвечают на один вопрос: как несколько независимых сборок оказываются на одной странице.
iframe: старый, изолированный, неудобный
<iframe src="https://checkout.shop.example/cart" title="Корзина"></iframe>
Полная изоляция стилей, скриптов и сбоев: упавшая корзина не уронит
каталог. Цена: своя прокрутка, дребезг высоты, общение только через
postMessage, дубль всего рантайма в каждом фрейме, боль с фокусом и
доступностью. Подходит для встраивания чужого и недоверенного: виджеты
оплаты, чаты поддержки, админки сторонних сервисов.
single-spa: оркестратор поверх SPA
single-spa делает корневое приложение диспетчером: оно владеет URL и по маршруту монтирует и размонтирует дочерние приложения.
// root-config.ts
import { registerApplication, start } from 'single-spa';
registerApplication({
name: '@shop/catalog',
app: () => System.import('@shop/catalog'),
activeWhen: ['/catalog'],
});
registerApplication({
name: '@shop/checkout',
app: () => System.import('@shop/checkout'),
activeWhen: ['/cart', '/checkout'],
});
start();
Каждое дочернее приложение экспортирует три функции жизненного цикла:
bootstrap, mount, unmount. Это и есть контракт. Внутри может быть
React, Vue, что угодно: оркестратору всё равно. Слабое место то же, что
и сила: рантаймы фреймворков легко задублировать, и страница возит два
React и один Vue одновременно.
Module Federation: федерация на уровне бандлера
Module Federation мы уже упоминали в уроке про бандлеры. Это самый популярный сегодня способ: модуль чужого приложения импортируется почти как обычный, а бандлер берёт на себя загрузку и разруливание общих зависимостей.
Remote объявляет, что он отдаёт наружу:
// rspack.config.ts приложения search (remote)
new ModuleFederationPlugin({
name: 'search',
filename: 'remoteEntry.js',
exposes: {
'./SearchPage': './src/pages/SearchPage',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
},
});
Host объявляет, откуда брать:
// rspack.config.ts приложения shell (host)
new ModuleFederationPlugin({
name: 'shell',
remotes: {
search: 'search@https://search.shop.example/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
},
});
И дальше обычный ленивый импорт:
// Remote выбирает runtime-реестр Module Federation; локального модуля для static import нет.
const SearchPage = lazy(() => import('search/SearchPage'));
Ключевое слово в конфиге, shared. Shared scope решает главную проблему single-spa: один React на всех.
Но он же главный источник боли: несовместимые версии общих библиотек
между командами это уже не конфликт в package.json, а инцидент в проде.
Web components: контейнер со стандартом
Custom elements плюс Shadow DOM дают нейтральный контракт: команда
отдаёт <shop-cart>, а кто и как его вставит, ей неважно.
class ShopCart extends HTMLElement {
connectedCallback() {
const root = this.attachShadow({ mode: 'open' });
mountCartApp(root); // внутри хоть React, хоть Lit
}
}
customElements.define('shop-cart', ShopCart);
Shadow DOM изолирует стили в обе стороны, и это одновременно фича и ограничение: глобальная тема приложения внутрь тоже не протекает, дизайн-токены приходится передавать через CSS custom properties.
Сводка
| Способ | Изоляция | Общие зависимости | Когда брать |
|---|---|---|---|
| iframe | полная | нет, всё дублируется | чужой и недоверенный код |
| single-spa | низкая | через import map, вручную | зоопарк фреймворков по историческим причинам |
| Module Federation | средняя | shared scope, автоматом | одна экосистема, независимые деплои |
| web components | стили через Shadow DOM | нет, договариваются сами | долгоживущий нейтральный контракт |
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.