58. Микрофронтенды
61. Микрофронтенды 🏗️
Заголовок раздела «61. Микрофронтенды 🏗️»Привет! Яша здесь. Микрофронтенды — это микросервисы для фронтенда. Каждая команда владеет своим куском интерфейса независимо. Сегодня разберём 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: концепция
Заголовок раздела «Module Federation: концепция»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']Native Federation с @angular-architects/native-federation
Заголовок раздела «Native Federation с @angular-architects/native-federation»Современный подход без Webpack — работает с esbuild:
npm install @angular-architects/native-federationng 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 remoteconst { 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
Заголовок раздела «Маршрутизация в Shell»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), },];Общие зависимости (Shared Dependencies)
Заголовок раздела «Общие зависимости (Shared Dependencies)»// Критическая настройка: 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', }, },});Module Federation (Webpack): классический подход
Заголовок раздела «Module Federation (Webpack): классический подход»npm install @angular-architects/module-federationng add @angular-architects/module-federation --project=shell --type=hostng add @angular-architects/module-federation --project=catalog --type=remote --port=4202// webpack.config.js — shellconst 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 tokenexport const USER_CONTEXT = new InjectionToken<UserContext>('USER_CONTEXT');// Shell передаёт контекст в remotesconst routes: Routes = [ { path: 'catalog', providers: [ { provide: USER_CONTEXT, useFactory: (authService: AuthService) => authService.currentUser, deps: [AuthService], } ], loadChildren: () => loadRemoteModule('catalog', './routes'), }];Shell Service: коммуникация между remotes
Заголовок раздела «Shell Service: коммуникация между remotes»// shell/src/app/shell.service.ts — шина событий для MFimport { 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));Shadow DOM для изоляции стилей
Заголовок раздела «Shadow DOM для изоляции стилей»// В remote компонентах — Shadow DOM предотвращает утечку стилей@Component({ selector: 'catalog-app', encapsulation: ViewEncapsulation.ShadowDom, template: `<router-outlet></router-outlet>`, styles: [`:host { display: block; }`],})export class CatalogShellComponent {}Динамические remotes: конфигурация в runtime
Заголовок раздела «Динамические remotes: конфигурация в runtime»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!); }}Playground 🎮
Заголовок раздела «Playground 🎮»Симулятор shell + remote архитектуры микрофронтендов: