Microfrontend: Registro dinâmico de componentes (module federation)
Alison Rodrigues
Posted on October 26, 2022
Hoje em dia existem várias formas de se construir aplicações front end distribuídas. Também existem inúmeros frameworks para facilitar a vida de quem está construindo um ambiente de componentes distribuídos e diferentes formas de gerenciar dependências e orquestrar os componentes visíveis no browser.
Neste artigo vou utilizar o "module federation", um modelo arquitetural proposto por Zack Jackson e outros Core Developers do Webpack. Como o module federation é uma especificação, existem implementações em diferentes Module bundlers como o Rollup. Porém a que está com a api mais avançada é mesmo o Webpack. Então os exemplos desse artigo são escritos utilizando do plugin ModuleFederation do Webpack.
Para que a arquitetura de module federation funcione, existem dois papéis, a application shell e o remote component. Esses papeis podem ser atribuídos a duas apps no mesmo projeto ou em projetos distintos (o mais comum é construí-los em projetos distintos).
O remote component deve informar quais trechos de códigos quer expor para serem utilizados em outras aplicações. Por exemplo uma classe, um componente, uma função e etc. E a application shell precisa ter registrado onde estão os componentes remotos que serão importados.
webpack.config.js (remote component)
module.exports = {
//...
plugins: [
new ModuleFederationPlugin({
name: 'RemoteComponent1', // nome do módulo remoto
filename: "remoteEntry.js",
exposes: {
"./AppContainer": "./src/App.js" // arquivos exportados, disponíveis para serem importados por outra aplicação
},
// ...
}),
],
};
Exemplo de como exportar um componente utilizando o plugin ModuleFederationPlugin
webpack.config.js (application shell)
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
name: "container",
filename: "containerEntry.js",
remotes: {
"AppContainer": "RemoteComponent1@https://host-of-component/remoteEntry.js"
},
shared: {
// ...
}
})
]
};
Exemplo de como configurar o plugin na aplicação shell para que seja possível encontrar o componente remotamente
Eis o problema
Como podemos ver, o registro dos componentes remotos acontece de forma estática através da propriedade remotes
na configuração do plugin. Mas e se quisermos adicionar um novo componente? E se tivermos que atualizar o endereço http de algum dos componentes remotos? E se haver um controle de versão no projeto do componente remoto e o arquivo remoteEntry.js for gerado dinamicamente com diferentes nomes para diferentes versões?
Para todos esses casos e muitos outros, terá que ser alterado o arquivo webpack.config.js
da aplicação shell e compilar o projeto todo novamente. Mesmo que não tenha sido alterado uma linha se quer de javascript na aplicação shell, o projeto tem que ser compilado novamente, ir para produção novamente, por conta de uma alteração em outro projeto. Ou seja pode acontecer de ser necessário recompilar a aplicação shell inúmeras vezes por causa dos projetos de componentes.
Eis a solução
O webpack + module federation fornece para nós uma API para que os componentes possam ser registrados dinamicamente. 🤩🤩🤩
Afinal esse registro acontece em tempo de execução, então nós podemos fazer manualmente a mesma coisa que o webpack faz!!!
O pulo do gato
Com o poder de registar os módulos remotos em suas mãos, você pode ler a lista de módulos remotos de qualquer lugar e não somente de um registro estático. Pode ser de um json hospedado em outro lugar ou até mesmo de algum cadastro servido por uma API (situação recomendada).
Mão na massa
A sintaxe para importação de um modulo remoto é muito simples, precisamos apenas de nome do componente, nome do módulo e a url, Exatamente como pudemos ver no exemplo acima da configuração dos remotes
na aplication shell.
O script abaixo cria uma tag script no head do html para importar o script remoto e após o arquivo js ser importado instancia o modulo
async function loadRemoteComponent({scopeName, moduleName, url} = params) {
const moduleExist = await window[scopeName]?.get(moduleName)
return await moduleExist ? loadRemoteModule(scopeName, moduleName) : importRemoteScope(scopeName, moduleName, url)
}
async function importRemoteScope(scope, moduleName, url) {
const element = document.createElement("script")
element.type = "text/javascript"
element.async = true
element.setAttribute('id', `scope_${scope}`) // id caso você queria remover o import depois por algum motivo
element.src = url
let promise = new Promise((resolve, reject) => {
element.onload = async () => {
const mod = await loadRemoteModule(scope, moduleName)
resolve(mod)
}
element.onerror = async () => { reject('Não foi possivel carregar o modulo desejado') }
})
document.head.appendChild(element)
return promise
}
async function loadRemoteModule(scope, moduleName) {
await __webpack_init_sharing__("default")
const container = window[scope]
await container.init(__webpack_share_scopes__.default)
const factory = await window[scope].get(moduleName)
const module = factory()
return module.default
// se você não trabalha com export default faça: return module
}
Esse trecho de código deve estar presente na sua aplicação shell.
E para instanciar seu componente basta chamar:
loadRemoteComponent({
moduleName: './AppContainer',
scopeName: 'RemoteComponent1',
url: 'http://https://host-of-component/remoteEntry.js'
});
Você pode ter cadastrado esses componentes em outro lugar e não de forma estática no arquivo webpack.confg.js
. Assim você faz uma requisição para essa lista de cadastro de componentes e os registra. Ou pode fazer o registro sob demanda, somente quando alguém pedir esse componente a sua aplicação shell.
Comparativo
Registro estático no webpack.config.js
// ...
new ModuleFederationPlugin({
remotes: {
"AppContainer": "RemoteComponent1@https://host-of-component/remoteEntry.js"
}
})
// ...
Registro dinâmico com a função loadRemoteComponent
loadRemoteComponent({
moduleName: './AppContainer',
scopeName: 'RemoteComponent1',
url: 'http://https://host-of-component/remoteEntry.js'
});
Gostou? Ficou com alguma dúvida? Deixa um comentário que eu respondo.
Posted on October 26, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.