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

Скрин

В сборном проекте:

  1. В файле 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)
);

  1. В файле module-federation.config.js указываем названия удалённых проектов:

Скрин

Код
module.exports = {
  name: 'main',
  remotes: ['org', 'name'],
};

  1. В файле src/remotes.d.ts декларируем новые модули:

Скрин

Код
// Declare your remote Modules here
// Example declare module 'about/Module';

declare module 'org/Module';
declare module 'name/Module';

  1. В файле 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):

  1. Добавляем файл module-federation.config.js:

Скрин

Код
module.exports = {
  name: 'org',
  exposes: {
    './Module': './src/remote-entry.ts',
  },
};

  1. Соответственно, добавляем файл src/remote-entry.ts:

Скрин

Код
export { default } from './app/app';

  1. Обновляем 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)
);

  1. Добавляем продовский конфиг webpack.config.prod.js:

Скрин

Код
module.exports = require('./webpack.config');

  1. Одно из самых неочевидных действий. Если вы посмотрите на проект main, то вы увидите, что входным файлом является main.ts, в котором находится импорт из файла bootstrap.tsx:

Скрин

В то время, как в проекте org входным файлом является main.tsx, в котором и находится разметка:

Скрин

Так вот необходимо микрофронты привести к тому же виду, что и хост. То есть, создаём файл bootstrap.tsx, в него переносим разметку, переименовываем main.tsx в main.ts и делаем импорт. Если этого не сделать, проект не взлетит и будет ошибка Uncaught Error: Shared module is not available for eager consumption (подробнее об этом здесь).

  1. Нужно обновить файл 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)


  1. nin-jin
    04.07.2023 07:46
    -1

    Ух, целая статья для того, что в $mol делается в 3 строчки..

    $my_portal $mol_book2_catalog
    	menu_title \My Portal
    	spreads *
    		excel <= Excel $hyoo_calc
    			title \My Excel
    		word <= Word $hyoo_page
    			menu_title \My Word
    		powerpoint <= Powerpoint $hyoo_page
    			menu_title \My PowerPoint
    		outlook <= Outlook $hyoo_mail
    			menu_title \My Outlook

    Тут мы взяли 3 приложения и объединили их в один портал:


    1. Solant
      04.07.2023 07:46
      +2

      Выглядит как немного неуместная реклама. Статья описывает использование module federation для объединения сборок разных библиотек в одно приложение, а в Вашем примере используются обычные компоненты одного фреймворка в одном приложении.


  1. Pecheneg2015
    04.07.2023 07:46
    +1

    Ещё стоит отметить,что для angular из коробки доступна динамическая подгрузка mfe, а для react нет,но есть 2 варианта решения:

    1. Использование готового решения на основе подхода для angular

    2. Реализовать свой вариант. Тут будет полезно почитать доку Webpack по этой теме


    1. ko22012
      04.07.2023 07:46
      +2

      Мы тоже динамическую подгрузку от webpack взяли.

      Он состоит из двух этапов:

      1. Загрузить js файл микрофронта, т.е. просто создать компонент динамической загрузки js файла.

      2. Подгрузить модуль, воспользовшись инструкцией по вашей ссылки. Мы взяли function loadComponent.

        Статистическая загрузка тоже возможно в реакте, но тогда все микрофронты будут загружаться при первом посещении.

        Сами разработчики nx советую использовать динамическую подгрузку модулей.


      1. Pecheneg2015
        04.07.2023 07:46
        +1

        Руслан, вы правы. Никому не хочется тащить кучу remoteEntry, которые могут и не пригодиться вовсе.