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.
Top comments (0)