Если вы, как и я, заинтересовались микрофронтами и пробуете развернуть проект на 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!
