Хочу представить решение по внедрению микрофронтендов в компании РТ МИС с помощью Custom Elements, чтобы связать приложения написанные на библиотеке ExtJS и React.

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

Микрофронтенды – общие моменты

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

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

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

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

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

Наши задачи

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

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

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

Кроме основного проекта, в компании разрабатываются несколько небольших приложений на React, которые полностью независимы. Часть функционала пересекается и по этой причине хотелось бы иметь возможность использовать React-блоки повторно в проектах как на React, так и на ExtJS.

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

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

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

    По процессу сборки нам близки интеграции Vanilla JS или jQuery с фреймворками, но подобной информации на момент знакомства с технологиями было крайне мало, поэтому многое пришлось выяснять самостоятельно.

  • Совместимость приложений
    Предусмотреть и добавить в приложения ограничения, чтобы избежать проблем при пересечении функционала, например, CSS стили, конфликты имён.

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

  • Процесс развертывания.
    Еще один пункт, который требует дополнительных усилий и времени, т.к. каждый микрофронтенд необходимо развёртывать и администрировать отдельно.

  • Увеличение объема итогового проекта
    Каждый микрофронтенд тянет за собой свои зависимости нескольких библиотек и фреймворков.

А что с достоинствами? 

  • Расширение технических возможностей
    Появляется возможность использовать любые фреймворки и библиотеки.

  • Модульность
    Больше гибкости, меньше монолитности, модули можно использовать в различных приложениях, их можно подключать между собой, разработка ведется независимо друг от друга.

  • Свобода выбора технологий
    В одном приложении можно сочетать различные технологии, например, Vue + React, или как в нашем случае — ExtJS + React.

  • Независимость
    Возможность полностью разделить ответственность на несколько независимых приложений, включая деплой и релиз;

  • Переиспользование
    Повторное использование микрофронтенда, нет привязки к монолиту, с данным подходом становится намного проще и естественнее создавать независимые приложения и повторно использовать их в нескольких проектах;

  • Время деплоя
    Сокращение времени на деплой отдельного приложения и возможность делать это параллельно;

  • Масштабирование
    Горизонтальное масштабирование команды, команды становятся независимы друг от друга.

И вот мы получаем с одной стороны более сложную систему, но с другой – больше гибкости и расширение возможностей, главное только заранее предусмотреть и учесть все сложности разработки. 

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

Выбор подхода

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

1. iframe

Подключать приложение с помощью тега <iframe>. Способ рабочий, но имеет ряд недостатков и накладывает ограничения:

  • Проблемы с взаимодействием микрофронтендов между собой.

  • Неудобная навигация. 

  • Сложно верстать в iframe.

  • Невозможность переиспользовать одно приложение в нескольких частях страницы.

  • Сложность поддержки.

И множество советов в интернете, которые крайне не рекомендуют строить микрофронтенд-архитектуру, используя iframe. 

Например в данных статьях: 

Микрофронтенды. Учимся на ошибках

Микросервисный подход в веб-разработке: micro frontends

Так что данный способ был оставлен на крайний случай и поиск подходящего способа внедрения продолжился.

2. Готовые решения

Например, single-spa, готовое решение для подключения различных микрофронтендов. В документации не приводилось примеров использования инструмента с чистым JS или простыми библиотеками.

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

3. Module Federation от Webpack 5

В Webpack 5 появился набор плагинов для обмена модулями между Javascript приложениями. Плагин Module Federation позволяет приложению экспортировать один или несколько модулей в отдельный JS файл. По отзывам отличный способ строить микрофронтенд приложения.

В нашем проекте на ExtJS не используется Webpack, а при попытке работы в ExtJS с помощью Module Federation возникли сложности, в тот момент была только beta версия Webpack 5 и модуль загрузки из коробки не работал.

Был вариант создавать свой модуль, который бы обращался к скрытым методам webpack для получения данных микрофронтенда. Обращаемся к скрытым методам вебпака (что само по себе не очень верно) и вручную прописываем логику загрузки модуля.

*возможно существует более лаконичный вариант с использованием Module Federation, в момент поиска решения была только бета версия webpack 5, да и на этапе выбора подходящего решения встретился четвертый подход работы с микрофронтендами и именно он показался максимально привлекательным.

4. Custom elements

Родительское приложение, к которому подключён микрофронтенд, взаимодействует с дочерним приложением довольно просто. 

На родительской странице подключаем итоговый бандл микрофронтенда, например, <script src="http://domen.ru/main.js"></script>

В коде микрофронтенда создаётся пользовательский HTML-элемент, принимающий данные от родителя с помощью механики custom elements.

Для работы с микрофронтендами через custom elements необходимо:

– связать микрофронтенд с кастомный тегом;
– в родительском приложении добавить созданный ранее кастомный тег в разметку.

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

Внедрение кастомных компонентов на практике

  1. Разработка модуля, который загружает бандл со сборкой микрофронтенда при переходе на соответствующий роут.

  2. Описание конфигурация микрофронтендов для привязки бандла к кастомному тегу.

  3. Углубленное изучение customElements. Когда загружается бандл, он содержит код, который записывает в window.customElements – кастомный тег в связке с классом, который является точкой входа микрофронтенда. Для названия кастомных тегов необходимо использовать дефис, например, <custom-element>.

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

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

Передача данных между родительским проектом и подключаемым микрофронтендом проще, чем в других способах за счет использования обычных атрибутов. Кастомные теги позволяют просто передавать данные между приложениями, сами следят за ними и выбрасывают события. Но также накладывается ограничение на объем данных, чтобы не создавать высокую нагрузку.

Как технически реализовать микрофронтенд с помощью кастомных элементов?

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

Для того, чтобы связать микрофронтенд и тег (название которого разделяется дефисами), необходимо точкой входа для всего приложения сделать класс, который наследуется от HTMLElement и определить кастомный тег следующей строкой:

customElements.define('custom-element', ContainerElement);

В результате мы получим следующий код:

class ContainerElement extends HTMLElement {
        ReactDOM.render(
            <Container is-active="false"/>,
            this
        );
    }
}

if (!customElements.get(custom-element')) {
    customElements.define(custom-element', ContainerElement);
}
  1. В родительском проекте необходимо загрузить бандл с полностью собранным приложением микрофронтенда.

После загрузки бандла можно проверить корректность работы тега, для этого прям в консоли получаем из customElements наш тег и смотрим, чтобы в результате был получен привязанный к тегу класс из пункта 1:

customElements.get(custom-element');

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

<custom-element></custom-element>

Когда в разметке браузер встречает <custom-element>, он начинает искать описанный для него сценарий в window.customElements.

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

Кроме того, HTMLElement позволяет передавать данные между приложениями в виде атрибутов тега и самостоятельно отслеживает их изменения.

class CustomElement extends HTMLElement {
    static get observedAttributes() { 
	/*массив имён атрибутов для отслеживания их изменений*/
        return ['special-program'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
       // вызывается при изменении одного из перечисленных выше атрибутов
    }

    connectedCallback() {
       // браузер вызывает этот метод при добавлении элемента в документ
    }
}

if (!customElements.get('component-with-event'))
    customElements.define('component-with-event', CustomElement);


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

<component-with-event special-program="regular"></component-with-event>

Событие attributeChangedCallback сработает при изменении значения у атрибута, что позволяет работать с данными в реальном времени.

Подробнее о Custom Elements можно почитать в данной статье: Пользовательские элементы (Custom Elements) 

Стоит обратить внимание на поддержку браузерами Window.customElements, она составляет 94.61% на текущий момент:

https://caniuse.com/?search=Custom%20Elements
https://caniuse.com/?search=Custom%20Elements

Что получилось в результате?

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

Изначально реализовано 3 приложения (не микрофронтенды), их необходимо объединить в один проект. Переносить их код в один репозиторий не хотелось, т.к. они уже написаны и могут пригодиться в других проектах, по-отдельности.

Получаем задачу: 3 приложения объединить в одно, с возможностью переключения между ними через общее меню.

В итоге было создано “главное приложение”, оно включает в себя авторизацию, заголовок, верхнее меню для переключения между микрофронтендами, модуль загрузки бандлов для соответствующего роута.

Примеры кода:

index.js

Генерация роутов на основе конфигурации.

Конфигурация подключаемых микрофронтендов

Содержит описание заголовка, наименования тега и путь до бандла

App.js

На основе роута получаем заголовок, передаём в Header + при наличии конфигурации для текущего роута подключаем компонент, который отвечает за работу с микрофронтендами

MainSpace.js

Остаётся только загрузить микрофронтенд, соответствующий текущему роуту и добавить в разметку тег, хранящийся в конфигурации

Итого, основное приложение отвечает только за авторизацию, меню и добавление нужного тега в разметку. Далее работа ведётся в приложениях микрофронтендов.

index.js

На этом отличия от обычного React-приложения заканчиваются. В App.js подключаем store, стили.

Стоит ли использовать данную технологию и целесообразно ли это вообще?

Перед применением нужно всё хорошо взвесить все "за" и "против", и после этого принимать итоговое решение, т.к. специфика проектов разная и подход не универсальный.

Всё зависит от сложившихся условий в проекте, бизнес-задач, архитектуры, ресурсах и числа разработчиков в компании.

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

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

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

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

Micro-frontends. Асинхронный подход к мультикомандной разработке

Микрофронтенды на tinkoff.ru

Webpack 5 — Asset Modules

Revolutionizing Micro Frontends with Webpack 5, Module Federation and Bit

Книга Building Micro-Frontends by Luca Mezzalira

Комментарии (4)


  1. nikfarce
    00.00.0000 00:00

    class ContainerElement extends HTMLElement {
            ReactDOM.render(
                <Container is-active="false"/>,
                this
            );
        }
    }
    
    if (!customElements.get(custom-element')) {
        customElements.define(custom-element', ContainerElement);
    }

    По-моему, connectedCallback потеряли перед ReactDOM.render и ' в вызове get


    1. yanabutorina Автор
      00.00.0000 00:00

      В этом примере только шаблон, без методов, а в примере ниже есть connectedCallback для срабатывания метода при добавлении элемента


  1. letzabelin
    00.00.0000 00:00

    Отличная статья!) а нет тестового репозитория, чтобы можно было поковыряться в коде?


    1. yanabutorina Автор
      00.00.0000 00:00
      +1

      Спасибо! Могу оформить в репозиторий только минимальный функционал, без итогового проекта, т.к. сам проект разглашать нельзя