Micro-frontends: Module Federation e estratégias
1. Introdução aos Micro-frontends com React e Node.js
Aplicações frontend monolíticas enfrentam desafios conhecidos: equipes grandes pisando nos mesmos arquivos, deploys lentos, dificuldade de escalar o desenvolvimento e impossibilidade de usar tecnologias diferentes em partes específicas do sistema. Micro-frontends surgem como resposta a esses problemas, aplicando ao frontend os mesmos princípios de microsserviços: cada domínio de negócio (bounded context) é desenvolvido, testado e implantado de forma independente.
No ecossistema JavaScript, o Module Federation do Webpack 5 se destaca por permitir que módulos sejam compartilhados em tempo de execução sem a necessidade de iframes ou proxies complexos. Combinado com Node.js para orquestração no servidor e React para construção de interfaces, temos um stack maduro para implementar micro-frontends de forma prática e escalável.
2. Fundamentos do Module Federation no Webpack 5
O Module Federation funciona com dois papéis principais: o host (aplicação que consome módulos remotos) e o remote (aplicação que expõe módulos para consumo). A comunicação acontece via um arquivo remoteEntry.js gerado pelo Webpack, que contém o manifesto dos módulos disponíveis.
Vamos criar um micro-frontend que expõe um componente React:
// webpack.config.js (remote - micro-frontend de produtos)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
mode: 'development',
devServer: { port: 3001 },
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// src/components/ProductList.jsx
import React from 'react';
const ProductList = () => {
const products = [
{ id: 1, name: 'Notebook', price: 4500 },
{ id: 2, name: 'Mouse', price: 120 },
];
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - R$ {product.price}
</li>
))}
</ul>
);
};
export default ProductList;
3. Estruturando uma Aplicação Full-Stack com Micro-frontends
No host (container principal), configuramos o consumo do remote:
// webpack.config.js (host - aplicação principal)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
mode: 'development',
devServer: { port: 3000 },
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
products: 'products@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
Para integrar com Node.js, podemos servir os bundles federados via Express:
// server.js (Node.js + Express)
const express = require('express');
const path = require('path');
const app = express();
const port = 3000;
app.use(express.static(path.join(__dirname, 'dist')));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
app.listen(port, () => {
console.log(`Host rodando em http://localhost:${port}`);
});
O compartilhamento de dependências com singleton: true garante que React e ReactDOM sejam instanciados apenas uma vez, evitando conflitos de contexto.
4. Estratégias de Roteamento e Navegação
No host, utilizamos React Router com lazy loading para carregar os micro-frontends sob demanda:
// src/App.jsx (host)
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
const ProductList = lazy(() => import('products/ProductList'));
const App = () => {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/produtos">Produtos</Link>
</nav>
<Suspense fallback={<div>Carregando...</div>}>
<Routes>
<Route path="/" element={<h1>Bem-vindo</h1>} />
<Route path="/produtos" element={<ProductList />} />
</Routes>
</Suspense>
</BrowserRouter>
);
};
export default App;
Para navegação entre domínios, utilizamos eventos customizados:
// No micro-frontend de checkout
const navigateTo = (path) => {
window.dispatchEvent(
new CustomEvent('app-navigate', { detail: { path } })
);
};
// No host
useEffect(() => {
const handler = (event) => {
navigate(event.detail.path);
};
window.addEventListener('app-navigate', handler);
return () => window.removeEventListener('app-navigate', handler);
}, []);
5. Gerenciamento de Estado entre Micro-frontends
Para estado global compartilhado, o Jotai oferece uma abordagem simples e performática:
// store.js (módulo compartilhado via Module Federation)
import { atom } from 'jotai';
export const cartAtom = atom([]);
export const userAtom = atom(null);
// No micro-frontend de produtos
import { useAtom } from 'jotai';
import { cartAtom } from 'shared/store';
const ProductCard = ({ product }) => {
const [cart, setCart] = useAtom(cartAtom);
const addToCart = () => {
setCart([...cart, product]);
};
return (
<div>
<h3>{product.name}</h3>
<button onClick={addToCart}>Adicionar ao carrinho</button>
</div>
);
};
Para workflows mais complexos, o XState permite criar máquinas de estado centralizadas:
import { createMachine, interpret } from 'xstate';
const checkoutMachine = createMachine({
id: 'checkout',
initial: 'idle',
states: {
idle: { on: { START: 'loading' } },
loading: { on: { SUCCESS: 'success', ERROR: 'error' } },
success: { type: 'final' },
error: { on: { RETRY: 'loading' } },
},
});
export const checkoutService = interpret(checkoutMachine).start();
6. Estilização e Design System Consistente
Criamos um módulo federado de UI para compartilhar temas e componentes:
// webpack.config.js (design-system)
new ModuleFederationPlugin({
name: 'design_system',
exposes: {
'./Theme': './src/theme',
'./Button': './src/components/Button',
'./Card': './src/components/Card',
},
shared: {
react: { singleton: true },
'styled-components': { singleton: true },
},
}),
// src/theme.js (design-system)
import { createGlobalStyle } from 'styled-components';
export const theme = {
colors: {
primary: '#0070f3',
secondary: '#0070f3',
background: '#ffffff',
},
spacing: (n) => `${n * 8}px`,
};
export const GlobalStyle = createGlobalStyle`
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
background-color: ${({ theme }) => theme.colors.background};
}
`;
O uso de CSS-in-JS com styled-components evita conflitos de escopo, pois os estilos são gerados com classes únicas em tempo de execução.
7. Deploy, Versionamento e Testes
Para CI/CD com múltiplos repositórios, cada micro-frontend tem seu próprio pipeline:
# .github/workflows/deploy-products.yml
name: Deploy Products Micro-frontend
on:
push:
branches: [main]
paths:
- 'micro-frontends/products/**'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run build
- run: npm test
- uses: some-deploy-action@v1
with:
bucket: ${{ secrets.S3_BUCKET }}
dist-folder: ./dist
Para testes de integração com Cypress:
// cypress/e2e/micro-frontends.cy.js
describe('Micro-frontends Integration', () => {
it('should load ProductList from remote', () => {
cy.visit('http://localhost:3000/produtos');
cy.contains('Notebook').should('be.visible');
cy.contains('Mouse').should('be.visible');
});
it('should share cart state between micro-frontends', () => {
cy.visit('http://localhost:3000/produtos');
cy.get('[data-testid="add-to-cart"]').first().click();
cy.visit('http://localhost:3001/carrinho');
cy.contains('Notebook').should('be.visible');
});
});
8. Conclusão e Próximos Passos
O Module Federation oferece uma abordagem madura e nativa do Webpack para implementar micro-frontends, permitindo que equipes trabalhem de forma independente enquanto mantêm uma experiência de usuário integrada. Os principais desafios incluem o gerenciamento de versões compartilhadas, a comunicação entre domínios e a manutenção de um design system consistente.
Ferramentas complementares como Single SPA (para orquestração avançada), Piral (com seu sistema de plugins) e Nx (para monorepos com geração de código) podem potencializar ainda mais a arquitetura. Na próxima etapa da série, exploraremos como integrar esses conceitos em uma aplicação full-stack completa, combinando React no frontend com Node.js no backend e um banco de dados real.
Referências
- Webpack Module Federation Documentation — Documentação oficial do Webpack sobre Module Federation, incluindo exemplos de configuração e boas práticas.
- Module Federation Examples (GitHub) — Repositório oficial com dezenas de exemplos práticos de Module Federation em diferentes cenários.
- Micro Frontends with React and Module Federation — Artigo técnico detalhado sobre implementação de micro-frontends com React e Module Federation.
- Jotai Documentation — Documentação oficial do Jotai, biblioteca de estado atômico para React ideal para comunicação entre micro-frontends.
- XState Documentation — Documentação oficial do XState para gerenciamento de máquinas de estado em aplicações complexas com micro-frontends.
- Cypress E2E Testing Guide — Guia oficial do Cypress para testes de integração em aplicações com micro-frontends.