Динамическая федерация модулей с помощью Angular


Английский перевод оригинальной статьи Манфреда Штайера «Dynamic Module Federation with Angular» обновлен 09-06-2022

В предыдущей статье этого цикла я показал, как использовать Webpack Module Federation для загрузки отдельно скомпилированных фронтендов Micro в оболочку. Поскольку конфигурация webpack оболочки описывает уже определенные Micro Frontends.

В этой статье я предполагаю более динамичную ситуацию, когда оболочка не знает Micro Frontend заранее. Вместо этого, эта информация предоставляется во время выполнения программы через конфигурационный файл. Хотя в приведенных здесь примерах этот файл представляет собой статический JSON-файл, его содержимое также может поступать из Web API.

Важно: Эта статья написана для Angular и Angular CLI 14 или выше.

На следующем изображении показана идея, описанная в этой статье:

Это пример настройки Micro Frontends, которые оболочка должна найти во время выполнения, они отображаются в меню, и когда вы нажимаете на них, они загружаются и отображаются маршрутизатором оболочки.

📂 Исходный код (простая версия, ветка: simple)

📂 Исходный код (полная версия)

Динамичный и простой

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

Добавление модульной федерации

Демо-проект, который мы использовали, содержит оболочку и два Micro Frontends под названиями mfe1 и mfe2. Как и в предыдущей статье, мы добавили и инициализировали плагин Module Federation для Micro Frontends:

npm i -g @angular-architects/module-federation -D

ng g @angular-architects/module-federation --project mfe1 --port 4201 --type remote

ng g @angular-architects/module-federation --project mfe2 --port 4202 --type remote
Войдите в полноэкранный режим Выход из полноэкранного режима

Генерация манифеста

Начиная с версии 14.3 плагина, мы можем генерировать динамический хост, который получает основные данные о Micro Frontend из json-файла.

ng g @angular-architects/module-federation --project shell --port 4200 --type dynamic-host
Войдите в полноэкранный режим Выход из полноэкранного режима

Это генерирует конфигурацию webpack, манифест и добавляет код в main.ts для загрузки манифеста, найденного в projects/shell/src/assets/mf.manifest.json.

В манифесте содержится следующее определение:

{
    "mfe1": "http://localhost:4201/remoteEntry.js",
    "mfe2": "http://localhost:4202/remoteEntry.js"
}
Войдите в полноэкранный режим Выход из полноэкранного режима

После создания манифеста важно убедиться, что порты совпадают.

Загрузка манифеста

Сгенерированный файл main.ts загружает манифест:

import { loadManifest } from '@angular-architects/module-federation';

loadManifest("/assets/mf.manifest.json")
  .catch(err => console.error(err))
  .then(_ => import('./bootstrap'))
  .catch(err => console.error(err));
Войдите в полноэкранный режим Выход из полноэкранного режима

По умолчанию loadManifest загружает не только манифест, но и удаленные записи, на которые указывает манифест. Таким образом, Module Federation получает все необходимые метаданные для получения Micro Frontends по запросу.

Загрузка микрофронтендов

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

export const APP_ROUTES: Routes = [
    {
      path: '',
      component: HomeComponent,
      pathMatch: 'full'
    },
    {
      path: 'flights',
      loadChildren: () => loadRemoteModule({
          type: 'manifest',
          remoteName: 'mfe1',
          exposedModule: './Module'
        })
        .then(m => m.FlightsModule)
    },
    {
      path: 'bookings',
      loadChildren: () => loadRemoteModule({
          type: 'manifest',
          remoteName: 'mfe2',
          exposedModule: './Module'
        })
        .then(m => m.BookingsModule)
    },
];
Войдите в полноэкранный режим Выход из полноэкранного режима

Опция type: 'manifest' заставляет loadRemoteModule искать необходимые ключевые данные в загруженном манифесте, а свойство remoteName указывает на ключ, который был использован в манифесте.

Конфигурация микрофронтендов

Мы ожидаем, что оба Micro Frontends будут предоставлять NgModule с подпутями через './Module'. NgModules раскрываются через webpack.config.js в Micro Frontends:

// projects/mfe1/webpack.config.js

const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({

  name: 'mfe1',

  exposes: {
    // Adjusted line:
    './Module': './projects/mfe1/src/app/flights/flights.module.ts'
  },

  shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
  },

});
Войдите в полноэкранный режим Выход из полноэкранного режима
// projects/mfe2/webpack.config.js

const { shareAll, withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({

  name: 'mfe2',

  exposes: {
    // Adjusted line:
    './Module': './projects/mfe2/src/app/bookings/bookings.module.ts'
  },

  shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
  },

});
Войдите в полноэкранный режим Выход из полноэкранного режима

Создание навигации

Для каждого пути, который загружает Micro Frontend, AppComponent оболочки содержит routerLink:

<!-- projects/shell/src/app/app.component.html -->
<ul>
    <li><img src="../assets/angular.png" width="50"></li>
    <li><a routerLink="/">Home</a></li>
    <li><a routerLink="/flights">Flights</a></li>
    <li><a routerLink="/bookings">Bookings</a></li>
</ul>

<router-outlet></router-outlet>
Войдите в полноэкранный режим Выход из полноэкранного режима

Вот и все. Просто запустите все три проекта (например, с помощью npm run run:all). Главное отличие от результата предыдущей статьи в том, что теперь оболочка сама информирует себя о Micro Frontends во время выполнения. Если вы хотите направить оболочку на разные Micro Frontends, просто настройте манифест.

Настройка динамических маршрутов

Решение, которое мы имеем на сегодняшний день, является адекватным во многих ситуациях: Использование манифеста позволяет адаптировать его к различным средам без перестройки приложения. Кроме того, если мы изменим манифест на динамический REST-сервис, мы сможем реализовать такие стратегии, как A/B-тестирование.

Однако в некоторых ситуациях количество Micro Frontends может быть даже не известно заранее. Это то, что мы обсуждаем здесь.

Добавление пользовательских метаданных в манифест

Для динамической настройки маршрутов нам нужны некоторые дополнительные метаданные. Для этого вам может понадобиться расширить манифест:

{
    "mfe1": {
        "remoteEntry": "http://localhost:4201/remoteEntry.js",

        "exposedModule": "./Module",
        "displayName": "Flights",
        "routePath": "flights",
        "ngModuleName": "FlightsModule"
    },
    "mfe2": {
        "remoteEntry": "http://localhost:4202/remoteEntry.js",

        "exposedModule": "./Module",
        "displayName": "Bookings",
        "routePath": "bookings",
        "ngModuleName": "BookingsModule"
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Кроме remoteEntry, все остальные свойства являются пользовательскими.

Типы для расширенной конфигурации

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

// projects/shell/src/app/utils/config.ts

import { Manifest, RemoteConfig } from "@angular-architects/module-federation";

export type CustomRemoteConfig = RemoteConfig & {
    exposedModule: string;
    displayName: string;
    routePath: string;
    ngModuleName: string;
};

export type CustomManifest = Manifest<CustomRemoteConfig>;
Войдите в полноэкранный режим Выход из полноэкранного режима

Тип CustomRemoteConfig представляет записи манифеста, а тип CustomManifest — полный манифест.

Динамическое создание маршрутов

Теперь нам нужна функция, которая итеративно просматривает весь манифест и создает маршрут для каждого Micro Frontend, описанного в нем:

// projects/shell/src/app/utils/routes.ts

import { loadRemoteModule } from '@angular-architects/module-federation';
import { Routes } from '@angular/router';
import { APP_ROUTES } from '../app.routes';
import { CustomManifest } from './config';

export function buildRoutes(options: CustomManifest): Routes {

    const lazyRoutes: Routes = Object.keys(options).map(key => {
        const entry = options[key];
        return {
            path: entry.routePath,
            loadChildren: () => 
                loadRemoteModule({
                    type: 'manifest',
                    remoteName: key,
                    exposedModule: entry.exposedModule
                })
                .then(m => m[entry.ngModuleName])
        }
    });

    return [...APP_ROUTES, ...lazyRoutes];
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Это дает нам ту же структуру, которую мы установили непосредственно выше.

Оболочка AppComponent позаботится о том, чтобы связать все вместе:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit  {

  remotes: CustomRemoteConfig[] = [];

  constructor(
    private router: Router) {
  }

  async ngOnInit(): Promise<void> {
    const manifest = getManifest<CustomManifest>();

    // Hint: Move this to an APP_INITIALIZER 
    //  to avoid issues with deep linking
    const routes = buildRoutes(manifest);
    this.router.resetConfig(routes);

    this.remotes = Object.values(manifest);
  }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Метод ngOnInit получает доступ к загруженному манифесту (он все еще загружен в main.ts, как показано выше) и передает его функции buildRoutes. Полученные динамические маршруты передаются маршрутизатору, а значения пар ключ/значение в манифесте, помещенные в поле remotesm, используются в шаблоне для динамического создания пунктов меню:

<!-- projects/shell/src/app/app.component.html -->

<ul>
    <li><img src="../assets/angular.png" width="50"></li>
    <li><a routerLink="/">Home</a></li>

    <!-- Dynamically create menu items for all Micro Frontends -->
    <li *ngFor="let remote of remotes"><a [routerLink]="remote.routePath">{{remote.displayName}}</a></li>

    <li><a routerLink="/config">Config</a></li>
</ul>

<router-outlet></router-outlet>
Войдите в полноэкранный режим Выход из полноэкранного режима

Теперь давайте проверим это «динамическое» решение, запустив оболочку и Micro Frontends (например, с помощью npm run run:all).

Некоторые подробности

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

loadManifest(...): Функция loadManifest, использованная выше, предоставляет второй параметр под названием skipRemoteEntries. Установка этого значения в true позволяет избежать загрузки точек входа. В этом случае загружается только манифест:

loadManifest("/assets/mf.manifest.json", true)
    .catch(...)
    .then(...)
    .catch(...)
Войдите в полноэкранный режим Выход из полноэкранного режима

setManifest(...): Эта функция позволяет установить манифест напрямую. Это очень полезно, если вы загружаете данные из другого места.

loadRemoteEntry(...): Эта функция позволяет загрузить удаленную точку входа напрямую. Это полезно, если манифест не используется:

Promise.all([
    loadRemoteEntry({ type: 'module', remoteEntry: 'http://localhost:4201/remoteEntry.js' }),
    loadRemoteEntry({ type: 'module', remoteEntry: 'http://localhost:4202/remoteEntry.js' })
])
.catch(err => console.error(err))
.then(_ => import('./bootstrap'))
.catch(err => console.error(err));
Войдите в полноэкранный режим Выход из полноэкранного режима

LoadRemoteModule(...): Если вы не хотите использовать манифест, вы можете загрузить Micro Frontend напрямую с помощью loadRemoteModule:

{
    path: 'flights',
    loadChildren: () =>
        loadRemoteModule({
            type: 'module',
            remoteEntry: 'http://localhost:4201/remoteEntry.js',
            exposedModule: './Module',
        }).then((m) => m.FlightsModule),
},
Войдите в полноэкранный режим Выход из полноэкранного режима

В целом, я думаю, что большинство людей будут использовать манифест в будущем. Даже если вы не хотите загружать его из файла JSON с помощью loadManifest, вы можете установить его с помощью setManifest.

Свойство type:'module' определяет, что вы хотите загрузить «настоящий» модуль EcmaScript, а не «просто» файл JavaScript. Это требуется с Angular CLI 13. Если вы загружаете не созданный материал, вам, скорее всего, придется установить это свойство в script. Это также может произойти через манифест:

{
    "non-cli-13-stuff": {
        "type": "script",
        "remoteEntry": "http://localhost:4201/remoteEntry.js"
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Если запись манифеста не содержит свойства type, плагин принимает значение module.

Заключение

Использование динамической федерации модулей обеспечивает большую гибкость, поскольку позволяет загружать микрофронтенды, которые мы не должны знать во время компиляции. Нам даже не нужно заранее знать их номер. Это возможно благодаря API времени выполнения, предоставляемому webpack. Чтобы немного упростить его использование, плагин @angular-architects/module-federation красиво обертывает его для упрощения нашей работы.

Фото Polina Sushko on Unsplash

Оцените статью
devanswers.ru
Добавить комментарий