Construindo uma plataforma com o Module Federation
Plataforma é um conceito cada vez mais evidente e tem se tornado cada vez mais necessário para empresas de tecnologia, varejo, serviços, streams e vários outros segmentos. De forma bem resumida, uma plataforma é uma base que fornece uma estrutura básica para que outros componentes possam se integrar a esta plataforma.
Por exemplo, carros são construídos a partir de uma plataforma, que é o esqueleto do carro, com essa plataforma podemos adicionar rodas, uma carroceria de uma pick-up ou de um SUV e adicionar os outros componentes que formam um carro.
Uma empresa como a Magazine Luiza, que tem lojas físicas e lojas virtuais, necessita de várias ferramentas digitais para vender seus produtos e serviços. Essas ferramentas são como exemplo, um catálogo de produtos em que pessoas da empresa cadastram as fotos, descrição, preço e etc. Além deste existem vários outros sistemas, cada um com suas responsabilidades. Agora imagine que você é um funcionário da empresa e diariamente acesse vários desses sistemas para fazer o seu trabalho. Imagine também que para cada sistema você precise abrir uma nova aba, fazer um novo login, lembrar qual é o link ou salvar nos seus favoritos. Para facilitar para as pessoas que trabalham no Magazine Luiza, construímos uma plataforma para as ferramentas de backoffice que fornece uma base para que esses sistemas se integrem à Plataforma para formar um local único em que usuários acessem as ferramentas que necessitam.
Quando começamos a desenvolver a plataforma, pensamos no que era necessário para que a integração de ferramentas fosse possível. A primeira necessidade que identificamos foi a autenticação, então construímos um serviço de autenticação de login único (single sign on) que é utilizado por todas as aplicações que se integram à Plataforma. Depois da autenticação, criamos ferramentas para cadastrar estas aplicações e cadastrar os menus dinâmicos, possibilitando o usuário navegar na aplicação que foi integrada dentro da plataforma. Com isso criamos a casca da nossa plataforma, que basicamente é a autenticação e uma tela base, que se tem a barra de menus e as ferramentas necessárias para adicionar outros sistemas.
Depois que criamos esta base, pensamos em como integrar todos estes sistemas em uma única tela. A primeira possibilidade que pensamos e construímos foi possibilidade de colocar iframes dentro da nossa plataforma, então qualquer aplicação pode se cadastrar na plataforma e criar um menu informando um título do menu e a url. Com isso, quando um usuário clicar neste menu cadastrado, a plataforma renderiza esta url dentro de um iframe na tela de plataforma.
O iframe foi uma solução rápida e simples de se implementar, mas cada sistema que fosse integrar precisaria ter uma estrutura e uma url para disponibilizar este front-end. A plataforma nasceu com o objetivo de ser a estrutura necessária para que outros sistemas se integrem usando o mínimo de recursos possíveis. Em nossas pesquisas encontramos um módulo do webpack 5, o Module Federation, que tem o objetivo de facilitar a construção de micro front-ends. Uma das funcionalidades disponíveis é o Dynamic Remote Containers que possibilita carregar bundles de aplicações de forma dinâmica, em tempo de execução.
Isso possibilitou que fosse carregado front-ends de outros sistemas na nossa plataforma, apenas fazendo algumas configurações. Com essa solução, criamos uma ferramenta na plataforma, para que outros times criem suas aplicações, gerem um bundle e enviem para a plataforma. A plataforma carrega este bundle e armazena em um bucket, depois de feito o cadastro de um novo menu, será possível renderizar a aplicação dentro da plataforma dinamicamente sem que essa aplicação necessite de uma nova estrutura.
Detalhes da implementação
Criamos o repositório module-federation-platform no github que tem o projeto que está descrito abaixo.
Tecnologias usadas:
- react
- react-router-dom
- react-app-rewired
- webpack 5
Quando desenvolvemos a solução de micro front-end com o Module Federation e até o momento da publicação deste artigo o react-scripts do create react app (cra), não tinha suporte ao webpack 5. Para não ejetar o react e continuar usando a facilidade do react-scripts, fizemos um fork do cra, criamos o pacote react-scripts-wp5 e isso foi disponibilizado no NPM. O que difere esse pacote para o oficial, é que fizemos as adaptações para suportar o webpack 5. Quando a versão oficial do react-scripts suportar o webpack 5, vamos simplesmente substituir o react-scripts-wp5 pelo react-scripts e o projeto continuará funcionando normalmente.
O Module Federation precisa algumas configurações simples e novamente para não ter que ejetar o react-scripts usamos o react-app-rewired para sobrescrever as configurações do webpack. Os desenvolvedores do react-scripts já estão fazendo alguns testes para configurar o Module Federation usando o react-scripts sem ter que usar este tipo de solução ou ejetar, então quando esta solução estiver pronta, não precisaremos mais utilizar o react-app-rewired.
Primeiro passo foi configurar o module federation, como usamos o react-app-rewired, criamos o arquivo config-overrides.js na raiz do projeto.
Basicamente essa configuração informa ao module federation que estamos criando um módulo com o nome plataformaBackoffice e compartilhando as bibliotecas, react, react-dom e react-router-dom. Note que a opção eager está setada como true, isso fala para o module federation que essa biblioteca será compartilhada com outros módulos. Com isso os bundles que forem integrar na plataforma, não terão essas bibliotecas nos seus bundles e utilizará da plataforma.
Depois configuramos o package.json, para título de exemplo, colocamos somente os pacotes necessários para a solução. Observe que os scripts de start e build usam o react-app-rewired e o react-scripts-wp5.
Agora configuramos nossa aplicação react, primeiro criamos o index.js em src/index.js.
Depois criamos o componente base do projeto, App.jsx em src/App.jsx.
O componente BundleRender é o responsável por fazer o carregamento de um bundle. Quando o usuário navega por exemplo https://plataforma-exemplo.com/bundle/615/home, o componente pega o path param id (615) e monta a url que busca o bundle. A url é a url do bucket, que estão armazenados os bundles, o id da aplicação e o remoteEntry.js, então neste caso montaria uma url como essa https://plataforma.exemplo-bucket.com/615/remoteEntry.js. O componente só precisa carregar o script remoteEntry.js, que é gerado pelo bundle do webpack com o module federation, e o próprio remoteEntry.js é responsável por carregar os demais scripts de acordo com a necessidade.
No front-end da plataforma é isso que precisamos para renderizar outras aplicações dentro da plataforma. Agora veremos como é feito a configuração nas aplicações para gerar um bundle e enviar para a plataforma.
Começamos com o package.json. Um detalhe importante é o name do package.json, este name é usado pelo webpack para criar contextos de aplicações e carregar os bundles e chunks e se duas aplicações tiverem o mesmo nome, causará problemas que são difíceis de serem encontrados. Note que temos um script zip que carrega os arquivos gerados na pasta bundle e gera um zip com estes arquivos. Este zip será enviado para a plataforma e ela descompacta os arquivos e envia para um bucket, na pasta com o id da aplicação.
Depois configuramos o module federation, a configuração é semelhante com a configuração na plataforma, mas aqui definimos o filename, exposes. Note que no shared as bibliotecas não são definidas como eager, com isso o module federation carrega as bibliotecas da plataforma e não as inclui no bundle.
Depois de configurar o module federation, podemos criar a nossa aplicação. A base da aplicação é o componente AppExport.js, neste componente envolvemos ele com um Switch do react-router-dom e dentro dele é renderizado todas as rotas da aplicação. No exemplo temos apenas um arquivo definindo os componentes, mas podemos construir a aplicação como uma aplicação react normal, inclusive usando o lazy loading para fazer code splitting e vários componentes. Note também que as rotas têm um prefixo, que é /bundle/APPLICATION_ID, no caso do exemplo /bundle/615, esse prefixo é necessário para que as rotas funcionem corretamente tanto na plataforma, quanto na aplicação. A plataforma cuida das rotas até /bundle/615 depois as rotas /bundle/615/user e /bundle/615/user/register são gerenciadas pela aplicação.
Pronto, já temos uma base de uma aplicação para ser integrado na plataforma, a partir dessa base é possível construir uma aplicação complexa em react, depois gerar o bundle, enviar para a plataforma e já está pronto para ser renderizado. Vale ressaltar que as aplicações somente serão carregadas quando o usuário acessar uma rota, não será carregado nenhuma aplicação antes que a rota seja acessada.