Раздел 7 · Браузер и Frontend

Микрофронтенды и наблюдаемость: Module Federation, Sentry, RUM

senior~35 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Микрофронтенды и наблюдаемость: 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.