Объединение микрофронтов на Nx в один проект
- вторник, 4 июля 2023 г. в 00:00:13
Если вы, как и я, заинтересовались микрофронтами и пробуете развернуть проект на 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!