Importante: Esta primera parte de la serie de artículos muestra usar Module Federation con un simple ejemplo sólo usando TypeScript
Traducción en español del artículo original de Manfred Steyer "The Microfrontend Revolution: Module Federation in Webpack 5" publicado el 22 diciembre 2020

Module Federación viene integrado con Webpack a partir de la versión 5 y permite la carga de partes de una aplicacion compiladas por separado. Por lo tanto, finalmente proporciona una solución oficial para la implementación de micro frontends.

Hasta ahora, al implementar micro frontends, había que rebuscar y hacer mucha magia. Una de las razones es seguramente que las herramientas de compilación y los frameworks no conocen este concepto y Module Federation inicia un cambio de rumbo en este sentido.

Module Federation presenta un enfoque para referenciar partes de una aplicacion que aún no se conocen en tiempo de compilación y estas pueden ser micro frontend autocompilados. Además, las partes individuales de la aplicacion pueden compartir bibliotecas entre sí, de modo que los proyectos no necesiten tener tener bibliotecas duplicadas.

En este artículo, voy a mostrar cómo utilizar la Module Federación un ejemplo sencillo. El código fuente se puede encontrar aquí.

Ejemplo

El ejemplo utilizado aquí consiste en un shell, que es capaz de cargar micro frontend individuales, proporcionados por separado si es necesario:

La shell está representada aquí por la barra de navegación negra, otro micro frontend  enmarcada. Esta se puede  iniciar sin un shell.

Esto es necesario para permitir el desarrollo y las pruebas por separado. También para los clientes como los dispositivos móviles, que sólo tienen que cargar la aplicación necesaria.

Conceptos de Module Federation

En el pasado, la implementación de escenarios como el mostrado aquí era difícil, especialmente porque herramientas como Webpack asumen que todo el código de la aplicacion está disponible cuando se compila. El Lazy loading es posible, pero sólo de las áreas que se separaron durante la compilación.

Con las arquitecturas de micro frontend queremos poder compilar y proporcionar las partes individuales de la aplicación por separado. Además, hacer referencias mutuas a través de la respectiva URL. Por lo tanto, sería deseable contar con codigo como este:

import('http://other-microfrontend');

Como esto no es posible, tenemos que recurrir a enfoques  de carga manual de scripts. Afortunadamente, esto cambiará con el Module Federation en Webpack 5.

La idea que hay detrás es sencilla: Un llamado host hace referencia a un remoto usando un nombre configurado y este nombre no se conoce en tiempo de compilación:

Esta referencia sólo se resuelve en tiempo de ejecución mediante la carga de un llamado punto de entrada remoto. Se trata de un script mínimo que proporciona la url externa real para dicho nombre configurado.

Implementación del Host

El host es una aplicación JavaScript que carga un remoto cuando se necesita. Para ello se utiliza una importación dinámica.

El host carga el componente mfe1/componente de esta manera mfe1 es el nombre de un remoto configurado y componente el nombre de un archivo que se proporciona.

const rxjs = await import('rxjs');

const container = document.getElementById('container');
const flightsLink = document.getElementById('flights');

rxjs.fromEvent(flightsLink, 'click').subscribe(async _ => {
    const module = await import('mfe1/component');
    const elm = document.createElement(module.elementName);
    […]    
    container.appendChild(elm);
});

Normalmente, Webpack tendría en cuenta esta referencia al compilar y dividiría un paquete separado para ella. Para evitarlo, se utiliza el ModuleFederationPlugin:

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

[…]
 output: {
      publicPath: "http://localhost:5000/",
      uniqueName: 'shell',
      […]
 },
plugins: [
  new ModuleFederationPlugin({
    name: "shell",
    library: { type: "var", name: "shell" },
    remoteType: "var",
    remotes: {
      mfe1: "mfe1"
    },
    shared: ["rxjs"]
  })
]

Con su ayuda se define el remoto mfe1 (Micro Frontend 1). La configuración mostrada aquí mapea el nombre interno de la aplicación mfe1 al mismo nombre oficial. Webpack no incluye ninguna importación que ahora se relacione con mfe1 en los paquetes generados en tiempo de compilación.

Las bibliotecas que el host debe compartir con los remotes se mencionan en el array shared. En el caso mostrado, se trata de Rxjs. Esto significa que toda la aplicación sólo necesita cargar esta biblioteca una vez. Sin esta especificación, rxjs terminaría en los paquetes del host así como en los de todos los remotos.

Para que esto funcione sin problemas, el host y el remoto deben acordar una versión común.

Además de la configuración del ModuleFederationPlugin, también necesitamos colocar algunas opciones en la sección output. El publicPath define la URL bajo la cual la aplicación puede ser encontrada posteriormente. Esto revela dónde se pueden encontrar los paquetes individuales de la aplicación, pero también sus assets, por ejemplo, imágenes o estilos.

El uniqueName se utiliza para representar el host o remoto en los bundles generados. Por defecto, webpack utiliza el nombre de package.json para esto. Para evitar conflictos de nombres cuando se utiliza Monorepos con varias aplicaciones, se recomienda establecer el uniqueName manualmente.

Cargando Bibliotecas compartidas

Para cargar bibliotecas compartidas, debemos utilizar importaciones dinámicas:

const rxjs = await import('rxjs');

Al ser llamadas asincronas, esto le da a webpack el tiempo necesario para decidir qué versión usar y cargarla. Esto es especialmente importante en los casos en los que diferentes remotes y hosts utilizan diferentes versiones de la misma bibliotecas. En general, webpack intenta cargar la versión más compatible.

Mas adelante hablaremos sobre  la gestion de versiones entre micro frontends.

Para evitar este problema, es una buena idea cargar toda la aplicación con importaciones dinámicas en el punto de entrada utilizado. Por ejemplo, el micro frontend podría usar un main.ts que se vea así:

import('./component');

Esto le da a webpack el tiempo necesario para la negociación y la carga de las bibliotecas compartidas cuando la aplicación se inicia. Por lo tanto, en el resto de la aplicación siempre se pueden utilizar importaciones estáticas  como:

import * as rxjs from 'rxjs';

Implementando el Remote

El remote también es una aplicación independiente y en este caso se basa en Web Components:

class Microfrontend1 extends HTMLElement {

    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
    }

    async connectedCallback() {
        this.shadowRoot.innerHTML = `[…]`;
    }
}

const elementName = 'microfrontend-one';
customElements.define(elementName, Microfrontend1);

export { elementName };

En lugar de web components, también se puede utilizar cualquier component creados por un framework. En este caso, los frameworks deben ser compartidos entre los remotos y el host.

La configuración webpack del remote, que también utiliza el ModuleFederationPlugin, exporta este componente con la propiedad exposes bajo el nombre component:

output: {
      publicPath: "http://localhost:3000/",
      uniqueName: 'mfe1',
      […]
 },
 […]
 plugins: [
    new ModuleFederationPlugin({
      name: "mfe1",
      library: { type: "var", name: "mfe1" },
      filename: "remoteEntry.js",
      exposes: {
        './component': "./mfe1/component"
      },
      shared: ["rxjs"]
    })
]    

El nombre del componente hace referencia al archivo. Además, esta configuración define el nombre mfe1 para el remoto. Para acceder a la remote, el host utiliza una ruta formada por los dos nombres configurados, mfe1 y component. El resultado es la instrucción que se muestra arriba:

import('mfe1/component')

Sin embargo, el host debe conocer la URL en la que encuentra mfe1. La siguiente sección muestra cómo se puede lograr esto.

Conectar el host con el remoto

Para dar al host la opción de resolver el nombre mfe1, el host debe cargar un entryPoint del remoto. Este es un script que el ModuleFederationPlugin genera cuando se compila el remoto.

El nombre de este script se puede encontrar en la propiedad filename mostrada en la sección anterior. La url del micro frontend se toma de la propiedad publicPath. Esto significa que la url del remoto ya debe ser conocida cuando se compila.

Afortunadamente, ya existe un PR que elimina esta necesidad.

Ahora este script sólo debe ser integrado en el host:

<script src="http://localhost:3000/remoteEntry.js"></script>

En tiempo de ejecución se puede observar que la instrucción

import('mfe1/component');

Esto hace que el host cargue el remoto desde su propia url (que es localhost:3000 en nuestro caso):

Conclusión

Module Federation integrado en Webpack a partir de la versión 5 llena un gran vacío para los micro fronted. Por fin se pueden recargar las partes una aplicacion compiladas y suministradas por separado y reutilizar las bibliotecas ya cargadas.

Sin embargo, los equipos que participan en el desarrollo de este tipo de aplicaciones deben asegurarse manualmente de que las partes individuales interactúan.

Esto significa también que hay que definir y cumplir contratos entre los micro frontend individuales, pero también que hay que acordar una versión para cada una de las bibliotecas compartidas.

Hasta ahora, hemos visto que la Module Federation es una solución sencilla para crear micro frontends.

Plataforma de cursos gratis sobre programación