Мікрофронтенди Angular з використанням Федерації модулів

Динамічний 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

Leave a Reply

Your email address will not be published. Required fields are marked *