Динамічний Module Federation — це техніка, яка дозволяє додатку динамічно визначати місця розташування своїх віддалених додатків під час виконання. Цей підхід підтримує концепцію "Build once, deploy everywhere".
Концепція "Build once, deploy everywhere" полягає в створенні єдиного артефакту збірки для вашого додатку, який можна розгортати в різних середовищах, таких як staging і production.
Мета:
- Налаштування Micro Frontends за допомогою Module Federation
Ми зробимо наступне:
- Створимо статичну архітектуру Federation для Micro Frontend
- Змінемо додаток Dashboard для використання динамічного Federation
- Він повинен використовувати існуючий додаток Login.
(для нашого проекту ми будемо використовувати npm/npx)
Перші кроки:
Для початку нам потрібно створити новий Nx Workspace і додати плагін Nx Angular. Ми можемо зробити це легко за допомогою:
npx create-nx-workspace@latest ng-mf — preset=apps
Далі виконуємо:
cd ng-mf
npx nx add @nx/angular
Створення наших додатків
Нам потрібно створити два додатки, які підтримують Module Federation.
Почнемо з додатку Admin Dashboard, який буде виступати як хост-додаток для Micro-Frontends (MFEs):
nx g @nx/angular:host apps/dashboard — prefix=ng-mf
Запуск команд nx:npx nx ...
Генератор host
створить і змінить файли, необхідні для налаштування Angular додатку.
Тепер давайте створимо додаток Login як віддалений додаток, який буде споживатися хост-додатком Dashboard.
nx g @nx/angular:remote apps/login — prefix=ng-mf — host=dashboard
Зверніть увагу, як ми вказали опцію --host=dashboard
. Це вказує генератору, що цей віддалений додаток буде споживатися додатком Dashboard. Генератор виконає наступні зміни для автоматичного зв'язку цих двох додатків:
- Додав віддалений додаток у файл
apps/dashboard/module-federation.config.ts
- Додав типове відображення шляху TypeScript у кореневий файл tsconfig
- Додав новий маршрут у файл
apps/dashboard/src/app/app.routes.ts
Для обох додатків генератори виконали наступні дії:
- Створено стандартні файли додатка Angular
- Додано файл
module-federation.config.ts
- Додано файли
webpack.config.ts
іwebpack.prod.config.ts
- Додано файл
src/bootstrap.ts
- Переміщено код, який зазвичай знаходиться в
src/main.ts
, вsrc/bootstrap.ts
- Змінено
src/main.ts
, щоб динамічно імпортуватиsrc/bootstrap.ts
(це необхідно для правильного завантаження версій спільних бібліотек у Module Federation) - Оновлено ціль
build
уproject.json
, щоб використовувати екзекутор@nx/angular:webpack-browser
(це необхідно для підтримки передачі налаштувань кастомної конфігурації Webpack Angular компілятору) - Оновлено ціль
serve
, щоб використовувати@nx/angular:dev-server
(це необхідно, оскільки спочатку нам потрібно, щоб Webpack зібрав додаток за допомогою нашої кастомної конфігурації Webpack)
Ключові відмінності полягають у налаштуваннях плагіна Module Federation у кожному додатку в файлі module-federation.config.ts
.
Ми можемо побачити наступне в конфігурації Login micro frontend:
//apps/login/module-federation.config.ts
import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'login',
exposes: {
'./Routes': 'apps/login/src/app/remote-entry/entry.routes.ts',
},
};
export default config;
Розглянемо кожну властивість конфігурації:
name
— це ім'я, яке Webpack присвоює віддаленому додатку.
Він повинен відповідати імені проекту.exposes
— це список вихідних файлів, які віддалений додаток надає для використання споживаючими хост-додатками.
Ця конфігурація потім використовується у файлі webpack.config.ts
:
//apps/login/webpack.config.ts
import { withModuleFederation } from '@nx/angular/module-federation';
import config from './module-federation.config';
export default withModuleFederation(config, { dts: false });
Ми можемо побачити наступне в конфігурації Dashboard micro frontend:
//apps/dashboard/module-federation.config.ts
import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'dashboard',
remotes: ['login'],
};
export default config;
Ключова відмінність, яку варто відзначити в конфігурації Dashboard, — це масив remotes
. Тут ви вказуєте віддалені додатки, які хочете використовувати у вашому хост-додатку.
Ви надаєте ім'я, яке можна використовувати в коді, в даному випадку login
. Nx знайде, де він обслуговується.
Тепер, коли ми створили наші додатки, давайте перейдемо до створення функціоналу для кожного.
Додавання функціоналу
Ми почнемо з побудови додатку Login, який складатиметься з форми входу та деякої дуже базової і небезпечної логіки авторизації.
Бібліотека користувачів
Давайте створимо бібліотеку доступу до даних користувачів, яка буде спільною між хост-додатком і віддаленим додатком. Вона буде використовуватися для перевірки, чи є автентифікований користувач, а також для надання логіки для авторизації користувача.
nx g @nx/angular:lib libs/shared/data-access-user
Це створить нову бібліотеку для нашого використання.
Нам потрібен Angular Service, який ми будемо використовувати для зберігання стану:
nx g @nx/angular:service libs/shared/data-access-user/src/lib/user
Це створить файл libs/shared/data-access-user/src/lib/user.service.ts
. Змініть його вміст на такий:
//libs/shared/data-access-user/src/lib/user.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
private isUserLoggedIn = new BehaviorSubject(false);
isUserLoggedIn$ = this.isUserLoggedIn.asObservable();
checkCredentials(username: string, password: string) {
if (username === 'demo' && password === 'demo') {
this.isUserLoggedIn.next(true);
}
}
logout() {
this.isUserLoggedIn.next(false);
}
}
Тепер експортуйте сервіс у файлі точки входу бібліотеки:
//libs/shared/data-access-user/src/index.ts
export * from './lib/user.service';
Додаток Login
Налаштуємо файл entry.component.ts
у додатку Login, щоб він рендерив форму входу.
Ми імпортуємо FormsModule
і впроваджуємо наш UserService
, щоб дозволити користувачу увійти:
//apps/login/src/app/remote-entry/entry.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { UserService } from '@ng-mf/data-access-user';
import { inject } from '@angular/core';
@Component({
standalone: true,
imports: [CommonModule, FormsModule],
selector: 'ng-mf-login-entry',
template: `
Username: Password: Login
User is logged in!
`, styles: [ ` .login-app { width: 30vw; border: 2px dashed black; padding: 8px; margin: 0 auto; } .login-form { display: flex; align-items: center; flex-direction: column; margin: 0 auto; padding: 8px; } label { display: block; } `, ], }) export class RemoteEntryComponent { private userService = inject(UserService); username = ''; password = ''; isLoggedIn$ = this.userService.isUserLoggedIn$; login() { this.userService.checkCredentials(this.username, this.password); } } ```
Тепер давайте запустимо додаток і переглянемо його в браузері, щоб перевірити, чи коректно відображається форма.
> nx run login:serve
Ми можемо побачити, що якщо ми перейдемо в браузер за адресою `http://localhost:4201`, то форма входу відобразиться. Якщо ввести правильні ім’я користувача та пароль _(demo, demo)_, ми також побачимо, що користувач був автентифікований! Чудово! Наш додаток для входу готовий.
## Додаток Dashboard
Тепер давайте оновимо наш додаток Dashboard. Ми сховаємо деякий контент, якщо користувач не автентифікований, і покажемо йому додаток для входу, де він зможе увійти.
Щоб це працювало, стан у `UserService` повинен бути спільним для обох додатків. Зазвичай, при використанні Module Federation у Webpack, потрібно вказати пакети, які слід поділити між усіма додатками в вашій архітектурі Micro Frontend. Однак, завдяки використанню графу проектів від Nx, Nx автоматично знаходить і ділиться залежностями між вашими додатками.
**Наступним кроком** давайте додамо логіку до файлу `app.component.ts`. Змініть його, щоб він виглядав так:
//apps/dashboard/src/app/app.component.ts
import { CommonModule } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { UserService } from '@ng-mf/data-access-user';
import { distinctUntilChanged } from 'rxjs/operators';
@Component({
standalone: true,
imports: [CommonModule, RouterModule],
selector: 'ng-mf-root',
template:
,
Admin Dashboard
You are authenticated so you can see this content.
})
export class AppComponent implements OnInit {
private userService = inject(UserService);
isLoggedIn$ = this.userService.isUserLoggedIn$;
ngOnInit() {
this.isLoggedIn$.pipe(distinctUntilChanged()).subscribe((isLoggedIn) => {
if (!isLoggedIn) {
// Redirect to login page if not authenticated
this.router.navigate(['/login']);
}
});
}
constructor(private router: Router) {}
}
Ми імпортуємо FormsModule
і впроваджуємо наш UserService
, щоб дозволити користувачу увійти:
//apps/login/src/app/remote-entry/entry.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { UserService } from '@ng-mf/data-access-user';
import { inject } from '@angular/core';
@Component({
standalone: true,
imports: [CommonModule, FormsModule],
selector: 'ng-mf-login-entry',
template: `
Username: Password: Login
User is logged in!
`, styles: [ ` .login-app { width: 30vw; border: 2px dashed black; padding: 8px; margin: 0 auto; } .login-form { display: flex; align-items: center; flex-direction: column; margin: 0 auto; padding: 8px; } label { display: block; } `, ], }) export class RemoteEntryComponent { private userService = inject(UserService); username = ''; password = ''; isLoggedIn$ = this.userService.isUserLoggedIn$; login() { this.userService.checkCredentials(this.username, this.password); } } ```
Тепер давайте запустимо додаток і переглянемо його в браузері, щоб перевірити, чи коректно відображається форма.
> nx run login:serve
Ми можемо побачити, що якщо ми перейдемо в браузер за адресою `http://localhost:4201`, то форма входу відобразиться. Якщо ввести правильні ім’я користувача та пароль _(demo, demo)_, ми також побачимо, що користувач був автентифікований! Чудово! Наш додаток для входу готовий.
## Додаток Dashboard
Тепер давайте оновимо наш додаток Dashboard. Ми сховаємо деякий контент, якщо користувач не автентифікований, і покажемо йому додаток для входу, де він зможе увійти.
Щоб це працювало, стан у `UserService` повинен бути спільним для обох додатків. Зазвичай, при використанні Module Federation у Webpack, потрібно вказати пакети, які слід поділити між усіма додатками в вашій архітектурі Micro Frontend. Однак, завдяки використанню графу проектів від Nx, Nx автоматично знаходить і ділиться залежностями між вашими додатками.
**Наступним кроком** давайте додамо логіку до файлу `app.component.ts`. Змініть його, щоб він виглядав так:
//apps/dashboard/src/app/app.component.ts
import { CommonModule } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { UserService } from '@ng-mf/data-access-user';
import { distinctUntilChanged } from 'rxjs/operators';
@Component({
standalone: true,
imports: [CommonModule, RouterModule],
selector: 'ng-mf-root',
template:
,
Admin Dashboard
You are authenticated so you can see this content.
})
export class AppComponent implements OnInit {
private userService = inject(UserService);
isLoggedIn$ = this.userService.isUserLoggedIn$;
ngOnInit() {
this.isLoggedIn$.pipe(distinctUntilChanged()).subscribe((isLoggedIn) => {
if (!isLoggedIn) {
// Redirect to login page if not authenticated
this.router.navigate(['/login']);
}
});
}
constructor(private router: Router) {}
}
Він має виглядати ось так:
//apps/dashboard/module-federation.config.ts
import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: 'dashboard',
remotes: [],
};
export default config;
Наступним кроком нам потрібно змінити, як наша програма намагається завантажити Remote, коли вона буде маршрутизована. Відкрийте файл app.routes.ts
у папці src/app/
і застосуйте наступні зміни:
//apps/dashboard/src/app/app.routes.ts
import { Route } from '@angular/router';
import { loadRemoteModule } from '@nx/angular/mf';
import { AppComponent } from './app.component';
export const appRoutes: Route[] = [
{
path: 'login',
loadChildren: () =>
loadRemoteModule('login', './Routes').then((m) => m.remoteRoutes),
},
{
path: '',
component: AppComponent,
},
];
Допоміжний метод loadRemoteModule
просто приховує деяку логіку, яка перевіряє, чи було завантажено Remote-додаток, і якщо ні, завантажує його, а потім запитує правильні маршрути, що були надані.
Резюме
Це всі зміни, необхідні для заміни Static Module Federation на Dynamic Module Federation.
nx serve dashboard — devRemotes=login
Тепер програма повинна вести себе так само, як і раніше, за винятком того, що наш додаток Dashboard чекає до часу виконання, щоб дізнатися місце розташування розгорнутого додатку Login.
Перекладено з: Angular micro front-ends with Module federation