Muchas veces, al definir una arquitectura de software, surge la disyuntiva entre microfrontends dinámicos y estáticos. Quizás esta confusión se debe a que algunos equipos de desarrollo están condicionados por las definiciones tradicionales de los microservicios en el backend. Sin embargo, los microfrontends presentan desafíos y consideraciones diferentes debido a que están en el lado del cliente.

  • Microfrontends Estáticos: Son aquellos que están precompilados y empaquetados de forma independiente y se integran en la aplicación principal en tiempo de despliegue. La carga suele ser más rápida, pero cada cambio requiere una actualización completa de la aplicación principal.
  • Microfrontends Dinámicos: En este enfoque, los microfrontends pueden cargarse en tiempo de ejecución, lo que permite una actualización y despliegue independientes. La aplicación puede cargar módulos según sea necesario, optimizando el rendimiento y permitiendo mayor flexibilidad en las actualizaciones sin necesidad de reinstalar la aplicación principal.
Microfrontends Estáticos

Uno de los enfoques más simples para implementar micro frontends es descomponer un desarrollo en varios paquetes que luego se ensamblan en tiempo de compilación. Sin embargo, este es un uso completamente estático de micro frontends.

Ventajas:

  • Toda la información se conoce en tiempo de compilación, lo que permite optimizaciones, integraciones más profundas y verificaciones mejoradas.
  • Conduce a aplicaciones más rápidas y confiables.

Desventajas:

  • Un cambio significativo en cualquier micro frontend, como una adición o eliminación, requiere un cambio en la aplicación principal.
  • Cualquier cambio disparará una reconstrucción de toda la aplicación.

Casos de Uso Principales:

Sitios web que cambian lentamente o aplicaciones web más pequeñas, como puede ser el caso de una App tipo Tab par MS Teams.

Ejemplo de Implementación Estática:

En el caso más simple, una solución de micro frontend estática solo incluye varios paquetes con un único punto de entrada. Vamos a considerar un ejemplo sencillo usando Node.js con Express para ilustrar esto. Configuraremos y utilizaremos un mono repositorio (monorepo) para transportar una solución de servidor que viene con múltiples paquetes de micro frontends. La idea también funcionaría sin un monorepo.

########################
# Crear el proyecto Node  
########################

npm init -y  
# Convertirlo en un monorepo Lerna  
npx lerna init --packages="packages/*"  
# Añadir la aplicación en sí  
npx lerna create @aom/app --yes  
npm init -w packages/app -y  
# Añadir algunos (por ejemplo, 2) micro frontends  
npx lerna create @aom/microfrontend-1 --yes  
npm init -w packages/microfrontend-1 -y  
npx lerna create @aom/microfrontend-2 --yes  
npm init -w packages/microfrontend-2 -y  
# Registrar las dependencias  
npm i @aom/microfrontend-1 -w @aom/app  
npm i @aom/microfrontend-2 -w @aom/app  
npm i express  

####################
# Codigo par Node.js
####################
const express = require('express');  
const app = express();  
const port = process.env.PORT || 1234;  
  
// Solo una página de índice "/"  
app.get('/', (_, res) => {  
  res.send('<html><head><title>Sample</title></head><body data-rsssl=1 data-rsssl=1><h1>Index</h1></body></html>');  
});  
  
// Configurar los micro frontends  
require('@aom/microfrontend-1')(app);  
require('@aom/microfrontend-2')(app);  
  
app.listen(port, () => {  
  console.log(`Running at ${port}.`);  
});  

############################
#Codigo para microfrontend-1
############################

const { join } = require('path');  
const express = require('express');  
  
module.exports = setupMicrofrontend1;  
  
function setupMicrofrontend1(app) {  
  const publicDir = join(__dirname, '..', 'public');  
  const middleware = express.static(publicDir);  
  app.use('/mf1', middleware);  
  app.get('/mf1', (_, res) => {  
    res.send('<html><head><title>Sample</title></head><body data-rsssl=1 data-rsssl=1><h1>MF1</h1></body></html>');  
  });  
}  

############################
#Codigo para microfrontend-2
############################

const { join } = require('path');  
const express = require('express');  
  
module.exports = setupMicrofrontend2;  
  
function setupMicrofrontend2(app) {  
  const publicDir = join(__dirname, '..', 'public');  
  const middleware = express.static(publicDir);  
  app.use('/mf2', middleware);  
  app.get('/mf2', (_, res) => {  
    res.send('<html><head><title>Sample</title></head><body data-rsssl=1 data-rsssl=1><h1>MF2</h1></body></html>');  
  });  
}  
Enfoque Dinámico

En el otro lado, un enfoque dinámico es mucho más difícil de implementar. Hay tres desafíos a abordar, que se describen a continuación:

  • Publicación de microfrontends: Se trata de cómo y dónde se almacenan y publican los microfrontends una vez que han sido desarrollados. En este caso puede ser un servidor, un sistema de almacenamiento de archivos o incluso una red CDN, donde los microfrontends se alojan y están disponibles para ser cargados en tiempo de ejecución.
  • Actualización: Se refiere al proceso de mantener esta «servidor» actualizado. Cuando se realiza una modificación en un microfrontend, debe ser capaz de almacenar la nueva versión sin interrumpir el funcionamiento de la aplicación principal y permitir que los usuarios accedan a las versiones más recientes.
  • Conexión de la aplicación principal: Este punto implica cómo la aplicación principal, o «shell» de la aplicación, se comunica para cargar los microfrontends cuando se necesiten. Esto incluye la lógica que permite a la aplicación decidir qué microfrontend cargar, en qué momento y desde qué ubicación específica dentro de la fuente.

Ventajas:

  • Los micro frontends pueden ser seleccionados a nivel de cada solicitud, lo que da a los desarrolladores mucha libertad.
  • Las actualizaciones de los micro frontends pueden ocurrir continuamente sin interrumpir la aplicación principal.

Desventajas:

  • La complejidad y el acoplamiento suelto conducirán a aplicaciones más frágiles.
  • Las herramientas adicionales y los límites de error están para ayudar, pero también añadirán más complejidad a nivel de infraestructura.

Casos de Uso Principales:

Sitios web personalizados en tiempo de ejecución o aplicaciones web más grandes. Un ejemplo de framework aquí es Module  Federation.

Ejemplo de Implementación Dinamica:

No tiene mucho sentido hacer esto con Node.js y Express, ya que lo que yo implementaría serían microservicios. Sin embargo, para no llenar de código de React o Angular que podría complicar su lectura, he optado por esta solución rápida que da continuidad al ejemplo anterior.

################
# Crear App Host
################

mkdir host-app  
cd host-app  
npm init -y  
npm install express webpack webpack-cli webpack-merge 
html-webpack-plugin @module-federation/webpack-5  

##################
# Crear App Remote
##################

mkdir remote-app  
cd remote-app  
npm init -y  
npm install express webpack webpack-cli webpack-merge 
html-webpack-plugin @module-federation/webpack-5  

########################
# Configurar Appp Remote
########################

# webpack.config.js en el directorio raíz de remote-app

const path = require('path');  
const { ModuleFederationPlugin } = require('webpack').container;  
  
module.exports = {  
  entry: './src/index.js',  
  target: 'node',  
  mode: 'development',  
  output: {  
    path: path.resolve(__dirname, 'dist'),  
    filename: 'remoteEntry.js',  
    libraryTarget: 'commonjs2',  
  },  
  plugins: [  
    new ModuleFederationPlugin({  
      name: 'remoteApp',  
      library: { type: 'commonjs-module' },  
      filename: 'remoteEntry.js',  
      exposes: {  
        './SharedModule': './src/sharedModule',  
      },  
      shared: [],  
    }),  
  ],  
};  

#Crea el módulo compartido que será expuesto en remote-app

// src/sharedModule.js  
module.exports = function () {  
  console.log('Hello from Shared Module!');  
};  

#Crea el archivo index.js en src para inicializar el servidor Express

// src/index.js  
const express = require('express');  
const app = express();  
const port = 3001;  
  
app.get('/', (req, res) => {  
  res.send('Hello from Remote App!');  
});  
  
app.listen(port, () => {  
  console.log(`Remote app listening at http://localhost:${port}`);  
});  

# Modifica el archivo package.json para usar Webpack

"scripts": {  
  "start": "webpack --config webpack.config.js && node dist/remoteEntry.js",  
  "build": "webpack --config webpack.config.js"  
}  

######################
# Configurar Appp Host
######################

# Crea o modifica el archivo webpack.config.js en el directorio raíz de host-app

const path = require('path');  
const { ModuleFederationPlugin } = require('webpack').container;  
  
module.exports = {  
  entry: './src/index.js',  
  target: 'node',  
  mode: 'development',  
  output: {  
    path: path.resolve(__dirname, 'dist'),  
    filename: 'bundle.js',  
    libraryTarget: 'commonjs2',  
  },  
  plugins: [  
    new ModuleFederationPlugin({  
      name: 'hostApp',  
      library: { type: 'commonjs-module' },  
      remotes: {  
        remote
Comparación entre Module Federation y Lerna

Esta comparación aborda las herramientas que impulsan los conceptos de microfrontends estáticos y dinámicos. Como cualquier persona que haya trabajado conmigo sabe, considero fundamental diferenciar la terminología de microservicio y microfrontend, ya que ambos tienen propósitos distintos y no deben confundirse. Aquí nos enfocamos en las herramientas, no en los principios de arquitectura subyacentes.

Antes de la llegada de herramientas como Lerna o Module Federation, solía implementar microfrontends alojados en servidores independientes, donde cada parte de la interfaz de usuario (frontend) se desplegaba por separado y comunicaba con otros microfrontends para intercambiar datos. Esta comunicación podía realizarse de dos formas principales:

  • Vía backend: Cada microfrontend se integraba indirectamente mediante servicios del backend que gestionaban la comunicación entre ellos. Por ejemplo, una aplicación de e-commerce podría tener un microfrontend para la visualización de productos y otro para el carrito de compras. Ambos microfrontends consultarían al backend para enviar y recibir datos. Si el usuario añadía un producto al carrito, el microfrontend de productos enviaría una solicitud al backend, que luego actualizaría el estado del carrito en el otro microfrontend.
  • Vía frontend directamente: También era (es) posible implementar comunicación directa entre microfrontends a través de eventos del navegador o el almacenamiento local (como localStorage o sessionStorage). Por ejemplo, el microfrontend de productos podía enviar un evento de JavaScript para notificar al microfrontend del carrito que se ha añadido un producto. Esto evitaba el paso por el backend, mejorando la velocidad de comunicación en ciertos casos.

Estas estrategias facilitaban la división de los frontends en componentes independientes, pero tenían limitaciones en cuanto a rendimiento y complejidad de gestión. Las herramientas modernas, como Lerna y Module Federation, han simplificado la integración y despliegue de microfrontends al gestionar mejor la comunicación y el acoplamiento entre módulos.

Lerna es una herramienta que facilita la gestión de proyectos que contienen múltiples paquetes dentro de un solo repositorio (monorepos). Es ideal para un enfoque estático donde los paquetes se ensamblan en tiempo de compilación.

    • Uso Principal: Aplicaciones que cambian lentamente o aplicaciones más pequeñas.
    • Ventajas: Optimizaciones en tiempo de compilación, integraciones más profundas, aplicaciones más rápidas y confiables.
    • Desventajas: Cualquier cambio significativo requiere una reconstrucción completa de la aplicación.

Module Federation es una funcionalidad de webpack que permite el consumo de módulos de otros proyectos en tiempo de ejecución. Es ideal para un enfoque dinámico donde los micro frontends se pueden actualizar independientemente.

    • Uso Principal: Sitios web personalizados o aplicaciones más grandes.
    • Ventajas: Selección de micro frontends a nivel de cada solicitud, actualizaciones continuas sin interrupciones.
    • Desventajas: Complejidad incrementada, aplicaciones más frágiles debido al acoplamiento suelto.
Conclusión

La elección entre un enfoque estático y uno dinámico para microfrontends depende en gran medida de las necesidades específicas del proyecto y de la estructura del equipo de desarrollo.

Ambos enfoques, tanto estático como dinámico, pueden beneficiarse del uso de monorepos.

Nota: Usar un monorepo con microfrontends estáticos tiende a simplificar el flujo de trabajo en comparación con microfrontends dinámicos, donde la complejidad puede aumentar con la necesidad de configurar módulos independientes y su actualización en tiempo real.