Na primeira parte, discutimos nossa abordagem para micro-frontends, federação de módulos e como as integrações são estruturadas. Nesta segunda parte, conforme prometido, abordaremos o compartilhamento e isolamento de bibliotecas, lazy loading, além de oferecer algumas dicas e recomendações.
Compartilhando Web Components
Nossas aplicações são autossuficientes, o que significa que elas não têm conhecimento direto umas das outras. Precisamos de uma maneira de compartilhar web components entre elas. Existem duas abordagens principais:
- Federação de Módulos com
Exposes: Através da federação de módulos, podemos expor classes, módulos e outros artefatos, fornecendo as informações necessárias para o container. A desvantagem aqui é que criamos um contrato de baixo nível, estabelecendo acoplamentos desde a fase de desenvolvimento. Qualquer alteração futura exigirá a recompilação da aplicação. - Arquivo JSON Estático: Podemos gerar um arquivo JSON estático, semelhante ao nosso metafile. O cliente receberia este JSON contendo informações sobre os web elements da aplicação. A desvantagem é que este arquivo é estático e sua modificação em tempo de execução pode ser problemática.
Optamos por utilizar os recursos da federação de módulos para o compartilhamento de bibliotecas. É crucial entender que isso vai além de simplesmente evitar a duplicação de versões de bibliotecas. Graças ao compartilhamento, podemos configurar bibliotecas como singletons em nossa configuração, garantindo que sejam carregadas apenas uma vez no sistema. Elas atuam como registradores ou barramentos, facilitando a comunicação entre os micro-frontends.
Dessa forma, formamos uma biblioteca widget registry, onde diferentes micro-frontends registram suas informações. Nossa plataforma ou outro componente pode então ler essa configuração e obter os dados necessários. Isso reduz significativamente o acoplamento, criando um ponto de interação unificado entre as aplicações. Se um produto decidir remover um widget ou web element, essa informação será refletida no registro atualizado, aumentando a estabilidade geral.
Lazy Load e Seus Benefícios
Discutimos web components como widgets, onde o dashboard principal é composto por eles. No entanto, um web component também pode servir como uma porta de entrada para uma aplicação web completa. Por exemplo, o módulo de monitoramento, como um produto separado, possui sua própria roteirização, módulos internos e múltiplas páginas. Carregar todo esse conjunto de uma vez não é uma abordagem eficiente.
Portanto, faz sentido utilizar o carregamento preguiçoso (lazy loading) de micro-frontends. Uma boa prática é ter múltiplos containers na aplicação. Criamos um container pequeno e leve que é carregado na inicialização, obtendo configurações específicas ou reagindo a eventos. O container maior e mais pesado, com a aplicação completa, é carregado sob demanda, apenas quando o usuário acessa a página correspondente. Todas as ferramentas necessárias para isso já estão disponíveis: router, element container, load remote.
Formamos outra biblioteca singleton, menu registry, semelhante aos nossos registradores. Diferentes micro-frontends registrarão suas informações, especialmente o segmento de URL e o web component que eles servirão. A plataforma lerá esses dados, formará um roteador dinâmico. O que acontece a seguir?
Quando o usuário navega para a seção "Monitoramento", verificamos se há algum registro para esse segmento. Em seguida, renderizamos o element container para o usuário e passamos todas as informações necessárias. O processo continua com:
- A função
load remoteé executada, carregando o container. - O container inicializa e inicia a aplicação web.
- A aplicação web define e identifica o web element.
- Este web element é então montado na página.
É importante lembrar que cada web element possui seu próprio roteador e eles não têm conhecimento um do outro. É necessário configurá-los dinamicamente para evitar conflitos. Por exemplo, se definirmos que o segmento de responsabilidade da plataforma é "monitoramento", o roteador correspondente cuidará dessa seção. Tudo o que estiver fora dela já pertence ao roteador do produto.
No entanto, os roteadores também precisam ser sincronizados. Quando acessamos a seção "Monitoramento" e montamos um web element, a aplicação "Monitoramento" e seu roteador não têm conhecimento disso. Precisamos notificá-los que um determinado tag está sendo montado. A aplicação de monitoramento ouvirá isso e poderá iniciar a inicialização ou o roteador apropriado para renderizar o componente correspondente.
Por sua vez, a aplicação de monitoramento executa sua própria navegação ou escreve parâmetros de URL, tudo isso ocorrendo dentro do roteador da aplicação "Monitoramento". Ao sair para qualquer outra seção, perdemos essa informação e o estado é destruído (a navegação "Voltar/Avançar" do navegador também não tem conhecimento disso). Portanto, a cada mudança em seu roteador, os micro-frontends executam um sync, notificando uns aos outros sobre as alterações. As aplicações ao redor simplesmente ouvem esses eventos, e o URL atua como a fonte da verdade, permitindo que elas atualizem o estado de seus roteadores.
Podemos configurar essas interações através de dispatchEvents clássicos. Anteriormente, mencionamos que podemos descrever eventos de ciclo de vida para web components. Estamos nos movendo em direção a bibliotecas singleton, que oferecem bons pontos de extensão (tanto para sync quanto para mount, podemos adicionar novas funcionalidades, como navigate para transições entre micro-frontends).
Otimizando a Arquitetura
Discutimos web components e aplicações. Agora, vamos falar sobre compartilhamento e como otimizar tudo isso.
Possuímos as bibliotecas widget e menu, que atuam como registradores em nosso contexto. Precisamos de apenas uma cópia, portanto, as marcamos explicitamente como singleton: true.
javascriptnew ModuleFederationPlugin({ shared: { '@platform/widget-registry': { requiredVersion: '>=1.0.0', version: '1.0.0', singleton: true }, '@platform/menu-registry': { requiredVersion: '>=1.0.0', version: '1.0.0', singleton: true }, '@angular/*': { requiredVersion: '19.2.0', version: '19.2.0' }, '@platform/buttons': { requiredVersion: '2.0.0', version: '2.0.0' } // ??? } });
Frameworks e Compartilhamento
Em relação a frameworks, especialmente Angular: não podemos impor nada rigidamente, mas podemos declarar que "Se as suas versões do Angular forem estritamente idênticas, a mesma aplicação pode reutilizar o Angular sem problemas." No entanto, bibliotecas escritas sobre nossos frameworks exigem atenção especial. Por exemplo, a biblioteca platform/buttons, parte do nosso UI kit, escrita sobre Angular. Pode parecer que, como sua versão 2.0 coincide com a da aplicação, podemos compartilhá-la.
Não podemos. Observe a imagem. Versões diferentes do Angular significam compiladores diferentes. Se pegarmos a mesma versão de botões, diferentes aplicações a compilarão de maneiras distintas, resultando em artefatos diferentes. Distribuir tal biblioteca não é possível. Mas se realmente quisermos, há uma solução.
Sabemos que o principal requisito da biblioteca buttons é a versão do Angular (seu principal peerDeps). Portanto, nossa aplicação "Monitoramento" é o fornecedor do framework que compilará e fornecerá essa biblioteca. Podemos também criar uma pequena utilidade auxiliar que receberá nossas necessidades: queremos compartilhar a biblioteca platform/buttons; seus peerDeps são Angular; então, precisamos consultar o JSON da aplicação "Monitoramento"; encontrar a versão do Angular; e com base nisso, criar uma versão sintética para a biblioteca. O resultado não será apenas a biblioteca 2.0, mas também uma menção com um sufixo indicando que é a versão 2.0 que vem com Angular 19.2.0.
Isso nos permite compartilhar a biblioteca de botões, mas apenas se a versão do Angular dessas bibliotecas coincidir (ou seja, se foram compiladas pelo mesmo compilador). Essa nuance é mais sensível em aplicações que incorporam instruções internas. Mas, tecnicamente, problemas podem surgir com bibliotecas comuns: se pegarmos a mesma biblioteca e diferentes aplicações fornecerem versões diferentes de peerDeps chave, isso pode criar conflitos.
Portanto, lembre-se: se uma biblioteca tem um peerDeps importante para seu funcionamento, ele também deve coincidir e ser compartilhado entre as aplicações.
Isolamento de Bibliotecas
Também vale mencionar bibliotecas que modificam o objeto Window (quando estendemos nossos objetos globais, e diferentes bibliotecas estendem as assinaturas de maneiras distintas). Na minha opinião, não há uma solução perfeita aqui, então restam medidas organizacionais. Por exemplo, simplesmente parar de fazer modificações e permanecer com uma das bibliotecas anteriores, ou aplicar um patch. Em resumo, garantir que tais bibliotecas possam coexistir em uma única página.
Quando se trata de uma abordagem arquitetônica que requer múltiplas versões, é comum encontrar um anti-padrão: "Menções são um dependency hell, nunca faça isso, é impossível trabalhar." E, em geral, sim, se você tiver um grande número de bibliotecas, elas entrarão em conflito. Imagine um zoológico de bibliotecas, onde você mesmo não entende completamente como (e por que) tudo funciona. Sim, é um dependency hell e é desagradável. Mas se você tiver um bom entendimento de como tudo funciona, não haverá problemas.
Algumas Conclusões sobre Bibliotecas Singleton:
- Escreva-as como agnósticas de framework (ou TypeScript puro, ou JavaScript puro).
- Lembre-se que não podemos expor a implementação. Por exemplo, ao escrever
widget registry, não podemos expor um método público para todos. Se você decidir atualizá-lo no futuro, a assinatura mudará, as aplicações em produção não saberão disso e tudo quebrará. A API deve ser estável: aberta para extensões, mas fechada para modificações.
Design System e Consistência de UI
Por um lado, nossas aplicações são independentes e os produtos têm a liberdade de escrever seus próprios componentes customizados. Por outro lado, precisamos criar uma experiência de usuário unificada. Estamos nos movendo em direção a um design system unificado; a própria plataforma carrega um conjunto global de variáveis CSS, e todas as aplicações as utilizam. Mas, assim como com bibliotecas singleton, é preciso lembrar da retrocompatibilidade e controlar rigorosamente quaisquer divergências. Afinal, se você começar a editar ou remover esses CSS sem critério, sempre haverá um produto em produção que os utiliza ativamente e que não ficará satisfeito com suas ações.
Algumas Recomendações:
- Crie seu próprio serviço de distribuição de estáticos: Esta é uma boa solução por si só, e também útil para implementar um mecanismo de fallback. Por exemplo, se você carregar um micro-frontend e ele derrubar tudo, ele pode ser simplesmente isolado. Na prática, isso resolve o problema em 99% dos casos.
- Desenvolva seus próprios serviços de monitoramento e logging: Para rastrear quais micro-frontends foram carregados, em que sequência e em quais combinações. Tudo isso ajudará a melhorar a qualidade do debug.
- Documentação: Quanto melhor você descrever suas soluções, mais confortável será para as equipes de produto trabalhar com a arquitetura.
Complexidades:
A variedade de micro-frontends descrita no artigo aumentará a complexidade da depuração. Um desenvolvedor pode dizer: "Ótimos interfaces, excelente documentação, mas nada funciona". E você não poderá simplesmente responder: "Temos 8 produtos assim e todos funcionam, mas o seu não". Será necessário ir e analisar o código dele. E lá, por exemplo, pode não estar o seu stack. E pronto. E não é só o stack – divergências em tabulações e linters também podem causar problemas.
Bugs de Integração: O obstáculo mais sutil. As aplicações parecem independentes, vivem e evoluem, mas estamos interessados em uma classe de erros que ocorre quando conectamos tudo em uma arquitetura unificada. Chamamos esses erros de "vazamentos" – vazamento de implementação, vazamento de Angular, vazamento de bibliotecas, etc.
Custos de Carregamento: Com mãos não muito habilidosas, pode-se criar um bundle muito desajeitado e grande. Sim, carregaremos vários frameworks e várias versões do Angular, mas nosso objetivo não é manter 10 aplicações, cada uma com seu próprio Angular, mas sim não bloquear a entrega de funcionalidades aos nossos clientes. Dizemos a todos: instalem, atualizem, tragam suas funcionalidades, e o débito técnico será resolvido quando for conveniente e houver tempo.
Ferramentas Comuns: A história em que é preciso ir além do trabalho habitual, pois será necessário escrever ferramentas, сборщики (build tools) e plugins para federação de módulos, além de se comunicar com engenheiros de DevOps. Nem todos estão preparados para esse tipo de trabalho.
Conclusão
Essa abordagem arquitetônica nos proporciona lançamentos verdadeiramente independentes: equipes, produtos e a plataforma dependem minimamente uns dos outros. Existem pontos unificados com contratos compartilhados, através dos quais o sistema é escalado.
Pessoalmente, encontro um benefício na flexibilidade: não impomos nada a ninguém e, na prática, trabalhamos sob o paradigma "Faça o que quiser, escolha a ferramenta conveniente". O preço disso é a complexidade da arquitetura, manutenção e depuração, além da necessidade de se comunicar com um grande número de equipes.
Na minha opinião, toda essa complexidade é justificada, pois nos ajudou a superar todos os desafios arquitetônicos que enfrentamos. E estamos prontos para novos.





