AngularAtlased. 22.2

Cap. 13Arhitectură la scară57 / 75

Micro frontends

Când merită să spargi o aplicație în mai multe deploy-uri și când e doar complexitate: Native Federation, Module Federation, iframes, Web Components.

arhitectură ~0 min de citit

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.

Pe scurt
  • 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.
De ce contează

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
CompunereCum funcționeazăE micro frontend?
Librării într-un monoreposhell-ul importă libs/catalog; un build, un deploynu — e build-time (vezi Monorepo)
Lazy routes, @deferchunk-uri separate, dar produse de același buildnu — aceeași aplicație
Native / Module Federationremotes publică ES modules + metadate; shell-ul le încarcă după manifest și partajează dependențeleda
Web Componentsfiecare echipă livrează un bundle care definește tag-uri; shell-ul doar le foloseșteda (vezi Angular Elements)
iframesdocumente separate, într-un cadruda — 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 țineCe rezolvă de fapt
scalabilitate / performanțăMFE adaugă bytes, request-uri și negociere de versiuni la pornirelazy routes, @defer, SSR, optimizări
cod curat, modularmodularitatea e o proprietate a codului, nu a deploy-uluilibrării + reguli de import (granițe, structură)
build-uri mai rapideN pipeline-uri înseamnă N locuri care se pot stricabuild incremental și cache (Nx affected) în monorepo
fiecare echipă să-și aleagă framework-uldiversitatea tehnologică e un cost permanent: design system dublat, oameni greu de mutat între echipeacceptă-o doar când e inevitabilă (migrare, achiziție)
Testul simplu

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.

CostCe se întâmplă concretCum îl ții sub control
Versiuni partajateun remote construit cu alt Angular nu poate folosi runtime-ul shell-ului; upgrade-urile de major devin coordonateaceeași versiune peste tot, calendar comun de upgrade, verificare în CI
Integrare la runtimeun export redenumit în remote strică shell-ul în producție, nu la compilareexposes tratat ca API public (semver), teste de contract, fallback per remote
Bundle-uri duplicatece nu e partajat se descarcă de N ori; ce e partajat se construiește separat, deci nu mai e tree-shaken pe fiecare aplicațiebundle analyzer, partajezi doar ce are sens
Design systemcinci echipe, cinci versiuni de butoanedesign system ca pachet partajat + tokens CSS, deținut de o echipă de platformă
Routingcine deține URL-ul, ce se întâmplă la un deep link direct în remoteshell-ul deține rutele de top, fiecare remote exportă Routes pentru prefixul lui
Auth și sesiunetoken-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 e2efiecare echipă e verde izolat, bug-ul apare în compuneree2e 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 unulmanifest 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:

  1. Build. Fiecare remote produce ES modules și un remoteEntry.json: ce expune (./routes → fișierul generat) și ce dependențe partajează, cu version, requiredVersion, singleton, strictVersion.
  2. Pornirea shell-ului. main.ts apelează initFederation() înainte de Angular: citește manifestul (nume → URL), descarcă metadatele fiecărui remote, negociază versiunile și scrie import map-ul (cu es-module-shims).
  3. Bootstrap. Abia apoi import('./bootstrap') pornește Angular. De aceea schematic-ul mută bootstrap-ul într-un fișier separat: @angular/core trebuie rezolvat prin import map, deci după ce acesta există.
  4. Navigare. loadRemoteModule('catalog', './routes') e, în esență, un import() dinamic al unui URL de pe serverul echipei Catalog. Când codul remote-ului face import '@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));
Ce s-a schimbat în 22.x
  • 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.mjs cu fromPackageJson(...); shareAll și share rămân disponibile.
  • loadRemoteModule importat direct din pachet e marcat @deprecated: folosește-l pe cel din rezultatul lui initFederation(), ca în app.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.

LiveShell + trei remotes (simulare, cu lazy loading real)
federation.manifest.json (editabil)
Deploy-uri (simulare de servere)
catalog.atlas.example
cart.atlas.example
reviews.atlas.example
shell@angular/core 22.2.2coș: —
<slot catalog>idle
<slot cart>idle
<slot reviews>idle
Ce face shell-ul
Pune reviews pe „offline”, cart pe @angular/core 21.2.4 sau strică un URL din manifest, apoi apasă „Încarcă pagina”: fiecare slot cade separat, cu fallback și „Reîncearcă”. „lent (4 s)” depășește timeout-ul shell-ului. Adaugă din catalog: badge-ul shell-ului se actualizează printr-un eveniment. În DevTools → Network vezi chunk-urile reale ale remote-urilor.

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 requiredVersionremote-ul folosește copia partajată; versiunea host-ului are prioritatefolosește singleton-ul
nu satisface, strictVersion: trueremote-ul primește copie proprie, în scopes-ul din import maperoare la încărcare: Unsatisfied version … of shared singleton module …
nu satisface, strictVersion: falsefolosește oricum versiunea partajată + avertismentfoloseș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")
LiveNegocierea versiunilor și import map-ul (simulare)

Shell-ul (host) oferă @angular/core în versiunea 22.2.2, care are prioritate în scope-ul global.

Remoteconstruit curequiredVersionsingletonstrictVersion
catalog
cart
reviews
catalogshare
22.2.2 satisface ^22.2.2 → folosește copia shell-ului, nu descarcă nimic în plus.
cartscope
22.2.2 nu satisface ~22.1.4; cu strictVersion primește propria copie 22.1.4, vizibilă doar în scope-ul lui. Două runtime-uri Angular: componentele lui nu se pot randa în arborele shell-ului — integrează-l izolat (Web Component cu bootstrap propriu).
reviewsskip + warning
22.2.2 nu satisface ^21.2.4, dar strictVersion: false → folosește oricum 22.2.2, cu un avertisment în consolă. Poate merge, poate crăpa la runtime — afli în producție.
Copii de @angular/core descărcate: 2un runtime Angular în plus
import map generat
{
  "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"
    }
  }
}
Simulează regulile resolver-ului Native Federation pentru @angular/core. Schimbă versiunea, range-ul și flag-urile fiecărui remote și urmărește verdictul și import map-ul. Observă cum ~ (acceptă doar patch-uri) produce o copie în plus fără niciun motiv real, iar singleton: false creează mereu un al doilea Angular.
  • 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 exemplu ng22.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ă:

  1. 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.
  2. Evenimente tipate pe window, dintr-un pachet contracts minuscul (tipuri + emit/listen). Nu uita de cel care pornește târziu: el trebuie să ceară starea.
  3. 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ă loadChildren să eșueze și transformă eroarea într-un redirect cu withNavigationErrorHandler. 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 și reload() 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.

LiveAvem nevoie de micro frontends?
Maturitatea organizației
Situații speciale
Probabil nu ai nevoie de micro frontends: Monorepo + lazy routes.
Monorepo + lazy routes (o singură aplicație)85
  • + 3 echipe: granițe în cod (librării, reguli de import) ajung de obicei
  • + un release comun e acceptabil
Native Federation (remotes Angular, runtime partajat)0
  • + 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
Web Components (Angular Elements, runtime per bundle)0
iframes (izolare totală)0
Pornește de la situația ta reală. Observă cât de mult cântăresc deploy-ul independent și maturitatea organizației, cum „mai multe majore Angular” mută recomandarea spre Web Components și cum codul nesigur cere iframe.
Micro frontends
  • 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.
Altceva
  • 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, deci providedIn: 'root' produce două instanțe (două clase, două token-uri). Include librăriile cu stare în shared și verifică în remoteEntry.json.
  • singleton: false pe @angular/core sau un remote pe alt major → două runtime-uri Angular. Aceeași versiune peste tot; pentru excepții, izolare (Web Component, iframe).
  • strictVersion: false ca 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ă cu reload().
  • Fără timeout → un remoteEntry.json care 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/contracts sau 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; ShadowDom pentru 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

Cap. 13 · 0 din 4 terminate

Ai terminat lecția?

Marcheaz-o și apare pe harta ta: în cuprins și pe prima pagină.

Se marchează singură după ce citești toată lecția.

  1. Micro frontends
  2. Web Components cu Angular Elements
  3. Monorepo, librării & granițe
  4. Arhitectură hexagonală (ports & adapters)
Vezi toată harta ta

Toate cele 75 de pagini · scrie ca să filtrezi

  • 01Ce e nou în Angular 21 & 22Toate schimbările importante din v21 și v22, pe scurt, cu link către pagina care le explică.Start
  • 01Prima ta aplicație, pas cu pasO listă de lecturi construită de la o componentă goală la o aplicație terminată — signals, @for, input-uri și output-uri, un serviciu și un formular, pas cu pas.Start
  • 01Decizii rapide„Unde pun asta?”, „Signal sau Observable?”, „Shared sau feature?” — răspunsuri scurte la întrebările de zi cu zi.Start
  • 01Întrebări de interviu Angular60 de întrebări reale de interviu pentru junior, mid și senior, cu răspunsul de dat, întrebarea care urmează și greșelile de evitat. Plus o simulare de interviu.Start
  • 02Standalone & bootstrapCum pornește o aplicație fără NgModule: bootstrapApplication, app.config.ts, imports în componentă.Fundamente moderne
  • 02Data binding și evenimenteInterpolare, binding pe proprietăți și pe atribute, evenimente cu $event și filtre de taste și cum e construit [( )] — ce intră în DOM și ce se întoarce.Fundamente moderne
  • 02Control flow: @if, @for, @switchSintaxa de blocuri care înlocuiește *ngIf/*ngFor/ngSwitch, cu track, @empty și verificare exhaustivă.Fundamente moderne
  • 02@let și expresiile noi din templateVariabile locale cu @let, arrow functions, spread, regex, instanceof, comentarii în tag-uri.Fundamente moderne
  • 02@defer — lazy loading în templateÎncarci componente grele doar când e nevoie: triggere, prefetch, placeholder, loading, error.Fundamente moderne
  • 02Class & style bindingsDe ce [class.x] și [style.x] au înlocuit ngClass și ngStyle (cu migrare automată din v21).Fundamente moderne
  • 02Stilurile componentelor și încapsularea:host, cele trei moduri de încapsulare, teme cu variabile CSS și de ce ::ng-deep e ultima soluție.Fundamente moderne
  • 02Pipes: când și cumPipe pur vs impur, pipe custom și cum alegi între pipe, computed() și o metodă.Fundamente moderne
  • 03signal() și computed()Primitivele reactive: stare scriibilă, stare derivată, egalitate, untracked și reguli de imutabilitate.Signals & reactivitate
  • 03effect() — când DA, când NUEfecte secundare sincronizate cu semnale: localStorage, logging, API-uri imperative. Și capcanele lor.Signals & reactivitate
  • 03linkedSignal() — stare derivată editabilăUn semnal care se resetează când se schimbă sursa, dar pe care îl poți și seta manual.Signals & reactivitate
  • 03resource, rxResource, httpResourceDate asincrone ca semnale: status, value, error, reload, anulare automată. Stabile din v22.Signals & reactivitate
  • 03debounced() — semnale cu întârziereVarianta „debounce” a unui semnal, fără RxJS. Perfect pentru căutare.Signals & reactivitate
  • 03Liste: căutare, filtrare, sortare, paginareUn lanț de computed-uri care recalculează doar ce s-a schimbat, cu filtrele ținute în URL, ca un link să redeschidă aceeași vedere.Signals & reactivitate
  • 03Observables de la zeroCe e un Observable, subscribe și unsubscribe, operatorii pe care îi folosești în fiecare săptămână, cold vs hot — și când ajunge un signal.Signals & reactivitate
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — și când RxJS rămâne alegerea bună.Signals & reactivitate
  • 04input() și output()Inputuri ca semnale (required, transform, alias) și outputuri tipizate. Data down, events up.Componente: comunicare
  • 04model() — two-way bindingBanana-in-a-box [( )] cu un singur API. Când îl folosești și când e mai bine input + output.Componente: comunicare
  • 04viewChild & contentChildInterogări ca semnale: viewChild, viewChildren, contentChild, contentChildren, opțiunea read.Componente: comunicare
  • 04Lifecycle & DestroyRefCe hook-uri mai contează în era signals, afterNextRender, DestroyRef și ngOnChanges tipizat.Componente: comunicare
  • 04Content projectionng-content cu select, sloturi multiple, conținut implicit și capcana proiecției condiționale.Componente: comunicare
  • 04ng-template & ngTemplateOutletTemplate-uri ca valori: render props în Angular, context tipizat și personalizare de componente.Componente: comunicare
  • 05Smart vs DumbContainer vs presentational: cine ține starea, cine doar afișează, și cum le împarți corect.Design de componente
  • 05Componente generice <T>Un DataTable<T> sau Select<T> tipizat corect, cu template-uri pentru celule.Design de componente
  • 05Componente dinamiceNgComponentOutlet, createComponent cu inputBinding/outputBinding și un dashboard construit din config.Design de componente
  • 05Directive & host compositionDirective atribut, metadata host: {} și Directive Composition API (hostDirectives).Design de componente
  • 05Controale custom de formularFormValueControl (Signal Forms) vs ControlValueAccessor: un rating cu stele, în ambele variante.Design de componente
  • 05Change detectionOnPush e implicit din v22, zone.js lipsește din v21. Ce declanșează randarea și cum să nu te arzi.Design de componente
  • 06inject() & @Service()Noul decorator @Service() din v22, inject() vs constructor și contextul de injecție.Dependency Injection
  • 06Providers & ierarhia de injectoriRoot vs rută vs componentă, useClass/useValue/useFactory/useExisting și modificatorii optional/self/skipSelf.Dependency Injection
  • 06InjectionToken & provideX()Configurare tipizată, funcții provideFeature(), makeEnvironmentProviders și inițializatori.Dependency Injection
  • 06injectAsync() — servicii lazyÎncarci un serviciu greu abia când ai nevoie de el, cu prefetch la idle.Dependency Injection
  • 07Structura de folderecore / shared / features, ce intră în fiecare și cum arată un feature pe dinăuntru.Arhitectura aplicației
  • 07Shared vs featureCând muți o componentă în shared și (mai ales) când NU. Checklist concret.Arhitectura aplicației
  • 07Interfețe & modeleInterfață inline sau fișier separat? DTO vs model de domeniu, type vs interface, unde le pui.Arhitectura aplicației
  • 07State management cu signalsStore-uri simple pe semnale, facade, stare locală vs globală și când îți trebuie NgRx.Arhitectura aplicației
  • 07Style guide & namingConvențiile oficiale actuale: nume de fișiere fără sufixe, protected, readonly, ordinea membrilor.Arhitectura aplicației
  • 07Barrels, aliasuri & granițeindex.ts, path aliases, API public pe feature și reguli de dependență care se pot verifica automat.Arhitectura aplicației
  • 07Erori: @boundary & ErrorHandlerNoul bloc @boundary din v22.2, ErrorHandler global, erori HTTP și erori din resource.Arhitectura aplicației
  • 08Routing modern & lazy loadingloadComponent, rute copil, parametri ca input-uri și moștenirea parametrilor (default nou în v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate și resolve ca funcții, cu redirect prin UrlTree / RedirectCommand.Routing
  • 09Signal Forms — bazeleFormulare construite pe semnale: form(), [formField], validare prin schemă. Stabile din v22.Formulare
  • 09Signal Forms — avansatValidare custom și cross-field, async/HTTP cu debounce, liste dinamice, disabled/hidden, scheme reutilizabile.Formulare
  • 09Reactive Forms tipizateFormGroup tipizat, nonNullable, FormArray — și cum conviețuiesc cu Signal Forms.Formulare
  • 10HttpClient, interceptors & API layerHttpClient fără setup (v21), Fetch implicit (v22), interceptori funcționali și stratul de API cu mapare DTO.HTTP & date
  • 11Performanță — checklistCe chiar contează: OnPush + signals, track, @defer, lazy routes, imagini, bundle și hidratare.Calitate & performanță
  • 11Testare cu VitestVitest e runner-ul implicit din v21. TestBed, componente cu semnale, servicii, HTTP și harness-uri.Calitate & performanță
  • 11DevTools: cum te uiți în aplicațieChrome DevTools și Angular DevTools pas cu pas: ce arată fiecare panou, cu exerciții care merg chiar pe site-ul ăsta.Calitate & performanță
  • 11Angular AriaComponente headless, accesibile, pe care le stilizezi cum vrei: tabs, accordion, menu, listbox...Calitate & performanță
  • 12SSR, prerender & render modesCând randezi pe server, când la build și când doar în browser: RenderMode pe rută, app.routes.server.ts și costurile reale.SSR & hidratare
  • 12Hidratare: event replay & incrementalCum preia browserul HTML-ul de pe server fără să-l refacă: hidratare completă, event replay, @defer (hydrate on …) și erorile de nepotrivire.SSR & hidratare
  • 12Cod compatibil cu serverulwindow nu există pe server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, cache-ul HTTP și PendingTasks.SSR & hidratare
  • 13Micro frontendsCând merită să spargi o aplicație în mai multe deploy-uri și când e doar complexitate: Native Federation, Module Federation, iframes, Web Components.Arhitectură la scară
  • 13Web Components cu Angular ElementscreateCustomElement: o componentă Angular folosită ca tag HTML oriunde, plus comunicarea între aplicații prin evenimente și BroadcastChannel.Arhitectură la scară
  • 13Monorepo, librării & granițeUn workspace cu mai multe aplicații și librării: ng generate library, Nx, entry points secundare, publicare și reguli de dependență.Arhitectură la scară
  • 13Arhitectură hexagonală (ports & adapters)Domeniul fără Angular, porturi ca clase abstracte și adaptoare interschimbabile: HTTP, localStorage sau in-memory, fără să atingi UI-ul.Arhitectură la scară
  • 14Semnalele pe dinăuntruGraful reactiv: producători și consumatori, push-pull, de ce computed e lazy și glitch-free, problema diamantului și când rulează efectele.Reactivitate avansată
  • 14RxJS avansat: operatori de ordin superiorswitchMap, mergeMap, concatMap și exhaustMap pe o diagramă live, plus retry cu backoff, shareReplay și cum eviți scurgerile.Reactivitate avansată
  • 14NgRx SignalStoreState management la scară pe semnale: withState, withComputed, withMethods, rxMethod, entități și hooks — și când un store simplu e suficient.Reactivitate avansată
  • 14Mașini de stări cu signalsFără stări imposibile: o mașină tipizată pe signals, cu tranziții, guards și diagrama stărilor, live.Reactivitate avansată
  • 15WebSockets: timp real care rezistă rețeleiUn socket tipizat ca signals: reconectare cu backoff și jitter, heartbeat, coadă de mesaje cât ești offline și siguranță la SSR.Timp real și offline
  • 15Actualizări optimiste și rollbackInterfață instantanee, dar onestă: snapshot și rollback, răspunsuri sosite în altă ordine, chei de idempotență.Timp real și offline
  • 15Offline-first: Service Worker și cacheService Worker-ul Angular: strategii de cache, anunțarea versiunilor noi și o coadă pentru modificările făcute offline.Timp real și offline
  • 16Web WorkersCalcule grele fără să blochezi interfața: un worker real în pagină, comunicare prin mesaje și când nu merită.UI & performanță avansată
  • 16Liste uriașe: virtual scroll100.000 de rânduri fără lag: CDK virtual scroll, itemSize, trackBy și alternativele (paginare, infinite scroll).UI & performanță avansată
  • 16Animații moderne & View Transitionsanimate.enter și animate.leave în locul @angular/animations (depreciat), tranziții CSS și View Transitions la navigare.UI & performanță avansată
  • 16Memory leaks: cum le găsești și le repariSubscripții, timere și listeneri care trăiesc mai mult decât componenta lor — cum îi vezi în DevTools și cum îi închizi.UI & performanță avansată
  • 17Securitate: XSS, sanitizare & CSPCe face Angular automat împotriva XSS, când devine periculos bypassSecurityTrust, plus CSP cu nonce, Trusted Types și XSRF.Securitate & globalizare
  • 17Autentificare: token-uri & refreshUn interceptor care atașează token-ul, reîmprospătează o singură dată la 401 și reia cererile din coadă — plus unde ții token-ul.Securitate & globalizare
  • 17OAuth 2.1 și OpenID Connect cu PKCEFluxul authorization code cu PKCE, pas cu pas: verifier și challenge, state și nonce, ID token-ul și de ce un BFF e mai sigur.Securitate & globalizare
  • 17Internaționalizare (i18n)$localize, atributul i18n, ICU plural/select, build per limbă vs. traduceri la runtime și formatarea după locale.Securitate & globalizare