Web Apps: Micro Frontend фреймворк с поддержкой Module Federation

    Хочу представить фреймворк для написания микрофронтеднов с поддержкой Webpack Module Federation. Фреймворк позволяет связывать приложения написанные на любых библиотеках, ванильном JS, и даже IFrame, если дела совсем плохи.


    Недавно была статья, где довольно подробно было расписано, что есть на рынке. Однако большинство решений навязывают некие дополнительные концепции, будь то раутинг или реестр приложений, хотя по факту иногда ничего из этого не нужно, а нужен лишь простой и легкий способ внедрить одно приложение в другое.


    Но отступим на секунду назад, зачем вообще нужен подход с микро-фронтендами? В какой-то момент любое большое приложение перерастает само себя, билд процесс замедляется, каждое мелкое изменение выливается в минуты ожидания в watch mode, кода становится слишком много, и его уже невозможно эффективно поддерживать.


    Проблема усугубляется если команда хочет переходить на новый стек/фреймворк. Мы пытались плавно перейти с одного фреймворка на другой, заменив один монолит другим. Весь период перехода приходилось поддерживать оба продукта, и когда мы, наконец, закончили финальный монолит оказался совершенно огромным. В какой-то момент была высказана идея начать дробить его на под-приложения, но каждое из них разрабатывалось разными командами, имело разный интерфейс общения с хост-приложением, а некоторые под-приложения вообще были IFrame, т.к. нужна была максимальная изоляция. Такой подход, не смотря на минусы зоопарка, проработал какое-то время и был весьма успешен. Но на долгую перспективу он не работал.


    Как включить приложение написанное на одном фреймворке в другое приложение на другом фреймворке? **Нужен абстрактный оркестратор!**

    Прежде чем мы перейдем непосредственно к фреймворку, обозначим базовые термины:


    image


    • Хост — это приложение которое содержит в себе более мелкие приложения
    • Под-приложение (или просто Приложение) — это IFrame, Глобальное Приложение или Приложение-Веб Компонент, которые включаются в хост, они в свою очередь могут быть хостами для более вложенных приложений
    • Оркестратор: Web Apps Framework — агент, позволяющий хосту общаться с приложениями, загружать их и тд
    • Приложения не говорят друг с другом напрямую, но хост может их соединить

    Web Apps Framework


    Web Apps framework это фреймворко-независимый способ внедрить любое приложение в любое приложение, с обобщенным коммуникационным интерфейсом и другими бонусами.


    Репозиторий и демо.


    Рассмотрим скриншот из демо:



    Стрелки указывают на разные приложения, которые бесшовно связаны, но при этом могут быть отдельно разработаны и задеплоены. Два из них даже IFrame, что может быть полезно, если нужно внедрить лютый легаси-код, который вообще может быть написан без JS, или приложение которому нужна максимальная изоляция стилей и кода.


    Само собой, существуют и другие фреймворки, делающие то же самое, но Web Apps отличается особым минимализмом по части оберток и загрузчиков, и предоставляет унифицированный интерфейс для всех фреймворков: Angular, Vue, React, jQuery, без JS вообще, и т.д.


    Есть два способа вставить приложение, как веб-компонент, и более оптимизированная версия для React. Все что нужно — указать URL и вуаля, приложение будет загружено и вставлено в DOM хоста, а все обработчики событий автомагически будут созданы:


    import {useApplication, eventType, useListenerEffect, dispatchEvent} from '@ringcentral/web-apps-host-react';
    
    const Page = () => {
        const {Component, node} = useApplication({
            id: 'xxx',
            type: 'script',
            url: [ 
               'http://example.com/styles.css', // стили тоже можно указывать
               'http://example.com/bundle.js',
               'http://example.com/entry.js'
            ]
        });
        useListenerEffect(node, eventType.message, (message) => {
            alert(`message from app: ${message}`);
        });
        return <>
            <Component someProp="someValue" />
            <button onClick={e => {
                dispatchEvent(node, eventType.message, {foo: 'bar'})
            }>Send Message</button>
        </>;
    };

    Коммуникация может быть через события или через props (они же HTML attributes, в зависимости от способа подключения). События передаются в любой тип приложений, включая IFrame, через одинаковый интерфейс, так что хост может подменять приложения в любой момент, без необходимости знать тип. Это пригодится если вам надо загружать приложение для главной контентной области в зависимости от URL, откуда может быть взять ID приложения (в демо сделано именно так).


    Вот пример как внедрять приложение в хосты, которые не написаны на React:


    // где-то в коде хоста
    import '@ringcentral/web-apps-host-web-component';
    // и просто добавить это в HTML/DOM
    <web-app id='appId' url='["http://example.com"]' type="iframe" />

    Как написать приложение?


    Никаких особых техник для подготовки приложения не требуется, просто пишете его как обычно, и добавляете небольшую обертку в самом начале, в зависимости от типа приложения:


    Глобальное приложение — просто скрипт который получает DOM Node и вставляет в него приложение
    Приложение-веб компонент — приложение загружается как скипт и создает Custom Element, в который завернуто все остальное, позволяет добиться более хорошей изоляции от хоста благодаря Shadow DOM и Shadow CSS
    Приложения IFrame Apps — для дополнительной изоляции и безопасности, когда хост не может полностью доверять приложению и показывать его в той же среде, что и он сам


    Глобальные приложения


    Допустим мы хотим внедрить приложение на React. С поддержкой Module Federation, точка входа будет выглядеть как просто default export функции, принимающей DOM Node и рисующей в ней приложение:


    import React from "react";
    import {render, unmountComponentAtNode} from "react-dom";
    import {registerAppCallback} from "@ringcentral/web-apps-react";
    import App from "./App";
    
    export default (node) => {
        ReactDOM.render(<App foo={node.getAttribute('foo')} />, node);
        return () => ReactDOM.unmountComponentAtNode(node);
    });

    И все.


    Мы плотно работали с авторами Module Federation (спасибо Zack Jackson) чтобы интеграция была максимально легкой.


    Можно добавить MutationObserver, тогда можно пробросить изменения динамически в Реакт приложение в качестве props.


    Если на хосте нет поддержки Module Federation, не беда, есть выход через простой JSONP-вызов:


    import React from "react";
    import {render, unmountComponentAtNode} from "react-dom";
    import {registerAppCallback} from "@ringcentral/web-apps-react";
    import App from "./App";
    
    registerAppCallback('%appId%', (node) => {
        ReactDOM.render(<App foo={node.getAttribute('foo')} />, node);
        return () => ReactDOM.unmountComponentAtNode(node);
    });

    Приложения на Web Component


    Другой способ внедрить приложение — завернуть его в Web Component для лучшей изоляции CSS стилей посредством Shadow DOM и Shadow CSS:


    import React from "react";
    import {render, unmountComponentAtNode} from "react-dom";
    import {App} from './app';
    
    const template = document.createElement('template');
    
    template.innerHTML = `
        <style>/* shadow CSS */</style>
        <div class="container"></div>
    `;
    
    customElements.define('web-app-%appId%', class extends HTMLElement {
        constructor() {
            super();
            this.attachShadow({mode: 'open'});
            this.shadowRoot.appendChild(
                document.importNode(template.content, true)
            );
            this.mount = this.shadowRoot.querySelector('.container');
        }
    
        connectedCallback() { render(
            <App 
                authtoken={this.getAttribute('authtoken')}
                node={this}
            />, 
            this.mount
        ) }
    
        disconnectedCallback() { unmountComponentAtNode(this) }    
    });

    Поскольку на изменения attributes можно подписаться, их можно пробросить в Реакт приложение.


    Приложения IFrame


    Для использования IFrame не нужно вообще никакого кода в самом простом случае. Но если вы хотите коммуницировать, то можно добавить следующее:


    import {
        IFrameSync, dispatchEvent, eventType
    } from '@ringcentral/web-apps-sync-iframe';
    
    export const sync = new IFrameSync({
        history: 'html5',
        id: 'iframe',
        origin: window.location,
    });
    
    // теперь можно слушать и посылать события
    const node = sync.getEventTarget();
    dispatchEvent(node, eventType.message, 'Hello from IFrame');
    node.addEventListener(eventType.message, message => alert(message));

    А что насчет общих библиотек?


    Очевидно, как только у каждого приложения появляется свой билд-процесс, то все общие зависимости вроде react or react-dom начинают дублироваться. Иногда на это можно закрыть глаза, но иногда это проблема. Можно, конечно, обойтись externals, и другими трюками, но, к счастью, есть и более хорошее решение: Webpack 5 Module Federation.


    Фреймворк Web Apps поддерживает Module Federation из коробки, и позволяет иметь общие зависимости между хостом и приложеиями, равно как и между приложениями (чего невозможно добиться стандартными костылями). Фреймворк и так довольно минималистичен, а федерация позволяет оптимизировать все еще больше. Module Federation также умеет автоматически догружать зависимости если хост не может их предоставить, грузить нужные версии. Настраивается все абсолютно стандартно:


    // на хосте в webpack.config.js
    module.exports = {
        ..., // all the usual stuff
        plugins: [
            new ModuleFederationPlugin({
                name: 'web-app-host',
                library: {type: 'var', name: 'web-app-host'},
                shared: ['react', 'react-dom'],
            }),
        ]
    };
    
    // в приложении в webpack.config.js
    module.exports = {
        ..., // all the usual stuff
        plugins: [
            new ModuleFederationPlugin({
                name: 'web_app_federated',
                library: {type: 'var', name: 'web_app_federated'},
                filename: 'remoteEntry.js',
                exposes: {
                    './index': './src/index',
                },
                shared: {
                    'react-dom': 'react-dom',
                    moment: '^2.24.0',
                    react: 'react',
                },
            }),
        ]
    };

    Вот и все, теперь хост и приложение могут иметь общие зависимости, чисто и прозрачно.


    Еще немного об IFrame


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


    Сообщения могут быть реализованы через PostMessage, но Web Apps упрощает и стандартизирует интерфейс, а также уберегает от "потопа" из широковещательных post messages.


    А вот взаимодействие с попапами несколько хитрее.



    Обратите внимание на визуальное различие хоста и приложения: серый фон под попапом не распространяется на хост (что неудивительно, т.к. он внутри фрейма). Выглядит некрасиво!


    Web Apps фреймворк имеет специальный метод для избавления от этой проблемы, фрейм может послать сообщение, что открыт попап с фоном определенного цвета, на хосте будет создан слой с совпадающим цветом, поэтому разница не будет заметна. Если кликнуть по этому слою в хосте попап закроется внутри фрейма.



    Как можно увидеть, границы больше нет.


    Также фреймворк умеет синхронизировать высоту фрейма и содержимое фрейма, чтобы длинные тексты отображались без подрезок.


    Итого


    Подытожим ключевые возможности Web Apps Framework:


    • Поддержка Module Federation
    • Синхронизация Location
    • Возможность "глубоких ссылок" — “приложение из приложения” или “приложение на хост” или “хост на приложение”
    • Унифицированный событийный интерфейс между хостом и приложениями
    • Поддержка подгонки размера IFrame под содержимое
    • Поддержка всплывающих окон в IFrame
    • Следование Веб стандартам
    • Написан на TypeScript
    • React и Web Component обертки для хоста
    • Бесконечная вложенность приложений — приложение может являться хостом для следующих приложения
    • Бесшовная навигация

    Репозиторий с примерами и демо.


    Благодарности


    Zack Jackson, Chris Van Rensburg и Антону Бульёнову.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 7

      0
      но, к счастью, есть и более хорошее решение: Webpack 5 Module Federation.

      Зачем? Есть же нативные модули? есть import-maps, и полифил для него, которое позволит из внутреннего приложения загрузить и стили и ресурсы через import.meta.url. и зависимости разруливаются.
        0

        Процитирую разработчика Webpack — Tobias Sokra:


        No billion or trillion dollar company will use that. Import maps will realistically take 5 years to gain enough browser support to be used. It's like depending on polyfills to things that do not exist. Production code depends on a shim ultimately worse perf. Even when import maps come out, so what. Something still needs to append them to the page. Who cares if its script or something else, webpack will still need to manage it at scale and you still need tree-shaking. Plus import maps only “share”code if the path is exactly the same. Making it still no good and not near a replacement.

        Еще добавлю отсутствие версионирования зависимостей, поддержку только JS модулей (без CSS и тд), повышенную нагрузку на сеть по кол-ву запросов и худшее сжатие. У меня была статья https://habr.com/ru/post/474672/ — import maps еще очень далеко до реального применения.

          0
          Процитирую разработчика Webpack — Tobias Sokra:

          цитата Tobias Sokra, если честно больше похожа на выкрик, импорты плохо, а вот наша штука лучше. С модулями, начиная с requirejs было примерно так же, каждый раз был, вот те модули плохие, а наши лучше, в итоге имеем AMD, SystemJs, CommonJs и вот еще модули от вебпака. Если к импортам относиться так же, что они не будут использованы. То к вебпак модулям наверное так же надо:). Только разница, что es модули поддерживаются(будут поддерживаться в полном мере) нативно. Полифилы всегда используются. А если полифил еще и следует какому-то стандарту. То ИМХО лучше идти этим путем.
          Еще добавлю отсутствие версионирования зависимостей

          import-map поддерживает scope

          поддержку только JS модулей (без CSS и тд)

          если использовать полифил то там не только css модули

          повышенную нагрузку на сеть по кол-ву запросов и худшее сжатие

          Использование es модулей подразумевает под собой использование http2, server push и тому подобное. нужно корректно настроить кеш, сервер пуш, etc.
          худшее сжатие, вы про tree shaking? если так, то получаеться вам нужно каждый раз пересобирать, базовое приложение чтоб добавить нужные вендор зависимости, выпилить то что не нужно(если я правильно понял как работает Module Federation). То какой профит от мини фронтенда, если при релизи вроде бы как атомарного мини приложения, которая не должна аффектить все остально, нужно запускать всю сборку?

          habr.com/ru/post/474672 — import maps еще очень далеко до реального применения.

          да читал, и решения засорять глобальную область видимость через сервисворкер так себе. Все можно было сделать проще используя тот же роллап с конфигом на несколько строк, и этот скрипт вызывать в postinstall. И будет реакт акктуальной версии с es модулями.

          На мой взгляд, вся суть микрофронтенды это атомарность, в вашем случае получаеться аффектиться сборка основного приложения с зависимостями.

          В вебе пытаются стандартизировать, модули, модуль-импорты, да, нет инструментов для разработки из коробки, но их и не будет, если идти по пути реакта который не может определиться как публиковать пакеты или вебпака, который в 4ой версии не умеет билдить в нативный es модуль. Раньше было куча браузеров который изобретал свой стандарт, сейчас браузеров меньше стандарт один, но тут разработчики, теперь каждый раз изобретают свой стандарт)
            0
            Tobias Sokra, если честно больше похожа на выкрик, импорты плохо, а вот наша штука лучше

            Полифил непринятого стандарта — это стремно. Вебпак давно зарекомендовавший себя инструмент, так что автор прав, ни одна серьезная компания на импорт-мапы пока не подпишется.


            Использование es модулей подразумевает под собой использование http2, server push и тому подобное

            Про HTTP2 я еще у себя в статье писал.


            худшее сжатие, вы про tree shaking?

            Крупные куски сжимаются лучше чем много мелких.


            если я правильно понял как работает Module Federation

            Не правильно, см презентацию https://github.com/sokra/slides/blob/master/content/ModuleFederationWebpack5.md — это живая система, а не "хоровой деплоймент".


            да читал, и решения засорять глобальную область видимость через сервисворкер так себе

            Это ж эксперимент, в котором по условиям нельзя было применять никакую сборку.


            в вашем случае получаеться аффектиться сборка основного приложения с зависимостями

            Вовсе нет. Хост разглашает, что у него есть, приложения — что нужно им и что есть у них, все это максимально развязано. В крайнем случае скачается несколько раз.

              0

              Самая главная проблема с импорт-мапами, даже со скоупами, в том, что хост обязан все знать про все модули, приложения, по всей видимости, могут догружать свои импорт-мапы но кто будет следить за чистотой, чтоб не было коллизий, и за оптимальным использованием версий и тд. Федерация поддерживает семвер например.

                0
                Полифил непринятого стандарта — это стремно. Вебпак давно зарекомендовавший себя инструмент, так что автор прав, ни одна серьезная компания на импорт-мапы пока не подпишется.


                Ну у Вас какие-то двойные стандарты:). В Вашем решении вы используете вебкомпоненты, которые, тоже не особо железобетонный стандарт. С теми же HTML импортами там много вопросов. Те вы используете полифил не принятого, до конца стандарта:)
                Так же, когда не был принят стандарт es модулей, в коде его активно использовали, а потом пропускали через babel, и как то не особо, люди беспокоились, что это не принятый стандарт. По поводу поддержки импортов, буквально пол года назад, была только поддержка в Chrome canary с флагом, сейчас во всех Chrome'ах с флагом, так что думаю скоро будет работать и без флага. По поводу Webpack, он хорош себя зарекомендовал для сборки бандла приложения, когда все в одном. А вот для сборки модуля, тут вопрос, тк на данный момент(в 4ой версии) он не умеет выплевывать es модуль(или я так и не смог понять как настроить его на это%).

                Крупные куски сжимаются лучше чем много мелких.

                Честно, я в этом вопще не вижу какой либо проблемы, тк такого рода проекты, пишутся где количество кода, ресурсов, стилей измеряется мегобайтами, и экономия в несколько килобайт как не выглядит не очень. ИМХО Тут другой подход нужен, тот же HTTP2, сервер пуш, сервис воркер.

                Вовсе нет. Хост разглашает, что у него есть, приложения — что нужно им и что есть у них, все это максимально развязано. В крайнем случае скачается несколько раз.

                Ну тогда получаеться:
                Module Federation — это набор утилит для вебпака которые включает в себя какое то описание модуля, со своими зависимостями, и сборка всего этого.
                Тот же import map, это просто будущий стандарт который позволяет зарезолвить нейминг в импортах для es модулей. es модули это не только модули js кода, но в будущем это и html и css в тех же вебкомпонентах. Тогда получаеться, если были бы утилиты которые могли сгенерировать import map, предоставить информацию другим мини приложении о том что есть у них, все это вместе собрать, то получиться все тоже самое, только с поддержкой нативных технологий. И тогда, наверное, можно было сравнивать. Но чтоб все это реализовать, нужно сделать несколько больше, чем просто написать конфиг для вебпака.

                В целом я понял вашу идею — это еще один вариант реализации микрофронтенда с таким же количеством вопросов к обсуждению, как и в десятке других решений, существующих на текущий момент. Спасибо за статью)
                  0

                  Прошу прощения, но это у вас неверные сведения. Web Components — это Living Standard, реализованный на уровне браузера. Import Maps — not a W3C Standard nor is it on the W3C Standards Track. На сайте красным написано Unofficial Draft. В Бабеле все вещи минимум в драфте.


                  Более того, использование WC опционально, фреймворк и без них работает.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое