Na primeira parte, vimos como criar uma arquitetura de Micro Frontends utilizando Angular 21 e Native Federation.
Nesta segunda parte, vamos aprofundar a integração entre Host e Remote, entendendo o que acontece por trás da configuração e, principalmente, alguns pontos que podem gerar erros durante a implementação.
A ideia é sair de uma configuração que simplesmente "funciona" para entender por que ela funciona.
1. Arquitetura que vamos construir
Teremos dois projetos:
angular-micro-frontends/
│
├── mfe-host-2/
│ └── Angular 21
│ └── Host
│
└── mfe-remote-2/
└── Angular 21
└── Remote
Durante o desenvolvimento:
Host
http://localhost:4200
Remote
http://localhost:4201
O fluxo será:
HOST
localhost:4200
│
│
▼
federation.manifest.json
│
│
▼
Remote :4201
│
▼
remoteEntry.json
│
▼
./Component
│
▼
App
O ponto importante é entender que o Host não importa diretamente o arquivo app.ts do Remote.
Ele conhece apenas o nome do Remote e o módulo que o Remote decidiu expor.
2. O papel do federation.config.js
No Remote temos:
const {
withNativeFederation,
shareAll
} = require('@angular-architects/native-federation/config');
module.exports = withNativeFederation({
name: 'mfe-remote-2',
exposes: {
'./Component': './src/app/app.ts',
},
shared: {
...shareAll({
singleton: true,
strictVersion: true,
requiredVersion: 'auto',
}),
},
skip: [
'rxjs/ajax',
'rxjs/fetch',
'rxjs/testing',
'rxjs/webSocket',
],
features: {
ignoreUnusedDeps: true,
},
});
Aqui temos uma das partes mais importantes da configuração:
exposes: {
'./Component': './src/app/app.ts',
}
Isso significa:
"Quando outro aplicativo solicitar
./Component, entregue o módulo localizado em./src/app/app.ts."
O nome ./Component é um nome de exposição, não precisa ser igual ao nome do arquivo.
Por exemplo, também poderíamos ter:
exposes: {
'./Products': './src/app/pages/products/products.ts',
}
E no Host:
loadRemoteModule('mfe-remote-2', './Products')
3. ./Component não é o nome da classe
Esse é um ponto que pode gerar bastante confusão.
Temos:
exposes: {
'./Component': './src/app/app.ts',
}
e dentro do arquivo:
export class App {
}
No Host:
loadRemoteModule('mfe-remote-2', './Component')
.then((m) => m.App);
São três coisas diferentes:
mfe-remote-2
│
└── nome do Remote
./Component
│
└── nome da exposição
App
│
└── exportação JavaScript/TypeScript
Podemos visualizar assim:
Host
│
│ mfe-remote-2
│
▼
Remote
│
│ ./Component
│
▼
src/app/app.ts
│
│ export class App
│
▼
m.App
Portanto, se seu componente se chama:
export class App {
}
o correto é:
.then((m) => m.App)
e não:
.then((m) => m.AppComponent)
4. O papel do federation.manifest.json
No Host criamos:
public/
└── assets/
└── federation.manifest.json
Com:
{
"mfe-remote-2": "http://localhost:4201/remoteEntry.json"
}
Esse arquivo funciona como um catálogo dos Remotes que o Host conhece.
Podemos ter vários:
{
"mfe-1": "http://localhost:4201/remoteEntry.json",
"mfe-2": "http://localhost:4202/remoteEntry.json",
"mfe-3": "http://localhost:4203/remoteEntry.json"
}
Isso facilita bastante a configuração de uma arquitetura com vários Micro Frontends.
5. Cuidado com o caminho do manifest
No main.ts do Host:
import { initFederation } from '@angular-architects/native-federation';
initFederation('/assets/federation.manifest.json')
.catch((err) => console.error(err))
.then(() => import('./bootstrap'))
.catch((err) => console.error(err));
Prefira:
/assets/federation.manifest.json
em vez de:
../assets/federation.manifest.json
O motivo é simples: o primeiro é resolvido a partir da raiz da aplicação.
Se o Host estiver rodando em:
http://localhost:4200
o navegador buscará:
http://localhost:4200/assets/federation.manifest.json
Podemos validar isso diretamente no navegador.
Se retornar:
{
"mfe-remote-2": "http://localhost:4201/remoteEntry.json"
}
o manifest está sendo servido corretamente.
6. remoteEntry.json é diferente do antigo remoteEntry.js
Quem está acostumado com Webpack Module Federation pode estranhar isso.
No modelo tradicional era comum encontrar:
remoteEntry.js
No Native Federation trabalhamos com:
remoteEntry.json
Por exemplo:
http://localhost:4201/remoteEntry.json
Esse arquivo contém as informações necessárias para que o runtime do Native Federation descubra os módulos expostos pelo Remote.
Por isso, uma boa estratégia de debug é sempre testar:
http://localhost:4201/remoteEntry.json
Se ele não abrir, não adianta investigar a rota do Host ainda.
Primeiro precisamos resolver o Remote.
7. A rota do Host
No Angular 21 podemos utilizar componentes standalone diretamente:
import { loadRemoteModule } from '@angular-architects/native-federation';
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'mfe-1',
loadComponent: () =>
loadRemoteModule('mfe-remote-2', './Component')
.then((m) => m.App),
},
];
Quando o usuário acessa:
http://localhost:4200/mfe-1
o Angular executa:
/mfe-1
│
▼
loadComponent()
│
▼
loadRemoteModule()
│
▼
mfe-remote-2
│
▼
./Component
│
▼
App
8. path precisa bater com o link
Um erro simples, mas fácil de passar despercebido.
Se temos:
{
path: 'mfe-2',
loadComponent: () =>
loadRemoteModule('mfe-remote-2', './Component')
.then((m) => m.App),
}
o link precisa ser:
<a routerLink="/mfe-2">
Acesse MFE-2
</a>
Não:
<a routerLink="/mfe-1">
Acesse MFE-2
</a>
Parece óbvio, mas em uma aplicação com vários MFEs esse tipo de erro pode fazer parecer que o Federation não está funcionando.
9. Não esquecer o RouterOutlet
O Host precisa ter onde renderizar o componente remoto.
No app.html:
<div>
<h1>Angular 21 - Micro Frontends</h1>
<nav>
<a routerLink="/mfe-1">
Acesse MFE-1
</a>
<br />
<a routerLink="/mfe-2">
Acesse MFE-2
</a>
</nav>
</div>
<hr />
<router-outlet></router-outlet>
E como estamos usando standalone components:
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [
RouterLink,
RouterOutlet
],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
}
Não basta configurar:
provideRouter(routes)
Também precisamos que o template possua:
<router-outlet></router-outlet>
10. Não esquecer o provideRouter
No app.config.ts:
import {
ApplicationConfig,
provideBrowserGlobalErrorListeners
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes)
]
};
A cadeia completa fica:
main.ts
│
▼
bootstrap
│
▼
app.config.ts
│
▼
provideRouter(routes)
│
▼
app.routes.ts
│
▼
loadRemoteModule()
Se qualquer uma dessas partes estiver faltando, a rota pode não funcionar.
11. O Remote também pode funcionar sozinho
Uma das características mais interessantes dessa arquitetura é que o Remote não precisa existir somente dentro do Host.
Podemos acessar:
http://localhost:4201/
diretamente.
O mesmo componente:
export class App {
}
pode ser usado como aplicação independente.
E também:
http://localhost:4200/mfe-1
como Micro Frontend.
Visualmente:
App
│
┌───────┴────────┐
│ │
▼ ▼
Acesso direto Federation
:4201 :4200/mfe-1
Isso é muito útil durante o desenvolvimento porque o time responsável pelo MFE pode trabalhar sem precisar subir toda a aplicação Host.
12. SSE: o que significa?
Durante o desenvolvimento podemos encontrar no terminal:
[Federation SSE] Client connected.
Active connections: 1
Isso é esperado.
SSE significa:
Server-Sent Events.
O Native Federation utiliza esse mecanismo para comunicação entre o servidor de desenvolvimento e o navegador.
Uma mensagem como:
[Federation SSE] Client connected.
significa que o navegador conseguiu estabelecer a conexão.
Portanto, essa mensagem isoladamente não representa um erro.
Se aparecer:
[Federation SSE] Client connected.
temos uma indicação positiva de que o canal de desenvolvimento foi estabelecido.
13. Como fazer troubleshooting
Quando o MFE não aparece, evite alterar várias configurações ao mesmo tempo.
Faça os testes em ordem.
Passo 1 — Remote
Abra:
http://localhost:4201/
O Remote deve funcionar sozinho.
Passo 2 — remoteEntry
Abra:
http://localhost:4201/remoteEntry.json
Deve retornar o metadata do Remote.
Passo 3 — Manifest
Abra:
http://localhost:4200/assets/federation.manifest.json
Deve retornar:
{
"mfe-remote-2": "http://localhost:4201/remoteEntry.json"
}
Passo 4 — Host
Abra:
http://localhost:4200/
ou a rota correspondente:
http://localhost:4200/mfe-1
Passo 5 — DevTools
Abra:
F12
e verifique a aba:
Network
Procure por:
federation.manifest.json
remoteEntry.json
Se ambos carregarem, o próximo ponto a investigar é a resolução do módulo exposto.
14. Um erro importante: 404 no manifest
Um erro como:
GET http://localhost:4200/assets/federation.manifest.json 404
significa que o navegador não encontrou o manifest.
Uma configuração comum no Angular 21 é:
"assets": [
{
"glob": "**/*",
"input": "public"
}
]
Nesse caso, o arquivo precisa estar em:
public/assets/federation.manifest.json
e não necessariamente em:
src/assets/
Portanto:
mfe-host-2/
│
├── public/
│ └── assets/
│ └── federation.manifest.json
│
└── src/
└── app/
15. CSS em Micro Frontends
Outro ponto importante é o isolamento dos estilos.
Imagine que o Remote tenha:
src/
└── app/
├── app.ts
├── app.html
└── app.css
Podemos utilizar:
@Component({
selector: 'app-root',
imports: [],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
}
A recomendação para um MFE é priorizar estilos associados aos próprios componentes.
Por exemplo:
.container {
padding: 20px;
}
.title {
font-size: 32px;
}
Evite, quando possível, que o MFE altere globalmente:
body {
...
}
* {
...
}
html {
...
}
porque o Remote pode ser executado dentro de outro aplicativo.
16. Por que evitar CSS global no MFE?
Imagine:
Host
│
├── Header
├── Menu
│
└── MFE
│
└── styles.css
Se o MFE possui:
body {
margin: 0;
background: black;
}
esse estilo pode interferir na aplicação Host.
Por isso, uma boa regra é:
O Host controla o layout global da aplicação. O MFE controla o visual dos componentes que ele entrega.
Isso permite que o mesmo MFE seja executado:
localhost:4201
ou:
localhost:4200/mfe-1
sem depender de um CSS global específico do Host.
17. Um Remote pode expor mais de um componente
Não precisamos expor apenas o App.
Podemos ter:
Top comments (0)