Microfrontend: Registro dinâmico de componentes (module federation)
Tags: webdev, javascript, webpack, frontend
Published: 2023-03-23 15:38:20

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_)

```javascript

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_)

```javascript

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

```javascript

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:

```javascript
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
```javascript

// ...
new ModuleFederationPlugin({
    remotes: {
        "AppContainer": "RemoteComponent1@https://host-of-component/remoteEntry.js"
    }
})
// ...

```

Registro dinâmico com a função loadRemoteComponent

```javascript
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.