Про микрофронты на коленке. Iframe


Мне в работе попадалась реализация микрофронтов на основе iframe. В целом, это вариант, когда надо «дёшево и сердито». Обычно такое используется, когда хост-приложение не одно и в каждом внутри своеобразная сборка, то есть федерацией модулей или монорепой всех проблем не решить.

Как это работает: по определённому адресу хостится мини-приложение, например, по урлу example.com/header грузится общее приложение хедера для всех возможных хост-сред. Во все хост-приложения хедер встраивается через iframe:

<iframe src="/header"></iframe>

Важный момент: чтобы не возникало проблем с CORS, при таком подходе приложения должны находиться на одном домене, а также полностью должны совпадать порт и протокол.

Редко когда такое мини-приложение живёт само по себе. Обычно нужно, чтобы оно как-то общалось со внешним миром. Для этого есть система двунаправленных событий, которые отправляются с помощью метода postMessage:

window.parent.postMessage — отправляет сообщение изнутри фрейма наружу, в родителя.

iframe.contentWindow.postMessage — отправляет сообщение снаружи, от родителя, внутрь фрейма.

Получаются отправленные события подпиской на message (как изнутри iframe, так и в родительском приложении снаружи):

parent.html:

<iframe id="myFrame" src="/child"></iframe>
<script>
const iframe = document.getElementById('myFrame');
// Отправка сообщения во фрейм
iframe.addEventListener('load', () => {
iframe.contentWindow.postMessage({ type: 'fromParent', payload: 'Привет!' }, '*');
});
// Получение сообщения от фрейма
window.addEventListener('message', (event) => {
if (event.data.type === 'fromChild') {
console.log('Получено от фрейма:', event.data.payload);
}
});
</script>

child.html:

<script>
// Отправка сообщения родителю
window.parent.postMessage({ type: 'fromChild', payload: 'Ответ!' }, '*');
// Получение сообщения от родителя
window.addEventListener('message', (event) => {
if (event.data.type === 'fromParent') {
console.log('Получено от родителя:', event.data.payload);
}
});
</script>

Эту идею можно раскрутить и дальше. Например, если одни и те же данные с сервера нужны в нескольких микрофронтах, то это может привести к дублированию запросов. Один из выходов — перенести этот дублирующийся запрос в хост-приложение и шарить его с фреймами. Ещё один вариант — сделать отдельный iframe, в котором визуально ничего нет (его можно даже скрыть), но есть данные. Данные этот стор-iframe может шарить наружу всем желающим, тоже через postMessage.

Также помимо общения через postMessage шаринг данных между контекстами можно построить на Broadcast Channel API или Channel Messaging API.

Broadcast Channel обеспечивает широковещательную связь между всеми контекстами одного origin, а Channel Messaging создает секьюрный двусторонний канал связи между двумя конкретными контекстами.

То есть этот самый «стор с шаренными данными» при получении данных может в зависимости от цели пулять данные или на всех, или точечно в конкретное место. Работают оба способа тоже на похожих postMessage.

В случае с Broadcast Channel надо как-то пошарить между потребителями созданный объект самого канала:

// Создание канала
const bc = new BroadcastChannel("test_channel");
// Отправка сообщения в канал
bc.postMessage("This is a test message.");
// Приём сообщения
bc.onmessage = (event) => {
console.log(event);
};

В случае с Channel Messaging параметры для связи двух контекстов передаются в доп параметрах postMessage:

main-page:

const channel = new MessageChannel();
const port1 = channel.port1;
// Подписка на загрузку iframe
iframe.addEventListener("load", onLoad);
function onLoad() {
// Слушание входящих сообщений на port1
port1.onmessage = onMessage;
// Отправка исходящего сообщения на port1
port1.postMessage(input.value);
// Отправка port2 в iframe
iframe.contentWindow.postMessage("init", "*", [channel.port2]);
}
// Обработка сообщений на port1
function onMessage(e) {
console.log(e.data);
}

child-page:

let port2;
// Слушание события инициализации
window.addEventListener("message", initPort);
// Настройка порта
function initPort(e) {
port2 = e.ports[0];
port2.onmessage = onMessage;
}
// Обработка сообщений на port2
function onMessage(e) {
port2.postMessage(`Message received by IFrame: "${e.data}"`);
}

Отдельно отмечу наличие атрибута loading=lazy у iframe. Это чтобы не подгружать те iframe, которые находятся вне вьюпорта, для экономии ресурсов.

Также дополнительно подсвечу, что в хромиум-браузерах все iframe на одном домене будут обрабатываться в одном потоке, то есть могут потенциально блокировать друг друга, даже если не блокируют родителя (лайфхак: если нет нужды держать их на одном домене, то выносом на другой домен можно «распараллелить потоки»).