Если вы, как и я, заинтересовались микрофронтами и пробуете развернуть проект на Nx, то возможно, у вас встанет вопрос, как в итоге объединить несколько своих микрофронтов в общий проект. По крайней мере, те статьи, которые я находил по этой теме, рассказывали про то, как создать в Nx несколько проектов (в т.ч. на разных фреймворках), как создать к ним компоненты и либы, и на этом всё заканчивалось. Разобравшись, решил оставить инструкцию для других.
Вводная информация
Структуру Nx и базовый принцип работы трогать не будем. Предполагается, что вы уже с этим знакомы;
Для сборки используем Webpack;
Для того, чтобы объединить несколько проектов в один, мы используем плагин Module Federation в вебпаке. Он позволяет объединять несколько разных сборок. В случае с Nx, есть два варианта настройки: простой и посложнее. Простой подойдёт в том случае, если вы только начали и ещё не успели создать свои микрофронты. В этом случае мы сразу создадим и отдельные микрофронты, и итоговое сборное приложение. Если вы уже успели создать какие-то приложения, то подойдёт вариант посложнее: мы создадим общее приложение и настроим конфиги вручную.
З.Ы. Есть ещё вариант "Самый сложный", это когда мы не создаём специально общее приложение, а настраиваем его из уже существующего, но этот вариант мы сегодня не будем рассматривать.
Простой вариант
В терминале заходим в наш монорепозиторий и запускаем команду:nx g @nx/react:host main --remotes=name,name2
, где
nx/react
- это модуль, с помощью которого мы создаём итоговое приложение (в данном случае, внезапно, на реакте, могут быть варианты@nx/angular
,@nx/js
и т.д.,main
- название итогового проекта,name,name2
- названия ваших микрофронтовых проектов.
Простота варианта заключается как раз во флаге --remotes
. Мы можем сразу создать все проекты, которые нам нужны, и автоматически все связи будут настроены, у нас будет возможность запустить как один конкретный проект командой nx serve name
, так и общую сборку командойnx serve main
(автоматически запустит и все связанные проекты тоже).
Вариант посложнее
Если у нас уже есть проекты и нам нужен только хост, то запускаем команду nx g @nx/react:host main
. Дальше нам нужно в общем проекте и в каждом микрофронте сделать некоторые настройки.
Для начала исходная точка: у нас монорепозиторий org
, в нём два микрофронта (org
и name
) и хост (то есть, итоговый сборный проект) main
.
Скрин
В сборном проекте:
В файле
webpack.config.prod.js
нам нужно указать наши удалённые проекты, которые будем подтягивать:
Скрин
Код
const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederation } = require('@nx/react/module-federation');
const baseConfig = require('./module-federation.config');
const prodConfig = {
...baseConfig,
remotes: [
['org', 'http://localhost:4201/'],
['name', 'http://localhost:4202/'],
],
};
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(
withNx(),
withReact(),
withModuleFederation(prodConfig)
);
В файле
module-federation.config.js
указываем названия удалённых проектов:
Скрин
Код
module.exports = {
name: 'main',
remotes: ['org', 'name'],
};
В файле
src/remotes.d.ts
декларируем новые модули:
Скрин
Код
// Declare your remote Modules here
// Example declare module 'about/Module';
declare module 'org/Module';
declare module 'name/Module';
В файле
src/app/app.tsx
импортируем наши микрофронты и настраиваем роутинг так, как нам нужно:
Скрин
Код
import * as React from 'react';
import styles from './app.module.scss';
import { Link, Route, Routes } from 'react-router-dom';
const OrgPage = React.lazy(() => import('org/Module'));
const NamePage = React.lazy(() => import('name/Module'));
export function App() {
return (
<React.Suspense fallback={null}>
<main className={styles.content}>
<nav>
<ul className={styles.nav}>
<li>
<Link className={styles.navlink} to="/org">
Org
</Link>
</li>
<li>
<Link className={styles.navlink} to="/name">
Name
</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/org" element={<OrgPage />} />
<Route path="/name" element={<NamePage />} />
</Routes>
</main>
</React.Suspense>
);
}
export default App;
В проектах (на примере org
):
Добавляем файл
module-federation.config.js
:
Скрин
Код
module.exports = {
name: 'org',
exposes: {
'./Module': './src/remote-entry.ts',
},
};
Соответственно, добавляем файл
src/remote-entry.ts
:
Скрин
Код
export { default } from './app/app';
Обновляем
webpack.config.js
, добавляем настройку moduleFederation:
Скрин
Код
const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederation } = require('@nx/react/module-federation');
const baseConfig = require('./module-federation.config');
const config = {
...baseConfig,
};
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(
withNx(),
withReact(),
withModuleFederation(config)
);
Добавляем продовский конфиг
webpack.config.prod.js
:
Скрин
Код
module.exports = require('./webpack.config');
Одно из самых неочевидных действий. Если вы посмотрите на проект
main
, то вы увидите, что входным файлом являетсяmain.ts
, в котором находится импорт из файлаbootstrap.tsx
:
Скрин
В то время, как в проекте org
входным файлом является main.tsx
, в котором и находится разметка:
Скрин
Так вот необходимо микрофронты привести к тому же виду, что и хост. То есть, создаём файл bootstrap.tsx
, в него переносим разметку, переименовываем main.tsx
в main.ts
и делаем импорт. Если этого не сделать, проект не взлетит и будет ошибка Uncaught Error: Shared module is not available for eager consumption
(подробнее об этом здесь).
Нужно обновить файл
project.json
. Обновляем точку входа наmain.ts
:
Скрин
Добавляем ссылку на продовский конфиг ("webpackConfig": "apps/org/webpack.config.prod.js"
):
Скрин
Обновляем разделы serve и serve-static, в частности, прописываем порты. Если их не прописать, то каждый проект по отдельности запустить получится, а общую сборку - нет.
Хост по дефолту взял себе порт 4200
, поэтому на проекты мы ставим 4201
, 4202
и т.д.:
Скрин
Итоговый код конфига
{
"name": "org",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/org/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"compiler": "babel",
"outputPath": "dist/apps/org",
"index": "apps/org/src/index.html",
"baseHref": "/",
"main": "apps/org/src/main.ts",
"tsConfig": "apps/org/tsconfig.app.json",
"assets": ["apps/org/src/favicon.ico", "apps/org/src/assets"],
"styles": ["apps/org/src/styles.scss"],
"scripts": [],
"isolatedConfig": true,
"webpackConfig": "apps/org/webpack.config.js"
},
"configurations": {
"development": {
"extractLicenses": false,
"optimization": false,
"sourceMap": true,
"vendorChunk": true
},
"production": {
"fileReplacements": [
{
"replace": "apps/org/src/environments/environment.ts",
"with": "apps/org/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"webpackConfig": "apps/org/webpack.config.prod.js"
}
}
},
"serve": {
"executor": "@nx/react:module-federation-dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "org:build",
"hmr": true,
"port": 4201
},
"configurations": {
"development": {
"buildTarget": "org:build:development"
},
"production": {
"buildTarget": "org:build:production",
"hmr": false
}
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/org/**/*.{ts,tsx,js,jsx}"]
}
},
"serve-static": {
"executor": "@nx/web:file-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "org:build",
"port": 4201
},
"configurations": {
"development": {
"buildTarget": "org:build:development"
},
"production": {
"buildTarget": "org:build:production"
}
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/org/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
},
"tags": []
}
Всё готово. Запускаем хост командой nx serve main
, переходим на http://localhost:4200 - вы прекрасны!
Обращаю ваше внимание на то, что запуская хост, мы запускаем и все связанные проекты. Поэтому если вы перейдёте на http://localhost:4201, то увидите там наш проект org
.
Теперь вы можете запускать проекты как по отдельности (чтобы работать с одним конкретным проектом, не поднимая всё остальное), так и общую сборку.
Спасибо, что воспользовались услугами нашей авиакомпании, happy hacking!
Комментарии (5)
Pecheneg2015
04.07.2023 07:46+1Ещё стоит отметить,что для angular из коробки доступна динамическая подгрузка mfe, а для react нет,но есть 2 варианта решения:
Использование готового решения на основе подхода для angular
Реализовать свой вариант. Тут будет полезно почитать доку Webpack по этой теме
ko22012
04.07.2023 07:46+2Мы тоже динамическую подгрузку от webpack взяли.
Он состоит из двух этапов:
Загрузить js файл микрофронта, т.е. просто создать компонент динамической загрузки js файла.
-
Подгрузить модуль, воспользовшись инструкцией по вашей ссылки. Мы взяли function loadComponent.
Статистическая загрузка тоже возможно в реакте, но тогда все микрофронты будут загружаться при первом посещении.
Сами разработчики nx советую использовать динамическую подгрузку модулей.
Pecheneg2015
04.07.2023 07:46+1Руслан, вы правы. Никому не хочется тащить кучу remoteEntry, которые могут и не пригодиться вовсе.
nin-jin
Ух, целая статья для того, что в $mol делается в 3 строчки..
Тут мы взяли 3 приложения и объединили их в один портал:
Solant
Выглядит как немного неуместная реклама. Статья описывает использование module federation для объединения сборок разных библиотек в одно приложение, а в Вашем примере используются обычные компоненты одного фреймворка в одном приложении.