Un micro frontend e o bucată de UI construită, testată și livrată independent de o echipă, apoi compusă cu celelalte la runtime, în browser. Cuvântul care contează e „independent”: dacă două părți trebuie livrate împreună, nu sunt micro frontends, ci module ale aceleiași aplicații — oricât de separate ar fi folderele.
Arhitectura rezolvă o problemă organizațională, nu una tehnică: mai multe echipe autonome care vor să livreze în ritmuri diferite fără să se blocheze unele pe altele. Prețul se plătește în complexitate la runtime: versiuni de dependențe, eșecuri de integrare, consistență vizuală, testare, observabilitate.
- Micro frontends rezolvă o problemă de organizare (echipe care livrează independent), nu una tehnică.
- Prețul e real: mai mult JavaScript, versiuni de dependențe de ținut în pas, teste end-to-end mai grele.
- Pentru Angular cu esbuild, varianta recomandată e Native Federation. Web Components și iframe-urile se potrivesc când amesteci framework-uri sau vrei izolare totală.
- Contractele dintre aplicații (rute, evenimente, versiuni) contează mai mult decât tehnologia. Fiecare aplicație încărcată de la distanță are nevoie de un fallback.
- Cu o singură echipă, un monorepo cu granițe clare e aproape mereu mai simplu.
Majoritatea aplicațiilor Angular, inclusiv cele mari, sunt mai bine servite de o singură aplicație într-un monorepo, cu granițe clare și lazy loading. Micro frontends sunt răspunsul corect pentru câteva situații precise și o sursă de probleme scumpe în rest. Pagina te ajută să le deosebești și, dacă e cazul, să le construiești corect cu Native Federation.
Ce este și ce nu este
Un sistem de micro frontends are trei proprietăți, toate obligatorii:
- Build separat — fiecare parte are propriul pipeline și propriul
package.json(sau măcar propriul target de build). - Deploy separat — echipa Catalog publică o versiune nouă fără ca shell-ul sau echipa Coș să facă ceva.
- Compunere la runtime — un shell (host) află abia în browser ce versiune a fiecărui remote să încarce, de obicei dintr-un manifest.
BUILD-TIME — NU e micro frontend
libs/catalog ─┐
libs/cart ─┼─► ng build shell ─► un singur deploy
libs/reviews ─┘
o versiune, un release, contracte verificate de compilator
RUNTIME — micro frontends
catalog → build → deploy → catalog…/remoteEntry.json ─┐
cart → build → deploy → cart…/remoteEntry.json ─┼─► shell-ul le compune
reviews → build → deploy → reviews…/remoteEntry.json ─┘ în browser
N versiuni, N release-uri, contracte verificate abia la runtime| Compunere | Cum funcționează | E micro frontend? |
|---|---|---|
| Librării într-un monorepo | shell-ul importă libs/catalog; un build, un deploy | nu — e build-time (vezi Monorepo) |
Lazy routes, @defer | chunk-uri separate, dar produse de același build | nu — aceeași aplicație |
| Native / Module Federation | remotes publică ES modules + metadate; shell-ul le încarcă după manifest și partajează dependențele | da |
| Web Components | fiecare echipă livrează un bundle care definește tag-uri; shell-ul doar le folosește | da (vezi Angular Elements) |
| iframes | documente separate, într-un cadru | da — cea mai puternică izolare |
Motive reale, motive false
Motivele care justifică prețul sunt aproape toate despre oameni și livrare:
- Echipe autonome cu cadențe diferite. Echipa de checkout livrează de mai multe ori pe zi, catalogul o dată pe săptămână, iar un release comun le blochează pe amândouă: o regresie din catalog oprește un fix urgent de plată.
- Organizații foarte mari. Zeci de dezvoltatori frontend pe același produs, mai multe domenii de business, fiecare cu ownership clar (legea lui Conway: arhitectura ajunge oricum să reflecte organizarea).
- Migrare graduală (strangler fig). Un shell nou „înconjoară” aplicația veche (AngularJS, un Angular vechi, alt framework); ecranele migrează pe rând, iar vechiul cod se micșorează până dispare — fără un rewrite „big bang”.
- Produse separate sub o singură experiență: achiziții, unități de business, portaluri care adună aplicații existente.
Motivele false sună bine în prezentări, dar au soluții mai ieftine:
| „Vrem MFE pentru…” | De ce nu ține | Ce rezolvă de fapt |
|---|---|---|
| scalabilitate / performanță | MFE adaugă bytes, request-uri și negociere de versiuni la pornire | lazy routes, @defer, SSR, optimizări |
| cod curat, modular | modularitatea e o proprietate a codului, nu a deploy-ului | librării + reguli de import (granițe, structură) |
| build-uri mai rapide | N pipeline-uri înseamnă N locuri care se pot strica | build incremental și cache (Nx affected) în monorepo |
| fiecare echipă să-și aleagă framework-ul | diversitatea tehnologică e un cost permanent: design system dublat, oameni greu de mutat între echipe | acceptă-o doar când e inevitabilă (migrare, achiziție) |
Dacă nu poți numi două echipe care azi se blochează reciproc la release, nu ai problema pe care o rezolvă micro frontends.
Prețul
Fiecare cost de mai jos apare în practică; diferența dintre un sistem sănătos și unul fragil e dacă ai un plan pentru el din prima zi.
| Cost | Ce se întâmplă concret | Cum îl ții sub control |
|---|---|---|
| Versiuni partajate | un remote construit cu alt Angular nu poate folosi runtime-ul shell-ului; upgrade-urile de major devin coordonate | aceeași versiune peste tot, calendar comun de upgrade, verificare în CI |
| Integrare la runtime | un export redenumit în remote strică shell-ul în producție, nu la compilare | exposes tratat ca API public (semver), teste de contract, fallback per remote |
| Bundle-uri duplicate | ce nu e partajat se descarcă de N ori; ce e partajat se construiește separat, deci nu mai e tree-shaken pe fiecare aplicație | bundle analyzer, partajezi doar ce are sens |
| Design system | cinci echipe, cinci versiuni de butoane | design system ca pachet partajat + tokens CSS, deținut de o echipă de platformă |
| Routing | cine deține URL-ul, ce se întâmplă la un deep link direct în remote | shell-ul deține rutele de top, fiecare remote exportă Routes pentru prefixul lui |
| Auth și sesiune | token-ul trebuie să ajungă la toate remotes, refresh-ul trebuie făcut o singură dată | cookie HttpOnly sau un serviciu de auth dintr-o librărie partajată ca singleton (auth refresh) |
| Testare e2e | fiecare echipă e verde izolat, bug-ul apare în compunere | e2e pe „shell + versiunile din producție ale celorlalți” |
| Observabilitate | „eroare în producție” — din ce remote, ce versiune? | versiunea fiecărui remote în fiecare log, source maps per remote |
| Dezvoltare locală | pornești cinci servere ca să lucrezi la unul | manifest per mediu: shell-ul local încarcă remotes deployate, rulezi doar ce modifici |
Native Federation pas cu pas
Native Federation implementează modelul Module Federation pe standarde web: remotes sunt ES modules obișnuite, legate printr-un import map. Pentru Angular 22 pachetul e @angular-architects/native-federation 22.x — versiunea urmează major.minor-ul Angular — și rămâne pe builder-ul esbuild al CLI-ului (@angular/build:application), pe care îl învelește. Proiectul s-a mutat în organizația github.com/native-federation. Mecanismul, pe pași:
- Build. Fiecare remote produce ES modules și un
remoteEntry.json: ce expune (./routes→ fișierul generat) și ce dependențe partajează, cuversion,requiredVersion,singleton,strictVersion. - Pornirea shell-ului.
main.tsapeleazăinitFederation()înainte de Angular: citește manifestul (nume → URL), descarcă metadatele fiecărui remote, negociază versiunile și scrie import map-ul (cues-module-shims). - Bootstrap. Abia apoi
import('./bootstrap')pornește Angular. De aceea schematic-ul mută bootstrap-ul într-un fișier separat:@angular/coretrebuie rezolvat prin import map, deci după ce acesta există. - Navigare.
loadRemoteModule('catalog', './routes')e, în esență, unimport()dinamic al unui URL de pe serverul echipei Catalog. Când codul remote-ului faceimport '@angular/core', import map-ul îl trimite la copia shell-ului: un singur Angular, un singur injector root.
# versiunea adaptorului urmează major.minor-ul Angular (22.2.x pentru Angular 22.2)
npm i -D @angular-architects/native-federation
ng g @angular-architects/native-federation:init \
--project catalog --port 4201 --type remote
ng g @angular-architects/native-federation:init \
--project shell --port 4200 --type dynamic-host
# Ce schimbă schematic-ul:
# • build/serve → @angular-architects/native-federation:build,
# care deleagă la @angular/build:application (esbuild)
# • main.ts → initFederation(...) apoi import('./bootstrap');
# bootstrap-ul vechi ajunge în bootstrap.ts
# • generează federation.config.mjs (+ public/federation.manifest.json la dynamic-host)
# • adaugă es-module-shims în polyfills (import maps cu scopes)
# • cu --webcomponent: bootstrap ca custom element (createApplication + define)// catalog/federation.config.mjs — un remote, așa cum îl generează 22.x
import {
withNativeFederation,
fromPackageJson,
} from '@angular-architects/native-federation/config';
export default withNativeFederation({
name: 'catalog',
exposes: {
// API-ul public al remote-ului
'./routes': './projects/catalog/src/app/catalog.routes.ts',
},
shared: fromPackageJson({
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
build: 'package',
})
// tot @angular/core, inclusiv entry point-urile secundare → o singură copie
.patch(['@angular/core'], { includeSecondaries: { keepAll: true } }),
skip: ['rxjs/ajax', 'rxjs/fetch', 'rxjs/testing', 'rxjs/webSocket'],
features: { denseChunking: true },
});// shell/src/main.ts — rulează ÎNAINTE de Angular:
// citește manifestul, descarcă remoteEntry.json-urile, negociază versiunile,
// scrie import map-ul — abia apoi încarcă bootstrap.ts (și @angular/core)
import { initFederation } from '@angular-architects/native-federation';
initFederation('federation.manifest.json')
.then((nf) => import('./bootstrap').then((m) => m.bootstrap(nf)))
.catch((err) => console.error(err));- Pentru Angular 22, Native Federation v4 vine din nou în pachetul principal
@angular-architects/native-federation(pentru Angular 20–21 exista puntea-v4). - Schematic-ul generează
federation.config.mjscufromPackageJson(...);shareAllșisharerămân disponibile. loadRemoteModuleimportat direct din pachet e marcat@deprecated: folosește-l pe cel din rezultatul luiinitFederation(), ca înapp.routes.ts.
// Varianta scurtă din tutorialele mai vechi există încă în 22.x,
// dar e marcată @deprecated:
import { loadRemoteModule } from '@angular-architects/native-federation';
const route = {
path: 'catalog',
loadChildren: () =>
loadRemoteModule('catalog', './routes').then((m) => m.CATALOG_ROUTES),
};
// Motivul, din d.ts: depinde de o instanță globală și de ULTIMUL initFederation,
// deci e fragil în teste și cu mai mulți host-i.
// Recomandat: loadRemoteModule din rezultatul lui initFederation (vezi app.routes.ts).Demo-ul de mai jos reproduce exact acest flux. Serverele sunt simulate, dar fiecare „remote” e o componentă Angular reală, aflată în propriul chunk și încărcată cu import() dinamic — același mecanism pe care loadRemoteModule îl folosește dedesubt. Catalogul și coșul vorbesc doar prin evenimente pe window; recenziile nu vorbesc cu nimeni.
Dependențe partajate și versiuni
Pentru fiecare pachet din shared, un remote declară patru lucruri: version (cu ce a fost construit), requiredVersion (ce acceptă; 'auto' = range-ul din package.json, de exemplu ^22.2.0), singleton (o singură instanță pentru toți) și strictVersion (refuză o versiune din afara range-ului).
De ce Angular trebuie să fie singleton. @angular/core nu e o librărie de funcții pure: e runtime-ul care randează, injectorul, change detection. Template-urile compilate apelează instrucțiunile runtime-ului cu care au fost construite, deci o componentă a unei copii nu poate fi randată de alta. Chiar și pentru o librărie obișnuită, două copii înseamnă două clase diferite: inject(AuthService) din remote cere alt token decât cel înregistrat de shell, iar „singleton-ul” tău are brusc două instanțe.
Ce se întâmplă când versiunile nu se potrivesc depinde de unealtă:
| Situație (singleton: true) | Native Federation (orchestrator v4) | Module Federation (webpack) |
|---|---|---|
versiunea partajată satisface requiredVersion | remote-ul folosește copia partajată; versiunea host-ului are prioritate | folosește singleton-ul |
nu satisface, strictVersion: true | remote-ul primește copie proprie, în scopes-ul din import map | eroare la încărcare: Unsatisfied version … of shared singleton module … |
nu satisface, strictVersion: false | folosește oricum versiunea partajată + avertisment | folosește singleton-ul + console.warn |
Pentru Angular, „copie proprie” nu salvează situația: remote-ul ar avea alt runtime, deci componentele lui nu pot intra în arborele shell-ului. Un remote pe alt major se integrează doar izolat — ca Web Component cu bootstrap propriu sau într-un iframe. Dacă preferi să afli la pornire, nu la navigare, orchestratorul are opțiunea strict.strictExternalCompatibility, care aruncă eroare la inițializare în loc să creeze scope-uri.
// Trei moduri de a scrie `shared` în federation.config.mjs
// 1. implicit în 22.x: tot ce e în dependencies din package.json, aceleași reguli
shared: fromPackageJson({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
// 2. stilul clasic (încă exportat)
shared: { ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }) },
// 3. explicit, pachet cu pachet
shared: share({
'@angular/core': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
// are stare → singleton
'@ngrx/signals': { singleton: true, strictVersion: true, requiredVersion: 'auto' },
// utilitar fără stare: fiecare remote poate avea copia lui
'date-fns': { singleton: false },
}),
// requiredVersion: 'auto' → range-ul din package.json (ex. "^22.2.0")
// autoShareScope({ level: 'minor' }) → partajează doar între remotes
// de pe aceeași linie de versiune (ex. "ng22.2") Shell-ul (host) oferă @angular/core în versiunea 22.2.2, care are prioritate în scope-ul global.
| Remote | construit cu | requiredVersion | singleton | strictVersion |
|---|---|---|---|---|
| catalog | ||||
| cart | ||||
| reviews |
@angular/core descărcate: 2un runtime Angular în plus{
"imports": {
"@angular/core": "https://shell.example/core-22.2.2.js"
},
"scopes": {
"https://cart.example/": {
"@angular/core": "https://cart.example/core-22.1.4.js"
}
}
}- Ideal: aceeași versiune exactă de Angular în shell și în toate remotes.
^îți dă toleranță între minore, dar codul compilat AOT folosește instrucțiuni interne ale runtime-ului — testează combinația, nu te baza pe semver. - Host-ul câștigă, deci la un minor nou shell-ul poate merge primul, cât timp range-urile remote-urilor îl acceptă. Un major nou cere upgrade sincronizat sau izolarea remote-ului rămas în urmă.
- Singleton + strictVersion pentru framework și pentru tot ce are stare: Angular, router, store-uri, design system, librăria de auth. Utilitarele fără stare pot rămâne non-singleton.
autoShareScope()leagă partajarea de o linie de versiune (de exemplung22.2), ca să nu amesteci accidental build-uri incompatibile.
Module Federation, Web Components, iframes
Module Federation (webpack)
@angular-architects/module-federation are versiune pentru Angular 22, dar e varianta „clasică”, pe webpack: ng add mută proiectul de pe builder-ul esbuild pe @angular-builders/custom-webpack (din Angular 22; până la 21 folosea ngx-build-plus) și te avertizează explicit că pentru proiecte noi alternativa e Native Federation. Există și un builder rspack, marcat experimental. Alege-l doar dacă extinzi un sistem Module Federation existent sau trebuie să împarți runtime-ul cu remotes construite deja pe webpack.
// Module Federation clasic (webpack) — @angular-architects/module-federation 22.x
const {
shareAll,
withModuleFederationPlugin,
} = require('@angular-architects/module-federation/webpack');
module.exports = withModuleFederationPlugin({
name: 'catalog',
exposes: {
'./routes': './projects/catalog/src/app/catalog.routes.ts',
},
shared: {
...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
},
});Web Components
Fiecare remote pornește propria aplicație cu createApplication() și definește un tag; shell-ul doar îl pune în pagină. Funcționează între majore Angular diferite și între framework-uri, pentru că nimic nu se partajează. Exact asta e și costul: fiecare bundle își aduce runtime-ul (≈ 33 kB transferați pentru un element simplu, măsurat pe pagina despre Angular Elements), nu există DI comun, iar comunicarea trece prin atribute și evenimente. Se combină bine cu federația: un remote pe alt major se expune ca Web Component și se încarcă tot prin loadRemoteModule.
// Remote pe ALT major Angular (sau alt framework): integrat izolat,
// ca Web Component cu runtime propriu.
//
// remote — federation.config.mjs: exposes: { './Bootstrap': './src/bootstrap.ts' }
// remote — bootstrap.ts:
// createApplication(appConfig).then(({ injector }) =>
// customElements.define('mfe-legacy', createCustomElement(App, { injector })));
//
// shell — o pagină care încarcă bundle-ul (efectul lui: define), apoi folosește tag-ul
import { CUSTOM_ELEMENTS_SCHEMA, Component, inject, input, resource } from '@angular/core';
import { NATIVE_FEDERATION } from '../app.config';
@Component({
selector: 'app-legacy-page',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
@if (bundle.hasValue()) {
<mfe-legacy [attr.customer-id]="customerId()"></mfe-legacy>
} @else if (bundle.error()) {
<p role="alert">
Modulul legacy nu e disponibil.
<button (click)="bundle.reload()">Reîncearcă</button>
</p>
}
`,
})
export class LegacyPage {
private readonly nf = inject(NATIVE_FEDERATION);
readonly customerId = input.required<string>();
protected readonly bundle = resource({
loader: () => this.nf.loadRemoteModule('legacy', './Bootstrap'),
});
}iframes
Cea mai puternică izolare: alt document, alt JavaScript, alt CSS; erorile, memory leak-urile și stilurile rămân în cadru, iar cu sandbox poți rula și cod în care nu ai încredere. Plătești cu integrarea: înălțimea se sincronizează prin postMessage, un dialog nu poate ieși din cadru, deep link-urile și istoria browserului trebuie legate manual, focusul și accesibilitatea se complică, iar fiecare cadru își încarcă toată aplicația. Potrivit pentru cod terț, aplicații legacy pe care nu le poți modifica și unelte interne.
<!-- Cea mai puternică izolare: alt document, alt JS, alt CSS;
erorile rămân în cadru -->
<iframe
src="https://billing.exemplu.ro/embed?customer=42"
title="Facturare"
sandbox="allow-scripts allow-same-origin allow-forms"
loading="lazy"
style="width: 100%; border: 0"
></iframe>
<!-- Prețul: înălțimea se comunică prin postMessage, un dialog nu poate ieși din
cadru, deep link-urile și istoria browserului se sincronizează manual,
iar fiecare cadru își încarcă toată aplicația. -->Contracte, comunicare, fallback
Contractul unui remote e tot ce depinde de el din afară: cheile din exposes, ce exportă ele, parametrii de rută pe care îi citește, evenimentele pe care le emite și dependențele partajate. Tratează-l ca pe un API public cu semver: o schimbare incompatibilă înseamnă o cheie nouă (./routes-v2), iar cea veche rămâne până migrează shell-ul.
Comunicarea, în ordinea în care merită încercată:
- URL-ul — parametri de rută și query params. Gratuit, deep-linkable, supraviețuiește unui refresh, iar remote-ul nu importă nimic din alt remote.
- Evenimente tipate pe
window, dintr-un pachetcontractsminuscul (tipuri +emit/listen). Nu uita de cel care pornește târziu: el trebuie să ceară starea. - Câteva servicii mici, partajate ca singleton — auth, utilizatorul curent, feature flags — dintr-o librărie inclusă în
shared.
Ce să eviți: un store global comun (același NgRx store sau același serviciu cu toată starea folosit de toate remotes). Orice schimbare de formă a stării devine o schimbare coordonată între echipe — exact cuplajul pe care voiai să-l elimini. Fiecare remote își ține starea; între ele circulă doar evenimente și URL-uri (vezi și state management).
// 1. URL-ul e primul canal: gratuit, deep-linkable, supraviețuiește unui refresh.
// /catalog/audio?sort=price → remote-ul citește din URL, nu dintr-un store comun.
// (withComponentInputBinding() se pune în SHELL: rutele remote-ului
// rulează în router-ul shell-ului)
export class ProductList {
readonly category = input<string>(); // din :category
readonly sort = input<'price' | 'rating'>('rating'); // din ?sort=
}
// 2. Navigarea între remotes trece tot prin URL:
// remote-ul nu importă nimic din alt remote.
this.router.navigate(['/cart'], { queryParams: { added: product.id } });Fallback. Orice remote poate lipsi: deploy eșuat, CDN căzut, greșeală în manifestul unui mediu. Două situații, două soluții:
- O rută întreagă: lasă
loadChildrensă eșueze și transformă eroarea într-un redirect cuwithNavigationErrorHandler. Un loader respins nu e memorat de router, deci navigarea următoare reîncearcă remote-ul. - Un remote într-o pagină (un widget, un slot): o componentă a shell-ului încarcă remote-ul cu
resource, cu timeout, fallback șireload()pentru „Reîncearcă” — ca în primul demo.
// O RUTĂ remote căzută: lasă loadChildren să eșueze și redirecționează.
// Un loader respins NU e memorat de router → navigarea următoare reîncearcă.
import { ApplicationConfig, inject } from '@angular/core';
import {
NavigationError,
RedirectCommand,
Router,
provideRouter,
withNavigationErrorHandler,
} from '@angular/router';
import type { NativeFederationResult } from '@angular-architects/native-federation';
import { routes } from './app.routes';
export const appConfig = (nf: NativeFederationResult): ApplicationConfig => ({
providers: [
provideRouter(
routes(nf),
withNavigationErrorHandler((e: NavigationError) => {
const router = inject(Router);
const target = `/indisponibil?url=${encodeURIComponent(e.url)}`;
return new RedirectCommand(router.parseUrl(target));
}),
),
],
});
// ✗ Capcana: un fallback întors din loader e un SUCCES pentru router.
// Îl memorează pe rută până la reîncărcarea paginii, deci „Reîncearcă”
// nu mai ajunge niciodată la remote:
// loadComponent: () => nf.loadRemoteModule(...).then(...).catch(() => RemoteDown)Când DA, când NU
Decizia ține de organizație mai mult decât de cod. Scorul de mai jos aplică regulile din pagină: nu e o formulă magică, dar te obligă să răspunzi la întrebările care contează și îți arată de ce iese o recomandare.
- + 3 echipe: granițe în cod (librării, reguli de import) ajung de obicei
- + un release comun e acceptabil
- + de la ~4 echipe autonomia începe să conteze
- − fără nevoie reală de deploy independent, MFE nu-și justifică prețul
- + aceeași versiune de Angular → @angular/core poate fi un singleton partajat
- − fără platformă, pipeline-uri per echipă și monitorizare, complexitatea MFE ajunge în producție
- Mai multe echipe autonome (orientativ: de la 4–5 în sus) pe același produs, cu cadențe de release diferite, care azi se blochează reciproc.
- Deploy independent obligatoriu (contractual, regulatoriu, ritmuri diferite) → Native Federation cu aceeași versiune de Angular.
- Migrare graduală dintr-o aplicație veche → shell nou + strangler fig; ecranele vechi ca Web Components sau iframe până dispar.
- Produse separate sub același brand, cu ownership clar pe domenii de business.
- Ai deja fundația: echipă de platformă pentru shell și design system, CI/CD per echipă, monitorizare pe versiune de remote.
- O echipă sau câteva echipe care pot livra împreună → o aplicație în monorepo, cu lazy routes și granițe.
- Problema e performanța →
@defer, lazy loading, SSR; MFE o înrăutățește. - Problema e codul încâlcit → librării, reguli de import, arhitectură; MFE doar mută încâlceala între repo-uri.
- Vrei doar să refolosești componente între aplicații → o librărie publicată sau partajată în monorepo.
- Un widget pentru pagini străine → un singur Web Component (Angular Elements), nu o arhitectură.
- Nu există cine să dețină shell-ul, contractele și upgrade-urile comune → amână până există.
Capcane frecvente
- Librăria „partajată” care nu e în
shared→ fiecare remote o împachetează separat, deciprovidedIn: 'root'produce două instanțe (două clase, două token-uri). Include librăriile cu stare însharedși verifică înremoteEntry.json. singleton: falsepe@angular/coresau un remote pe alt major → două runtime-uri Angular. Aceeași versiune peste tot; pentru excepții, izolare (Web Component, iframe).strictVersion: falseca să „treacă” → mesajul dispare din consolă, problema rămâne și apare în producție. Repară versiunile, nu flag-ul.- Fallback întors din
loadComponent(.catch(() => RemoteDown)) → router-ul îl memorează pe rută până la refresh, iar „Reîncearcă” nu mai ajunge la remote. Lasă loader-ul să eșueze sau folosește o componentă-gazdă cureload(). - Fără timeout → un
remoteEntry.jsoncare nu răspunde ține pagina în „loading” la nesfârșit. Pune un timeout la fiecare încărcare de remote. - Un manifest pentru toate mediile → staging încarcă remotes de producție (sau invers). Manifest per mediu, generat la deploy.
- Store global comun între remotes → release-uri în lockstep, adică un monolit distribuit. Stare locală + evenimente + URL.
- Remote care importă cod direct din alt remote → dependență ascunsă între deploy-uri. Totul trece prin
libs/contractssau prin URL. - Rute absolute scrise în remote (
/products/42) → se strică atunci când shell-ul îl montează sub alt prefix. Navighează relativ la ruta activă. - CSS global din remotes → stilurile unei echipe le strică pe ale alteia. Fără stiluri globale în remotes; tokens CSS din design system;
ShadowDompentru Web Components. - Erori fără context → nu știi ce remote, în ce versiune, a crăpat. Atașează numele și versiunea remote-ului la fiecare eroare raportată (vezi error handling).
Fișă rapidă
// REMOTE — federation.config.mjs
withNativeFederation({
name: 'catalog',
exposes: { './routes': './src/app/catalog.routes.ts' }, // API public, versionat
shared: fromPackageJson({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
});
// SHELL — public/federation.manifest.json (unul per mediu: dev / staging / prod)
// { "catalog": "https://catalog.exemplu.ro/remoteEntry.json" }
// SHELL — main.ts
initFederation('federation.manifest.json')
.then((nf) => import('./bootstrap').then((m) => m.bootstrap(nf)));
// SHELL — rute
const catalog = {
path: 'catalog',
loadChildren: () =>
nf.loadRemoteModule<{ CATALOG_ROUTES: Routes }>('catalog', './routes')
.then((m) => m.CATALOG_ROUTES),
};
// VERSIUNI: aceeași versiune de Angular peste tot; host-ul are prioritate.
// incompatibil + strictVersion → copie proprie în scopes (NF)
// „Unsatisfied version …” (webpack MF)
// incompatibil, fără strictVersion → folosește versiunea partajată + warning
// COMUNICARE: URL → evenimente tipate (libs/contracts) → servicii mici partajate.
// Fără store-uri comune.
// EȘEC: rută → withNavigationErrorHandler + redirect
// slot → componentă-gazdă cu timeout + resource.reload()
// ALT MAJOR / ALT FRAMEWORK → Web Component cu runtime propriu sau iframe