- read

Minha experiência montando uma arquitetura de Microfrontend

Christian Benseler 69

Microfrontends com Vue, Angular e React

Contextualizando

No começo de 2021 fui convidado a assumir o cargo de tech lead de frontend na Farm Investimentos, uma fintech voltada ao agronegócio. Trabalho desde sempre com front, em cargos específicos ou como fullstack e a experiência que eu tive com MFE (vou usar essa sigla para abreviar a expressão) foi no começo de 2010, quando o time em que trabalhava na MMCafé começou a desenvolver módulos em Ruby on Rails que se acoplavam a uma grande aplicação Java, tudo dentro da JVM, e a única forma na época de fazer os frontends se conversarem era com iframes.

Cenário Inicial

A necessidade, quando entrei na Farm, era a curto prazo participar do desenvolvimento de uma nova aplicação (que já havia sido iniciada) e, a longo, juntar o frontend de algumas aplicações já existentes e em produção, pensando em como deixar preparada a arquitetura, como um todo, para o cenário futuro: novas funcionalidades e serem desenvolvidas por um time de tech bem maior, com squads tocando partes específicas dos produtos digitais mantendo a experiência do usuário, tudo isso com a chance de crescimento muito rápido.
Depois de 2 meses, essa nova aplicação já estava pronta, então tínhamos… 3 aplicações!

Como dividimos

Quando entrei no time, comecei a ver o que tinha de tecnologia já sendo usada, e todo o front tinha sido construído com VueJS. Eu nunca tinha trabalhado com essa tecnologia (conhecia de estudos apenas), mas fica a dica pra quem vem do mundo Angular e especialmente React: a curva de aprendizado é pequena e já sai jogando rapidamente.
Em parceria com um outro dev do time, o Álvaro, a gente separou essa nova aplicação em módulos que não se conheciam: críamos uma pasta mfes dentro do src e dentro dela, uma pasta para o que seria cada módulo, com rotas, views, vuex, componentes próprios, etc… e na raiz de src, uma pasta shared/ com componentes gerais, helpers, libs.
Já tinhamos em mente o que faríamos depois…

Provando o Conceito

Ao fazer a entrega desse projeto, começamos a pensar (Álvaro, mais o Johny, tech lead também, e eu) na necessidade a longo prazo: como juntar os frontends dessas aplicações todas? Já sabíamos que iríamos para a abordagem de microfrontends, mas como operacionalizar?
Nada melhor do que uma PoC (Proof of Concept), algo com o que trabalhei bastante nos 3 anos em que estive no BVLab (Laboratório de Inovação do Banco BV).

Pesquisamos algumas abordagens, todas vendo os pontos que queríamos atingir:

  • boa dev experience (o setup tem que ser simples pro dev)
  • possibilidade de usar mais do que uma tecnologia de frontend
  • já ter sido bem testada e com cases de sucesso
  • simples de rodar em produção

A melhor opção para o nosso cenário se mostrou ser a single-spa e partimos para fazer o teste:

  • criamos uma aplicação nova Vue seguindo o passo-a-passo e colocamos dentro dela o conteúdo de um dos nossos módulos, apontando para rodar em dev na porta 9001
  • criamos ainda uma outra aplicação Vue, em branco, que seria o nosso segundo mfe, apontando para porta 9002
  • criamos a aplicação que a gente chama hoje de orquestrador: ela tem o entrypoint (o html), carrega a single-spa e tem a lógica de qual mfe será carregado, dependendo da rota. Em dev, o orquestrador roda na porta 5000

O exemplo na documentação do single-spa é super completo, mas resumidamente, o html do orquestrador tem uma série de placeholders

<div id=”appMFE1-placeholder” class=”mf-Container”></div>
<div id=”appMFE2-placeholder” class=”mf-Container”></div>
<div id=”appMFE3-placeholder” class=”mf-Container”></div>

e você cria uma lista de aplicações, que serão carregadas dependendo da sua lógica

const apps = [{
name: “app-mfe1”,
app: () => System.import(“app-mfe1”),
activeWhen: () => location.pathname.indexOf(“/users”) >= 0
},
{
name: “app-mfe2”,
app: () => System.import(“app-mf2”),
activeWhen: () => location.pathname.indexOf(“/auth”) >= 0
}];

E uma json define qual aplicação será importada

“imports”: {
“orchestrator”: “http://localhost:5000/main.js",
“app-mfe1”: “http://localhost:9001/app.js",
“app-mfe2”: “http://localhost:9002/app.js",
}

Resumidamente, o que ocorre:

  • rodamos o orquestrador na porta 5000 e ele carrega o index e os scripts
  • se acessar uma url com /users, o single-spa deixa o app-mfe1 ativa (activeWhen) e carrega o .js do app-mfe1, que está sendo service em http://localhost:9001/app.js
  • o main.js de cada app Vue foi alterado para ao invés de fazer o build de uma aplicação web, renderizar dentro de um elemento do DOM. Exemplo:
const containerSelector = ‘#app-mfe1’;const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
render: h => h(App),
el: containerSelector,
router,
vuetify,
store,
i18n,
},
});

Fizemos os testes, criando links numa navbar do html do orquestrador e funcionou perfeitamente: ao navegar entre contextos /users e /admin, os mfes eram trocados de forma natural.

Testando a PoC

O teste em tempo de desenvolvimento foi um sucesso, então nosso próximo passo era testar em um ambiente "de verdade", no caso AWS (nosso cloud provider). Na mão (sem um CI/CD), fizemos build (npm run build) dos 3 projetos e mudamos o .json de importação do orquestrador:

“imports”: {
“orchestrator”: “/main.js",
“app-mfe1”: “/mfe1/app.js",
“app-mfe2”: “/mfe2/app.js",
}

Criamos um bucket na S3 e jogamos o conteúdo do orquestrador na raiz dele e o dist de cada mfe dentro de pastas mfe1 e mfe2; configuramos o bucket e… tudo funcionando!

Wrapping Up (ou: juntando tudo)

Então fizemos uma bela força-tarefa:

  • criar projetos no Github para todos os módulos da nossa aplicação, colocando em cada um a estrutura base do projeto do single-spa
  • CI/CD (com Github Action) para rodar testes, lint, enviar para o bucket cada projeto em sua pasta específica, ambientes de dev/uat/prd, invalidar cache, etc…
  • projeto do orquestrador mais "parrudo", com libs que podem ser compartilhadas os mfes (como todas usam Vue, Vuex, etc… é possível fazer com que o webpack ignore essas na hora do build e falar para pegar de um external)
  • criamos um projeto de componentes, para onde movemos todo o conteúdo do que era shared/ e agora temos um projeto independente com os componentes que são compartilhados entre todos (e faz parte do nosso design system)
Exemplo de componente no projeto compartilhado, documentado com Storybook

Cenário Atual

Temos atualmente, além do orquestrador, 9 mfes fazendo parte do nosso projeto:

  • Conseguimos de forma muito simples migrar funcionalidades das 2 aplicações que já estavam em produção, ou criando mfes novos ou encaixando dentro de alguns que já existiam (por conta da duplicidade que existia).
  • Temos a certeza, quando estamos trabalhando em um mfe específico, que nada de outra área vai ser afetada, o que garante uma tranquilidade, tanto para a equipe de tech quanto de produto
  • Com a criação do projeto de componentes, melhoramos o nosso design system, tanto na qualidade final como na documentação (com o Storybook)
  • Processo de manutenção, build, deploy egovernança menos custoso: só alteramos uma pequena parte de um pequeno projeto, e não uma pequena parte de um grande projeto que necessitaria de um build gigante, testes regressivos, etc…
  • Com essa separação em projetos, facilitamos a organização de squads que serão montadas e que serão donas de projetos já divididos

Mas e outras tecnologias?

No começo do texto eu comentei que um dos requisitos era poder usar mais do que uma tecnologia de frontend… e já estamos!
Aproveitamos um projeto que já tinha sido feito, de autenticação, em Angular, e seguindo também a documentação do single-spa conseguimos colocar ele pra rodar junto com os demais (exemplo: quando entra numa rota /auth, carrega esse mfe). Também fizemos o teste com um projeto React dummy e funcionou.
Então, apesar de não ser nosso plano (por diversos motivos, como tentar manter uma stack relativamente homogênea e consistência no code base), se necessário conseguiremos trabalhar com diversas tecnologias diferentes e ai daremos mais flexibilidade às squads.

Próximos Passos

Em termos da arquitetura dos microfrontends, como o projeto não foi para produção ainda, o que vamos fazer é mais testes nos ambientes pré e produtivo e refinar a observability deles, além de melhorar a performance (no primeiro carregamento de um mfe, ainda há um gargalo, pois o build de cada mfe ainda leva diversos assets e libs; precisamos fazer um refinamento disso).
Em paralelo, temos os processos internos, de definição de squads tomando conta de alguns mfes e ver como será essa dinâmica (todos desenvolvem em separado, mas o produto final pro usuário deve ser uma aplicação única).
Além disso, estamos tendo o cuidado de não granularizar demais os mfes, para não perdemos a mão (em quantidade de projetos, na gestão deles, etc).

De qualquer maneira, o processo de pesquisa, testes (PoC) e implementação (esse, principalmente) foi bem mais tranquilo do que eu imaginava que seria e, até agora, o resultado tem sido excelente.