Английский перевод оригинальной статьи Манфреда Штайера «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