Микрофронтенд с использованием Module Federation. Соединяем компоненты между системами на разных фреймворках

от автора

Всем привет! Мы — Иван и Даниил, ведущие разработчики компании 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *