Перейти к содержимому

58. Микрофронтенды

Привет! Яша здесь. Микрофронтенды — это микросервисы для фронтенда. Каждая команда владеет своим куском интерфейса независимо. Сегодня разберём Module Federation и Native Federation — самые популярные подходы для Angular 🚀


Проблема: Монолитный Angular-проект на 50+ разработчиков:

  • Долгие сборки (15+ минут)
  • Частые merge-конфликты
  • Риск деплоя — один баг ломает всё
  • Трудно масштабировать команды

Решение — разбить на независимые приложения:

Shell (оркестратор) — загружает удалённые модули
├── Team A → header, nav (порт 4201)
├── Team B → catalog, product-card (порт 4202)
├── Team C → cart, checkout (порт 4203)
└── Team D → user-profile, settings (порт 4204)

Module Federation (Webpack 5) позволяет загружать JavaScript модули из других приложений в runtime:

Shell App (localhost:4200)
├── загружает → Team A Remote (localhost:4201)
│ exposes: ['./Header', './Nav']
└── загружает → Team B Remote (localhost:4202)
exposes: ['./CatalogPage', './ProductCard']

Современный подход без Webpack — работает с esbuild:

Окно терминала
npm install @angular-architects/native-federation
ng add @angular-architects/native-federation --project=shell --type=host
# В remote приложении
ng add @angular-architects/native-federation --project=catalog --type=remote --port=4202
// federation.config.js — shell (host)
const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
remotes: {
'catalog': 'http://localhost:4202/remoteEntry.json',
'cart': 'http://localhost:4203/remoteEntry.json',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});
// federation.config.js — catalog remote
const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'catalog',
exposes: {
'./routes': './src/app/catalog/catalog.routes.ts',
'./ProductCard': './src/app/catalog/product-card/product-card.component.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});

shell/src/app/app.routes.ts
import { loadRemoteModule } from '@angular-architects/native-federation';
export const routes: Routes = [
{
path: '',
component: HomeComponent,
},
{
path: 'catalog',
// Lazy loading удалённого модуля!
loadChildren: () =>
loadRemoteModule('catalog', './routes')
.then(m => m.CATALOG_ROUTES),
},
{
path: 'cart',
loadChildren: () =>
loadRemoteModule('cart', './routes')
.then(m => m.CART_ROUTES),
},
];

federation.config.js
// Критическая настройка: singleton dependencies
// Если Angular загрузится дважды — всё сломается!
module.exports = withNativeFederation({
shared: {
'@angular/core': {
singleton: true, // ← только одна копия!
strictVersion: true, // ← версии должны совпадать
requiredVersion: 'auto', // ← берётся из package.json
},
'@angular/router': {
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
},
'@ngrx/store': {
singleton: true,
strictVersion: false, // ← менее критично
requiredVersion: 'auto',
},
},
});

Окно терминала
npm install @angular-architects/module-federation
ng add @angular-architects/module-federation --project=shell --type=host
ng add @angular-architects/module-federation --project=catalog --type=remote --port=4202
// webpack.config.js — shell
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
remotes: {
catalog: 'catalog@http://localhost:4202/remoteEntry.js',
cart: 'cart@http://localhost:4203/remoteEntry.js',
},
shared: share({
'@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/common': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
'@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
}),
}),
],
};

// shared-contracts/src/index.ts — публичный API между командами
// Это библиотека в Nx монорепо, или отдельный npm пакет
export interface UserContext {
id: string;
name: string;
roles: string[];
}
export interface NavigationEvent {
type: 'navigate';
path: string;
}
// Каждый remote получает контекст через injection token
export const USER_CONTEXT = new InjectionToken<UserContext>('USER_CONTEXT');
// Shell передаёт контекст в remotes
const routes: Routes = [
{
path: 'catalog',
providers: [
{
provide: USER_CONTEXT,
useFactory: (authService: AuthService) => authService.currentUser,
deps: [AuthService],
}
],
loadChildren: () => loadRemoteModule('catalog', './routes'),
}
];

// shell/src/app/shell.service.ts — шина событий для MF
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
export interface MFEvent {
type: string;
source: string;
payload: unknown;
}
@Injectable({ providedIn: 'root' })
export class ShellService {
private eventBus = new Subject<MFEvent>();
emit(event: MFEvent): void {
this.eventBus.next(event);
}
on<T = unknown>(type: string): Observable<MFEvent & { payload: T }> {
return this.eventBus.pipe(
filter(e => e.type === type),
) as Observable<MFEvent & { payload: T }>;
}
}
// В catalog remote:
// this.shellService.emit({ type: 'cart:add', source: 'catalog', payload: { productId } });
// В cart remote:
// this.shellService.on('cart:add').subscribe(({ payload }) => this.addToCart(payload.productId));

// В remote компонентах — Shadow DOM предотвращает утечку стилей
@Component({
selector: 'catalog-app',
encapsulation: ViewEncapsulation.ShadowDom,
template: `<router-outlet></router-outlet>`,
styles: [`:host { display: block; }`],
})
export class CatalogShellComponent {}

shell/src/app/mf-config.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { initFederation } from '@angular-architects/native-federation';
@Injectable({ providedIn: 'root' })
export class MFConfigService {
async loadRemotes(): Promise<void> {
// Загружаем список remotes с сервера
const config = await this.http
.get<Record<string, string>>('/api/mf-config')
.toPromise();
// config = { catalog: 'https://catalog.example.com', cart: 'https://cart.example.com' }
await initFederation(config!);
}
}

Симулятор shell + remote архитектуры микрофронтендов: