Интеграция React и AngularJS через Webpack Module Federation

cd99c789bfab21b44df6892d5d002b4a.jpeg

Почему пишу об этом?

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

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

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

Иметь два приложения = иметь в 2 раза больше работы

Иметь два приложения = иметь в 2 раза больше работы

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

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

Сегодня я хочу поделиться не столько пошаговым руководством, сколько представлением одной интересной идеи, которая может помочь разработчикам, столкнувшимся с задачей интеграции приложений на Angular и React. Мой фокус сегодня — на использовании Module Federation для решения этой проблемы. Хотя это и не может считаться абсолютно идеальным решением, оно тем не менее работает и может стать тем самым толчком для кого-то, кто сейчас ищет способы объединения этих двух миров.

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

Дружное взаимодействие компонентов на React и AngularJS

Дружное взаимодействие компонентов на React и AngularJS

Рассмотрим стек:

Микрофронтенды

Как микрофронтенды решают проблемы

Как микрофронтенды решают проблемы «больших» проектов

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

Однако, концепция «микрофронтендов» предполагает существенное изменение этой парадигмы. Суть микрофронтендного подхода заключается в том, что в рамках одного приложения запускаются несколько фронтенд проектов (или модулей), которые между собой слабо связаны. Это означает, что каждый из этих микрофронтендов может быть разработан, протестирован, собран и развернут независимо от остальных, при этом все они совместно формируют единое приложение.

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

Плюсы:

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

  • Технологическая гибкость: Микрофронтенды позволяют собирать веб-сайты, используя различные технологии в рамках одного проекта. Это особенно ценно для проектов, стремящихся соединить новые и устаревшие технологии, например, React и AngularJS, о чем и идет речь в данной статье.

Минусы:

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

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

  • Вопросы производительности: Интеграция множества независимых приложений может привести к увеличению общего объема загружаемого кода и, как следствие, к снижению производительности, особенно если одни и те же библиотеки (например, Lodash) используются в разных приложениях. Однако, использование таких механизмов, как Module Federation, может помочь решить эту проблему, позволяя делиться зависимостями между микрофронтендами, тем самым уменьшая общий объем загружаемого кода.

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

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

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

В этой статье мы рассмотрим, как можно преодолеть это ограничение и интегрировать приложение React, использующий Webpack, в AngularJS, собираемый с помощью Gulp.

Сравнение подходов

Разработчики решают какой подход лучший

Разработчики решают какой подход лучший

Как мы упомянули ранее, существует несколько подходов к созданию проекта, в котором микрофронтенды на React и Angular могут успешно сосуществовать:

  1. Использование iframe: В Angular-приложении можно встроить iframe, указывающий на отдельное приложение на React. Главный недостаток этого метода заключается в трудностях взаимодействия между приложениями через postMessage, что требует разработки специального механизма для обмена сообщениями.

  2. Хитрый routing через Reverse Proxy: Этот подход предлагает отображение разных приложений на разных URL. Например, Angular-приложение доступно по пути http://site.com/angular/…, а React-приложение — по пути http://site.com/react/…. Среди минусов:

    • Сложности с передачей состояния между приложениями, возможно, через localStorage или параметры URL.

    • Невозможность переиспользования компонентов между двумя отдельными приложениями, например, Sidebar или Topbar.

    • Ухудшение пользовательского опыта из-за перезагрузки страниц при переходах между приложениями.

  3. Module Federation: Это фишка Webpack, позволяющая создавать приложения, которые могут действовать как зависимости друг для друга и обмениваться сторонними библиотеками. Основной минус — необходимость использования единой системы сборки (Webpack), но в рамках этой статьи мы рассмотрим, как преодолеть это ограничение.

  4. Single SPA: Фреймворк для создания микрофронтендов, использующий подход сборки в браузере через современные ES модули. Подходит для проектов с фронтендами на разных технологиях и предоставляет «оболочку», которая через JavaScript объединяет различные приложения. В этой статье мы сосредоточимся на более простом решении, однако в будущем стоит рассмотреть и этот подход более детально.

Поделитесь в комментариях, если знаете другие эффективные методы для реализации микрофронтендной архитектуры!

Взгляд сверху

Здорово когда весь подход как на ладони

Здорово когда весь подход как на ладони

В нашем проекте мы организуем два типа сборок: dev и production. Angular-приложение собирается с использованием Gulp, а React-приложение — через Webpack.

Для production сборки: мы используем Docker для параллельного запуска процессов сборки, после чего результаты — статические файлы для Angular и React — отдаем в Nginx. React-приложение собирается с применением плагина ModuleFederation.

Для dev сборки: на локальной машине запускается webpack-dev-server, который обслуживает Angular как статические файлы и React в штатном режиме. В данной статье мы не будем углубляться в детали этой сборки, поскольку она представляет собой достаточно стандартную конфигурацию без использования Module Federation.

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

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

- Dockerfile
- package.json
- packages // microfrontends
	- react-app
		- package.json
    - src
			- angularIntegration.ts // entry to react app for angular app
			- angularContext.tsx // pass global props from angular
			- index.tsx
  - angular-app
    - package.json
    - app
      - index.html
			- scripts
        - react-integration.js // load react API and add that to `window`
				- routes.js
        - components
	        - sidebar
		        - sidebar-d.js // example of component

Сборка

Фронтендеры придумывают новый сборщик проектов

Фронтендеры придумывают новый сборщик проектов

Монорепозиторий

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

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

Чтобы сосредоточиться на ключевых моментах и не усложнять статью деталями управления монорепозиторием, мы не будем подробно останавливаться на инструментах для работы с монорепами. Вместо этого предположим, что у нас уже настроены отдельные скрипты для запуска production и dev сборок прямо из корневого package.json: react-app:build, react-app:dev, angular-app:build, angular-app:dev.

Production сборка

Собираем микрофронтенды на React и Angular вместе в Docker

Собираем микрофронтенды на React и Angular вместе в Docker

Для production сборки мы принимаем подход, при котором сборка Angular и React приложений происходит отдельно в Docker. После завершения сборки, результаты — статические файлы обоих приложений — помещаются в директорию app/www для дальнейшей раздачи через Nginx. Одной из ключевых особенностей этого процесса является использование плагина ModuleFederation в сборке React-приложения, который генерирует файл remoteEntry.js.

Этот файл remoteEntry.js играет центральную роль в механизме Module Federation, позволяя различным фронтенд приложениям динамически загружать и использовать код друг друга как зависимости во время выполнения. Это обеспечивает гибкость и модульность, позволяя, например, Angular-приложению интегрировать и использовать компоненты React без необходимости статического включения всего приложения в сборку. Таким образом, обе сборки могут оставаться независимыми, в то время как ModuleFederation способствует эффективному взаимодействию между ними на этапе выполнения.

В контексте совместной работы AngularJS, собранного через Gulp, и React, использующего Webpack и Module Federation, мы напрямую взаимодействуем с remoteEntry.js из Angular. Этот файл, созданный для React, автоматически управляет загрузкой нужных частей приложения. Подключив remoteEntry.js к Angular, мы получаем доступ к React-компонентам и функциям, облегчая интеграцию между различными частями проекта без сложных настроек.

Docker

Конфигурация Docker для нашего проекта организована в несколько стадий. На первом этапе происходит копирование package.json для обоих приложений и установка их зависимостей. Далее, процесс продолжается параллельной сборкой продакшен версий React и Angular приложений. Завершающий шаг включает копирование настроек Nginx и необходимой статики.

Примерная конфигурация выглядит так:

#
# Stage 1.1 - APP: Install modules
#
FROM node:20.3.0 AS app_modules_installer
WORKDIR /app
COPY package.json yarn.lock /app/
COPY packages/angular-app/package.json /app/packages/angular-app/
COPY packages/react-app/package.json /app/packages/react-app/
RUN yarn install --frozen-lockfile
#
# Stage 2.1 - APP: Build Angular app
#
FROM app_modules_installer AS app_angular_builder
WORKDIR /app
COPY packages/angular-app /app/packages/angular-app
RUN yarn angular-app:build
#
# Stage 2.2 - APP: Build React app
#
FROM app_modules_installer AS app_react_builder
WORKDIR /app
COPY packages/react-app /app/packages/react-app
RUN yarn react-app:build
#
# Stage 3.1 - NGINX: Build
#
FROM nginx:1.25-alpine3.18 AS nginx_builder
COPY deploy/nginx.conf /etc/nginx/nginx.conf
#
# Stage 3.2 - NGINX: Add assets
#
FROM nginx_builder
COPY --from=app_angular_builder /app/packages/angular-app/www /app/www
COPY --from=app_react_builder /app/packages/react-app/build /app/www/react-app

Module Federation Plugin

В проект интегрирован плагин Module Federation, о котором подробнее можно узнать на официальном сайте Webpack. Суть заключается в том, что наше приложение идентифицируется как react-app, для интеграции используется файл remoteEntry.js, и мы экспортируем модуль под названием angularIntegration.

new ModuleFederationPlugin({
    name: 'react-app',
    library: { type: 'global', name: 'react-app' },
    filename: 'remoteEntry.js',
    exposes: {
        angularIntegration: './src/angularIntegration.ts',
    },
}),

Для завершения интеграции необходимо добавить в index.html Angular-приложения строку . Этот скрипт будет подробно рассмотрен позже, но его подключение уже готовит платформу для интеграции между Angular и React.

Dev сборка

Буквально пару слов. Для dev сборки мы не используем Module Federation, а основываемся на webpack-dev-server, который обеспечивает раздачу статики и HMR для React. Angular собирается и прокидывается как статика для Webpack.

P.S.: Честно, в первой версии статьи было много деталей об этом процессе, но из-за рутины в настройке путей файлов решил не усложнять. Как-то так!

Взаимодействие в коде

Когда программировал и вошел в поток

Когда программировал и вошел в поток

От интеграции React и Angular мы ожидаем следующего:

  1. Возможность интеграции React компонента: Нам нужна легкость во вставке React компонентов в Angular-приложение, как будто это были обычные Angular компоненты.

  2. Динамическая передача данных в React компонент: Хотим передавать данные из Angular в React не только при инициализации компонента, но и обновлять эти данные на лету, аналогично тому, как работают props в React.

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

  4. Корректное удаление React компонента: При удалении React компонента из DOM Angular-приложения требуется также очистить Virtual DOM этого компонента, чтобы избежать его накопления и возможных утечек памяти, что видно через DevTools.

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

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

Схема взаимодействия React и Angular на уровне кода

Схема взаимодействия React и Angular на уровне кода

Для успешной интеграции React компонента в Angular приложение необходимо выполнить следующие шаги:

  1. Загрузка remoteEntry.js: Этот файл, предоставляемый Module Federation, содержит необходимую информацию для загрузки и исполнения модулей React в Angular.

  2. Сохранение функции отрисовки в window: Это позволяет глобально обращаться к функции отрисовки React компонента из Angular приложения.

  3. Вызов отрисовки React компонента из Angular: Инициируется процесс отрисовки React компонента в заданном месте Angular приложения.

  4. Создание React root: Используя ReactDOM.createRoot, создается корневой элемент React на основе указанного контейнера.

  5. Отрисовка компонента: С помощью метода root.render происходит отрисовка React компонента.

  6. Подготовка компонента с Observer: Компонент оборачивается в Observer, что позволяет динамически передавать в него props из Angular. Возвращается объект на сторону Angular, содержащий:

    • Функцию unmount для очистки Virtual DOM, когда компонент больше не нужен, например, при смене страницы.

    • Observer для динамической передачи props.

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

AngularJS

Мудрый AngularJS смотрит на тебя

Мудрый AngularJS смотрит на тебя

Шаг 1: На стороне Angular, в файл index.html напрямую включается скрипт react-integration.js, как это принято делать с любыми другими скриптами. Этот скрипт отвечает за динамическую загрузку на страницу файла remoteEntry.js. Файл remoteEntry.js является специальным артефактом, создаваемым с использованием плагина Module Federation, и предоставляет API для взаимодействия с React приложением. В нашем случае, это API именуется react-app:

// react-integration.js

(function () {
    // #1
    if (
        ["localhost", 'some-dev-domen.com'].some((host) =>
            window.location.host.includes(host)
        )
    ) {
        return; // dev mode
    }

    const createScript = (src) => {
        const script = document.createElement("script");

        script.src = src;
        script.type = "module";

        document.head.appendChild(script);
    };

	// #2
    createScript(
        `react-app/remoteEntry.js?hash=${window.REMOTE_ENTRY_HASH}`
    );

    // #3
    const reactAppName = "react-app";
    const moduleName = "angularIntegration";

	// #4
    const getApi = () => {
        return window[reactAppName]
            .get(moduleName)
            .then((mod) => mod())
            .then((mod) => mod.default);
    };

	// #5
	window.renderReactSidebar = (container, globalAngularProps, props) => {
        return getApi().then((render) =>
            render(container, "sidebar", globalAngularProps, props)
        );
    };

    window.renderReactPage = (container, globalAngularProps, props) => {
        return getApi().then((render) =>
            render(container, "page", globalAngularProps, props)
        );
    };
})();

Давайте более детально рассмотрим процесс работы со скриптом react-integration.js:

  1. Проверка среды выполнения: Сначала убеждаемся, что находимся в production режиме. Для dev сборки Module Federation не используется, а вместо этого запускается webpack-dev-server.

  2. Добавление скрипта remoteEntry.js: На страницу динамически добавляется скрипт с remoteEntry.js, включая в его адрес хеш для предотвращения кэширования браузером. Это особенно важно для обновлений, чтобы браузер загружал актуальную версию. Хеш может быть сгенерирован в процессе сборки Angular через Gulp, например, используя текущее время (Date.now()).

  3. Подготовка переменных: Определяются переменные с названием приложения и экспортируемыми элементами, настроенными в Module Federation. Это позволяет ясно определить, какие части React приложения будут использоваться.

  4. Создание интерфейса для загрузки модуля: Реализуется функционал для удобного получения и отрисовки React компонентов из модуля.

  5. Регистрация функций отрисовки: В объект window добавляются функции renderReactSidebar и renderReactPage, которые будут вызываться из Angular, например, в контроллерах или директивах, для отрисовки React компонентов.

Шаг 2: Вместо добавления функций, таких как renderReactSidebar, в систему Dependency Injection Angular, было принято решение использовать более простой подход, включая эти функции напрямую в объект window. Это облегчает их доступность и вызов из различных частей Angular-приложения. При вызове этих функций передается контейнер из Angular, в который будет встроен React компонент, глобальные настройки, такие как обработчики для ui-router, а также начальные props для компонента. Этот метод позволяет гибко интегрировать и управлять React компонентами внутри Angular-приложения, предоставляя все необходимые данные и контекст для их корректной работы.

Шаг 3: Далее, используя Angular директиву, можно инициировать процесс отрисовки React компонента. Это делается путем вызова соответствующей функции, добавленной в window (например, renderReactSidebar), прямо из директивы. В этот момент, в качестве аргументов функции, передается контейнер (DOM элемент), в который должен быть встроен React компонент, а также необходимые props и любые глобальные настройки.

// sidebar-d.js

(function () {
    'use strict';

		// # 1
    angular.module('front').directive('frontSidebar', sidebar);

    /* @ngInject */
    function sidebar() {
        return {
            controller: sidebarCtrl,
            templateUrl: 'scripts/components/sidebar/sidebar-d.html',
        };
    }

    /* @ngInject */
    async function sidebarCtrl(
        $scope,
        $state,
        DynamicService,
    ) {
		// #2
        const globalAngularProps = {
            goToRoute: (state, payload) => {
                $state.go(state, payload, { reload: false });
            },
        };

		// #3
        const sidebarNode = document.getElementById('front-sidebar');
        // #4
        const { observer, unmount } = await window.renderReactSidebar(sidebarNode, globalAngularProps, {
            someProp: 'initial state'
        });

		// #5
        DynamicService.subscribe().then(function (count) {
              observer.updateProps({ someProp: 'updated state' });
        });

		// #6
        $scope.$on('$destroy', () => {
            if (unmount) {
                unmount();
            }
        })
    }
})(); 

Описание процесса интеграции React компонента в Angular приложение через директиву включает следующие шаги:

  1. Регистрация Angular директивы: Создается и регистрируется Angular директива, позволяющая использовать React компоненты как Angular компоненты. Это делается для упрощения вставки React компонента в Angular шаблоны, например, через использование в Angular приложении.

  2. Объявление глобальных значений: В Angular можно объявить и передать глобальные значения, такие как настройки роутинга, в React компонент. Эти значения могут быть впоследствии использованы в React Context для предоставления данных React компонентам.

  3. Поиск контейнера: В директиве осуществляется поиск контейнера по идентификатору, в который будет вставлен React компонент. Этот контейнер предварительно определен в шаблоне Angular.

  4. Инициализация отрисовки React компонента: С помощью ранее объявленных функций отрисовки, таких как renderReactSidebar, осуществляется отрисовка React компонента в найденном контейнере, при этом возвращается observer для управления props и функция unmount для удаления информации о компоненте из памяти (Virtual DOM).

  5. Взаимодействие с Angular сервисами: Директива может подписываться на Angular сервисы и использовать observer для обновления props React компонента при изменении данных, получаемых из сервиса.

  6. Управление жизненным циклом: При уничтожении директивы Angular, например, при переходе на другую страницу, необходимо вызвать функцию unmount для React компонента, чтобы корректно очистить Virtual DOM и предотвратить утечки памяти.

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

// routes.js

.state('dashboard.test', {
    url: '/test',
    template: '',
})

Настройка роутинга между Angular и React действительно может быть непростой задачей, особенно когда используются ui-router для Angular и react-router-dom для React. Оба роутера стремятся контролировать изменения состояния и навигацию в приложении, что может привести к конфликтам, если они не настроены должным образом для совместной работы.

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

React

Свежий React не смотрит на тебя

Свежий React не смотрит на тебя

Переходя к работе на стороне React, мы используем скрипт angularIntegration.js как точку входа для нашего приложения. Этот скрипт служит ключевым элементом для интеграции React компонентов в Angular приложение, позволяя осуществлять точечную вставку React элементов непосредственно из Angular.

// angularIntegration.ts

import { renderPage, renderSidebar } from './index';
import { GlobalPropsFromAngular } from './angularContext';

export default function angularIntegration(
    container: HTMLElement | null,
    content: 'page' | 'sidebar',
    globalPropsFromAngular: GlobalPropsFromAngular,
    props: Record,
) {
    switch (content) {
        case 'page':
            return renderPage(container, globalPropsFromAngular);
        case 'sidebar':
            return renderSidebar(container, globalPropsFromAngular, props as any);
    }
}

Функция angularIntegration, экспортируемая из React-приложения, была предварительно объявлена для плагина Module Federation. Она задействуется для инъекции React-дерева в указанный контейнер Angular-приложения, определяя, какие конкретно компоненты будут отображаться.

Любая из функций отрисовки похожа по своей сути. Посмотрим детальный путь отрисовки sidebar:

// index.tsx

// #1
export const renderSidebar = (
    container: HTMLElement | null,
    globalAngularProps: GlobalPropsFromAngular,
    props: SideMenuBarProps,
) => {
	// #2
    const { Component, observer } = withPropsObserver(SideMenuBar, props);

	// #3
    const unmount = render(container, globalAngularProps, , 'sidebar');

	// #4
    return { observer, unmount };
};

function withPropsObserver(Component: FC, initialProps: T) {
    let lazyUpdatedProps: T | null = null;

	// #2.a
    const observer: {
        updateProps: (props: T) => void;
    } = {
        updateProps: (props) => {
            lazyUpdatedProps = lazyUpdatedProps ? { ...lazyUpdatedProps, ...props } : props;
        },
    };

	// #2.b
    const Wrapper: FC = () => {
        const [props, setProps] = useState(initialProps);

		// #2.c
        useEffect(() => {
            if (lazyUpdatedProps) {
                setProps(lazyUpdatedProps);
                lazyUpdatedProps = null;
            }

            observer.updateProps = (newProps) => {
                setProps((prev) => ({ ...prev, ...newProps }));
            };
        }, []);

        return ;
    };

    return {
        Component: Wrapper,
        observer,
    };
}

const render = (
    container: HTMLElement | null,
    globalAngularProps: GlobalPropsFromAngular,
    content: ReactNode,
    identifierPrefix: string,
) => {
    if (!container) {
        throw Error('Root element not found!');
    }

	// #3.a
    const root = ReactDOM.createRoot(container, {
        identifierPrefix,
    });

	// #3.b
    root.render(
		{content},
    );

	// #3.c
    return () => root.unmount();
};
  1. Функция renderSidebar инициирует процесс рендеринга боковой панели, представленной React компонентом.

  2. Мы хотим иметь возможность прокидывать props в наш компонент, поэтому мы оборачиваем его в withPropsObserver. Здесь используется паттерн Higher-Order Component:

    1. Создается объект observer с функцией updateProps. Эта функция позволяет обновлять props компонента из Angular. Вначале, updateProps является заглушкой для сбора lazyUpdatedProps — пропсов, которые были обновлены до инициализации компонента.

    2. Определение Wrapper — обертки, которая расширяет компонент дополнительными возможностями для хранения и управления пропсами, полученными из Angular.

    3. При монтировании компонента, Wrapper использует lazyUpdatedProps для инициализации локального состояния и обновляет observer.updateProps, чтобы эта функция могла изменять состояние компонента.

  3. Теперь мы начинаем отрисовывать компонент, для этого вызываем функцию render

    1. С помощью ReactDOM.createRoot, компонент внедряется в предоставленный контейнер Angular приложения, создавая новое React дерево.

    2. AngularContextProvider включается для предоставления доступа к глобальным данным и обработчикам Angular, например, для поддержки роутинга.

    3. Функция unmount возвращается вместе с результатом рендеринга, чтобы позволить последующее удаление Virtual DOM.

  4. После успешного рендеринга, объект observer и функция unmount передаются обратно в Angular приложение. Это дает Angular возможность управлять состоянием и жизненным циклом React компонента, а также деревом компонентов.

Dev сборка

Для разработки выбираем webpack-dev-server из-за его преимущества в Hot Module Replacement по сравнению с Gulp, что ускоряет работу с React кодом. Angular статический файл служит шаблоном index.html для webpack, а функции рендера React указываем в window через index.tsx, упрощая интеграцию и избегая использование react-integration.js.

// index.tsx

if (process.env.NODE_ENV === 'development') {
    window.renderReactPage = (container, globalAngularProps) => {
        return renderPage(container, globalAngularProps);
    };

    window.renderReactSidebar = (container, globalAngularProps, props) => {
        return renderSidebar(container, globalAngularProps, props);
    };
}

Заключение

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

При интересе к теме, я готов поделиться решениями часто встречающихся задач, таких как настройка роутинга, авторизации и использование фича флагов. Желаю вам приятного дня!

Мир! Дружба! Жвачка!

Мир! Дружба! Жвачка!

© Habrahabr.ru