Интернет-магазин делают пять команд: каталог, поиск, корзина, оформление заказа, личный кабинет. Код лежит в одном репозитории и выкатывается одной сборкой. Команда корзины исправила опечатку, но выехать не может: у поиска упали тесты. Релиз раз в неделю, и каждая команда ждёт остальных.
Микрофронтенды решают именно эту проблему: страницу собирают из частей, которые каждая команда разрабатывает и выкатывает сама. Ниже — какими способами это делают, как устроен самый популярный из них, Module Federation, и почему это решение для организации, а не для кода.
Оболочка знает только адреса частей; что внутри каждой и когда она обновилась, решает её команда.
Зачем делить фронтендспросят на собеседовании
Микрофронтенд — часть интерфейса, которой целиком владеет одна команда: от кода до выкатки. Выигрыш один, но большой: независимые релизы. Корзина выезжает пять раз в день, не дожидаясь поиска, и ошибка в поиске не блокирует остальных.
Цена тоже большая. Появляются несколько сборок, версии общих библиотек, договорённости о стилях и событиях, загрузка частей по сети. Если над приложением работает одна команда в десять человек, всё это — расходы без выгоды: ей хватит модульного монолита с чёткими границами, как в Feature-Sliced Design. Микрофронтенды окупаются, когда команд несколько и они мешают друг другу выкатываться.
Как собрать страницу из частейспросят на собеседовании
Части можно соединить на разных этапах, и от этого зависит, насколько они независимы.
Сборка из npm-пакетов — самый простой вариант, но независимости он не даёт: чтобы обновить корзину, хост всё равно пересобирают и выкатывают. Это разделение кода, а не релизов.
Сборка на сервере: сервер или CDN склеивает HTML из фрагментов, которые отдают разные сервисы. Хорошо для страниц, где важна скорость первой отрисовки, но интерактивность каждой части приходится оживлять отдельно.
Сборка в браузере: оболочка на лету загружает код частей. Старейший способ — iframe: полная изоляция, но трудно сделать общую навигацию, размеры и единый вид. Чуть современнее — Web Components: часть регистрирует свой тег, например <cart-widget>, и хост просто вставляет его в разметку. А самый распространённый сегодня в React-проектах — Module Federation.
Module Federation: модули, загружаемые на летуспросят на собеседовании
Module Federation появился в webpack 5, сейчас есть и в Rspack, и плагином для Vite. Идея: сборка одного приложения может отдать наружу часть своих модулей, а другое приложение импортирует их во время работы, как обычный import, хотя код лежит на другом сервере.
Корзина объявляет, что отдаёт, и собирает файл-оглавление remoteEntry.js:
new ModuleFederationPlugin({
name: "cart",
filename: "remoteEntry.js",
exposes: { "./CartWidget": "./src/CartWidget" },
shared: { react: { singleton: true, requiredVersion: "^18.2.0" }, "react-dom": { singleton: true } },
});
Хост знает только адрес оглавления и загружает виджет лениво:
new ModuleFederationPlugin({
name: "shell",
remotes: { cart: "cart@https://cart.shop.ru/remoteEntry.js" },
shared: { react: { singleton: true }, "react-dom": { singleton: true } },
});
const CartWidget = lazy(() => import("cart/CartWidget"));
Когда команда корзины выкатывает новую версию, хост ничего не пересобирает: при следующей загрузке страницы он скачает новый remoteEntry.js. Отсюда же главная грабля: сервер корзины может не ответить. Удалённый модуль всегда оборачивают в Suspense и предохранитель ошибок, чтобы падение корзины не уронило страницу.
Общие зависимости: одна копия Reactспросят на собеседовании
Если каждая часть принесёт свой React, страница будет весить вдвое больше, а хуки сломаются: React из одной копии не узнает компонент из другой и упадёт с ошибкой «Invalid hook call». Поэтому общие библиотеки объявляют в shared, и загрузчик решает, какую копию использовать.
С singleton: true копия всегда одна — та, что у хоста. Если её версия не подходит удалённому модулю, загрузчик только предупредит в консоли, а с strictVersion: true упадёт. Без singleton неподходящая версия приведёт вторую копию. Логика выбора в упрощённом виде:
живой пример
const hostShared = { react: "18.3.1" };
function satisfiesCaret(version, range) {
const [major, minor, patch] = version.split(".").map(Number);
const [rMajor, rMinor, rPatch] = range.slice(1).split(".").map(Number);
if (major !== rMajor) return false;
return minor > rMinor || (minor === rMinor && patch >= rPatch);
}
function resolve(remote, { range, own, singleton }) {
const host = hostShared.react;
if (satisfiesCaret(host, range)) return `${remote}: react ${host} от хоста`;
if (singleton) return `${remote}: react ${host} от хоста, в консоли предупреждение — ждали ${range}`;
return `${remote}: свой react ${own} — на странице две копии React`;
}
console.log(resolve("cart", { range: "^18.2.0", own: "18.2.0", singleton: true }));
console.log(resolve("search", { range: "^19.0.0", own: "19.1.0", singleton: true }));
console.log(resolve("search", { range: "^19.0.0", own: "19.1.0", singleton: false }));
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Отсюда правило: мажорные версии React, роутера и дизайн-системы обновляют согласованно, всеми командами сразу. Это единственное место, где микрофронтенды не независимы.
Как частям общатьсяспросят на собеседовании
Корзина должна сообщить шапке, что товаров стало три. Общий Redux-стор на все части связывает команды так же крепко, как монолит: изменил форму состояния — сломал чужой код. Поэтому части общаются через узкие и явные каналы: адрес страницы, атрибуты Web Component и события в браузере.
Так не надо: импортировать стор корзины в шапку и читать его поля напрямую.
Так надо: договориться об имени события и форме данных, это и есть контракт между командами:
живой пример
const bus = new EventTarget();
bus.addEventListener("cart:changed", (event) => {
console.log("шапка: в корзине", event.detail.count);
});
function addToCart(count) {
bus.dispatchEvent(new CustomEvent("cart:changed", { detail: { count } }));
}
addToCart(1);
addToCart(2);
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
В браузере вместо своего EventTarget обычно берут window. Стили изолируют префиксами классов, CSS-модулями или Shadow DOM у Web Components, иначе правило одной команды перекрасит кнопки другой.
Коротко
- Микрофронтенды — это независимые релизы частей интерфейса разными командами; для одной команды они дороже модульного монолита.
- Способы соединения: npm-пакеты (без независимых релизов), склейка на сервере, iframe, Web Components, Module Federation.
- Module Federation: удалённое приложение отдаёт модули через
exposesиremoteEntry.js, хост подключает их черезremotesиimportна лету. - Удалённый модуль — всегда в
lazy,Suspenseи предохранителе ошибок: его сервер может не ответить. - React и другие библиотеки с состоянием — в
sharedсsingleton: true; две копии React ломают хуки. - Части общаются через URL, атрибуты и события с оговорённым именем и формой, а не через общий стор.
Что почитать дальше
- Feature-Sliced Design — модульный монолит, которого хватает одной команде.
- Границы архитектуры — как не дать модулям лезть друг в друга.
- Сборка фронтенда — что делает бандлер и как работает динамический
import. - Обработка ошибок — предохранители, которые спасают страницу при падении части.