
Всем привет! Мы — Иван и Даниил, ведущие разработчики компании ITFB Group. У компании два собственных продукта — ЕСМ/CSP/BPM-платформа СИМФОНИЯ (документооборот, хранение контента, архив, портал) и система распознавания/обработки документов ITFB EasyDoc. Пару месяцев назад к нам прилетела задача интегрировать ряд функций распознавания из продукта ITFB EasyDoc и оформить их в отдельный модуль платформы СИМФОНИЯ, дабы пользователь всё делал в одном месте и не дрейфовал по разным системам. Однако возникла загвоздка: СИМФОНИЯ — на React, а ITFB EasyDoc — на Vue. Для решения вопроса посерчили различные источники информации и плавно ушли в собственное творчество, поскольку не обнаружили стоящих вариантов с вменяемой технической детализацией. В какой-то момент возникло острое желание поделиться нашими итоговыми наработками на Хабре и заполнить пробелы базы знаний в интернете по этому вопросу. Всем, кому интересно увидеть наше решение, добро пожаловать под кат)
Итак, этот пост посвящен интеграции между двумя решениями с использованием технологии микрофронтендов, в частности — Module Federation. Пример, который мы сейчас опишем, будет актуален для любых фреймворков: Vue, React, Angular и других. Из доступных подходов, таких как использование iframe, проксирование на уровне Nginx или микрофронтенд, мы выбрали последний как наиболее оптимальный для наших целей.
В реализации мы применяли технологии веб-компонентов и модульной федерации. Веб-компоненты представляют собой набор API веб-платформы, позволяющий разработчикам создавать настраиваемые, многоразовые и инкапсулированные HTML-теги для использования на веб-страницах и в веб-приложениях.
Для наглядности можно рассмотреть данную технологию на примере тега <video>. Это стандартный веб-компонент, который объединяет множество функций и разметки, такие как стилизованные кнопки управления, логика загрузки, воспроизведения и паузы. Компонент позволяет нам взаимодействовать через атрибуты, и на нем легко понять все плюсы и минусы веб-компонентов.
Плюсы:
-
Стили и логика компонента не конфликтуют с остальной частью приложения.
-
Компонент является частью стандарта, следовательно, обеспечена полная совместимость и стабильность.
-
Есть возможность использовать компонент независимо от фреймворков, и он легко интегрируется в существующее приложение.
Минусы:
-
Ограниченная поддержка браузерами.
-
Сложность кастомизации стилей извне.
Для нашего подхода важно, что технология предоставляет возможность исполнять код в моменты инициализации и размонтирования компонента на странице с помощью хуков жизненного цикла — connectedCallback и disconnectedCallback, — а также объявлять атрибуты и отслеживать их изменения. Это позволит инициализировать Vue-приложение с требуемым функционалом.
Базовый пример инициализации веб-компонента выглядит следующим образом:
class ExampleComponent extends HTMLElement { connectedCallback() { // cb вызывается при инициализации на странице /** исполняемый код **/ } disconnectedCallback() { // cb вызывается при удалении со страницы /** исполняемый код **/ } static get observedAttributes() { return ['example’]; // массив зависимостей, для которых будет отслеживаться изменение и вызываться cb attributeChangedCallback } attributeChangedCallback(name, oldValue, newValue) { /** исполняемый код **/ } } /** Добавление в CustomElementRegistry, добавляет глобально для приложения **/ customElements.define('example-component', ExampleComponent)
Module Federation — функция, которая предоставляет возможность JavaScript-приложению динамически загружать код из другого приложения во время выполнения. Module Federation позволяет независимым частям продукта делиться зависимостями, библиотеками или компонентами, что помогает уменьшить общий размер пакета и оптимизировать время загрузки. Данную технологию мы будем использовать для загрузки предварительно подготовленного веб-компонента.
Решение
Давайте начнем с подготовки встраиваемого компонента на Vue. Для этого будем использовать веб-компонент. В методе connectedCallback мы инициализируем экземпляр Vue-приложения (VerificationApp), а в disconnectedCallback — удалим приложение из памяти при удалении веб-компонента со страницы. Заодно устраним ошибки и немного перепишем код, чтобы он был более читабельным и аккуратным:
import Vue from 'vue' import store from './vuex' Vue.config.productionTip = false Vue.use(...) /* eslint-disable no-new */ const ConnectedApp = Vue.extend({ created: function () { /** бизнес логика **/ }, beforeDestroy() { /** бизнес логика **/ }, methods: { /** бизнес логика **/ }, render: (h) => h(App), store }) export class VerificationPackageService extends HTMLElement { instance = null; connectedCallback() { this.instance = new VerificationApp({ /** здесь будем дополнять **/ }).$mount() this.appendChild(this.instance.$el) } disconnectedCallback() { this.instance.$destroy(); this.removeChild(this.instance.$el) this.instance = null; } } customElements.define('verification-package-service', VerificationPackageService)
Включаем и настраиваем плагин для работы с Module Federation в Vite:
npm i –save-dev @originjs/vite-plugin-federation
import federation from '@originjs/vite-plugin-federation' export default defineConfig({ /** остальная часть конфига **/ plugins: [ federation({ name: "verification-package-service", filename: "remoteEntry.js", exposes: { './entry': './src/main.js' }, }) ], })
В React-приложении необходимо установить плагин для работы с Module Federation и указать путь до Vue-продукта:
import federation from '@originjs/vite-plugin-federation'; export default ({ mode }) => { return defineConfig({ /** остальная часть конфига **/ plugins: [ federation({ name: 'host-app', remotes: { 'verification-package-service': ‘https://…/assets/remoteEntry.js’, }, }), ], }); };
Для успешного подключения в продукт необходимо выполнить несколько шагов:
-
Добавляем отдельный роут.
-
В компоненте, который соответствует конкретному роуту, инициализируем React-компонент EasyDocWrapper. Этот компонент будет отвечать за загрузку и отображение Vue-компонента. Сначала объявим useRef для получения ссылки на DOM-элемент, в который будет добавлен веб-компонент (Verification Package Service). Затем функция LoadModule будет асинхронно загружать Vue-компонент и встраивать его в React-приложение. Это обеспечит гладкую интеграцию и изолированное взаимодействие между двумя фреймворками.
import { memo, useEffect, useRef } from 'react'; const EasyDocWrapper = () => { const ref = useRef<HTMLDivElement | null>(null); const loadModule = async () => { if (isLoaded.current) { return; } isLoaded.current = true; await import('verification-package-service/entry'); const element = document.createElement('verification-package-service'); ref.current?.appendChild(element); }; useEffect(() => { loadModule(); }, []); return <div ref={ref} />; }; export default memo(EasyDocWrapper, () => true);
После инициализации у нас есть функционирующий, но пока статичный вариант. Однако в текущем состоянии мы не можем обмениваться данными между приложениями или обрабатывать события.
Для передачи событий мы будем использовать небольшой самописный класс. Мы выбрали самый простой подход, но в дальнейшем можем усложнить его — например, используя EventEmitter и прочее. Логику мы добавим в React-приложение. Для возможности использования в другом продукте мы добавим экземпляр в глобальную переменную:
import { memo, useEffect, useRef } from 'react'; type EasyDocEventEmitter = Window & typeof globalThis & { easyDocEvents: any; }; const EasyDocWrapper = () => { const ref = useRef<HTMLDivElement | null>(null); … const initEventEmitter = () => { if ((window as EasyDocEventEmitter).easyDocEvents) { return; } class EasyDocEvents extends EventTarget { closePackage() { … } verifyPackage() { … } dispatchErrorStatus(statusCode: number) { … } } const instance = new EasyDocEvents(); (window as EasyDocEventEmitter).easyDocEvents = instance; }; const removeEventEmitter = () => { delete (window as EasyDocEventEmitter).easyDocEvents; }; useEffect(() => { … initEventEmitter(); return () => { removeEventEmitter(); }; }, []); return <div ref={ref} />; }; export default memo(EasyDocWrapper, () => true);
В приложении на Vue.js вызываем объявленные методы:
const VerificationApp = Vue.extend({ methods: { goToReactApp(type) { try { if (type === "verify") { window.easyDocEvents?.verifyPackage?.() } if (type === "close") { window.easyDocEvents?.closePackage?.() } } catch {...} }, }, … })
Для передачи данных между компонентами отлично подходят атрибуты. Если необходимо отслеживать изменения атрибутов, можно усложнить логику с использованием методов attributeChangedCallback.
Пример инициализации атрибута в продукте Vue:
export class ConnectedAppService extends HTMLElement { connectedCallback() { this.instance = new VerificationApp({ data: { packageId: this.getAttribute("package-id") // props } }).$mount() this.appendChild(this.instance.$el) } …}
Использование и передача данных в React-приложении:
const loadModule = async () => { … const element = document.createElement('verification-package-service'); element.setAttribute('package-id', packageId || ''); };
Итог
После выполнения всех исправлений и отладки два продукта успешно соединены визуально и функционально без видимых швов. Веб-компонент встроен на страницу и содержит необходимые разметку и логику:

Плюсом является то, что сборки продуктов полностью независимы и могут деплоиться и храниться в любом месте при условии правильно указанного пути до remoteEntry.js в конфигурации.
Минусом стало то, что в dev-сборке Vite с модульной федерацией не формирует файл remoteEntry, поэтому необходимо постоянно поддерживать стенд с собранной версией второго продукта.
Пример реализации можно посмотреть на GitHub.
ссылка на оригинал статьи https://habr.com/ru/articles/797429/
Добавить комментарий