Microfrontends con Module Federation

0
2103

Índice

Introducción

En este tutorial repasaremos el concepto de microfrontends y aprenderás a crear uno paso a paso desde cero con las tecnologías de Module Federation y React. Aunque ya se ha explorado el concepto de microfrontends en otros tutoriales, este artículo se enfocará en la implementación práctica. Desglosaremos cada paso para que puedas aplicar esta estrategia desde cero

¿Qué son los microfrontends?

Los microfrontends son un patrón arquitectónico en la construcción de aplicaciones web que consiste en dividir la aplicación web en partes más pequeñas y autónomas, que se pueden desarrollar, testar y desplegar de forma independiente.

Esto permite a los equipos de desarrollo trabajar de forma más independiente y permite a las empresas escalar sus aplicaciones web de manera más eficiente.

¿Qué es Module Federation?

Module Federation es una característica de Webpack 5 que permite a las aplicaciones web compartir módulos y dependencias de JavaScript entre sí en tiempo de ejecución de forma dinámica sin la necesidad de incluirlos en la compilación.

Nuestro primer microfrontend

En nuestro caso de ejemplo, nuestra aplicación va a constar de una cabecera simple y una galería de imágenes. Crearemos dos aplicaciones Home y Header. La primera aplicación contendrá la galería de imágenes y actuará como host o shell. Esta, en tiempo de ejecución, inyectará nuestro componente remoto, la cabecera.

 

Boceto del diseño de la aplicación a crear

Crear las aplicaciones e instalar Webpack 5

Para ahorrar pasos en este caso vamos a crear las dos aplicaciones React utilizando, create-react-app aunque podrías hacerlo de forma manual o incluso utilizarlo en tu propia aplicación, creando tu propio fichero webpack.config.js

npx create-react-app home-app
cd home-app
npm install --save-dev webpack webpack-cli html-webpack-plugin webpack-dev-server babel-loader css-loader
npx create-react-app header-app
cd header-app
npm install --save-dev webpack webpack-cli html-webpack-plugin webpack-dev-server babel-loader css-loader

Crear la galería y la cabecera

Con el esqueleto de las aplicaciones creadas vamos a crear la galería de imágenes. En mi caso he hecho una galería de imágenes simple con una pequeña animación. Personaliza las dos aplicaciones como desees y hasta el punto que quieras. A modo de ejemplo te dejo cómo lo he hecho yo.

Galería de imágenes

Aplicación home en el navegador

import "./App.css";
import React from "react";

function App() {
  return (
    <div className="wrapper">
      <Gallery />
    </div>
  );
}

const Gallery = () => {
  return (
    <section>
      <img
        src={`https://picsum.photos/seed/${Math.random()}/1920/1024`}
        alt="random image"
      />
      <img
        src={`https://picsum.photos/seed/${Math.random()}/1920/1024`}
        alt="random image"
      />
      <img
        src={`https://picsum.photos/seed/${Math.random()}/1920/1024`}
        alt="random image"
      />
      <img
        src={`https://picsum.photos/seed/${Math.random()}/1920/1024`}
        alt="random image"
      />
      <img
        src={`https://picsum.photos/seed/${Math.random()}/1920/1024`}
        alt="random image"
      />
    </section>
  );
};

export default App;
.wrapper {
  height: 100vh;
  margin: 0 auto;
  max-width: 80%;
}

section {
  display: flex;
  height: 100%;
}

section img {
  flex-grow: 1;
  width: 0;
  object-fit: cover;
  opacity: 0.8;
  transition: 0.5s ease;
}

section img:hover {
  opacity: 1;
  width: 50%;
  filter: contrast(1.2);
}

Cabecera

Aplicación header en el navegador

import "./App.css";
import React from "react";

function App() {
  return <Header />;
}

function Header() {
  return (
    <header>
      <h1>My Gallery</h1>
    </header>
  );
}

export default App;
header {
  background-color: #222;
  padding: 20px;
  text-align: center;
  font-size: 35px;
  color: white;
}

header h1 {
  font-size: 35px;
  margin: 0;
  padding: 20px;
}

Crear nuestra propia configuración de Webpack

Vamos a crear dos ficheros webpack.config.js en la raíz de ambas aplicaciones. Recuerda configurar un puerto diferente para cada aplicación.

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  entry: "./src/index.js", // Punto de entrada de la aplicación
  module: {
    rules: [
      {
        test: /\\.(js|jsx)?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: ["@babel/preset-env", "@babel/preset-react"],
            },
          },
        ],
      },
      {
        test: /\\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
  devServer: {
    port: 3000, // Puerto en el que se ejecutará el servidor de desarrollo. 3001 para la otra aplicación
  },
};

Dado que hemos creado nuestra propia configuración de Wepbpack, debemos modificar los ficheros public/index.html de ambas aplicaciones para que no haya referencias a la variable %PUBLIC_URL%.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React with Webpack</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Para permitir que Webpack cargue asíncronamente todas las dependencias necesarias para renderizar la aplicación remota, necesitaremos renombrar los ficheros index.js por boostrap.js y crear dos nuevos ficheros index.js que importen dinámicamente nuestras aplicaciones.

import('./bootstrap.js')

Una vez hecho esto solamente nos queda modificar los scripts del package.json para que nuestras aplicaciones utilicen nuestra configuración de Webpack y lanzar nuestras aplicaciones.

"scripts": {
    "start": "webpack serve",
    "build": "webpack --mode production"
}
npm run start

Configuración Module Federation

Con la configuración básica de Webpack hecha podemos hacer uso del plug-in ModuleFederationPlugin para definir que módulos y dependencias vamos a compartir entre nuestras aplicaciones.

Volvemos al fichero de configuración webpack.config.js de la aplicación header y añadimos la siguiente configuración del plug-in.

...
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const { dependencies } = require("./package.json");

module.exports = {
  ...
  plugins: [
		...
    new ModuleFederationPlugin({
      name: "headerApp",
      filename: "remoteEntry.js",
      exposes: {
        "./Header": "./src/App",
      },
      shared: {
        ...dependencies,
        react: {
          singleton: true,
          requiredVersion: dependencies["react"],
        },
        "react-dom": {
          singleton: true,
          requiredVersion: dependencies["react-dom"],
        },
      },
    }),
  ],
};

De las propiedades que hemos configurado destacamos las siguientes:

  • name: Especifica un nombre único para tu módulo o aplicación. Este nombre se utilizará para identificar tu módulo cuando sea consumido por otros módulos remotos.
  • filename: Establece el nombre del archivo de entrada remota. Este archivo contiene la información necesaria para que otros módulos puedan cargar y utilizar los módulos expuestos por tu aplicación.
  • exposes: Define los módulos locales que deseas exponer para ser utilizados por otros módulos remotos. El objeto exposes tiene pares clave-valor donde la clave es el «identificador» del módulo que se utilizará desde los módulos remotos, y el valor es la ruta local al módulo en tu aplicación. En nuestro caso, lo que queremos exponer es el fichero App.js que contiene el código de nuestra cabecera bajo el identificador ./Header
  • shared: Especifica las dependencias que deben ser compartidas entre los módulos remotos. Esto evita que las mismas dependencias se incluyan múltiples veces en diferentes bundles, lo que puede reducir el tamaño total del bundle. En este ejemplo, se comparten las dependencias 'react' y 'react-dom' además de configurar que funcionen en modo singleton para evitar efectos no deseados al haber varias instancias de react en la misma página

 

Solamente nos falta añadir la configuración a nuestra aplicación home.

...
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const { dependencies } = require("./package.json");

module.exports = {
  ...,
  plugins: [
	  ...,
    new ModuleFederationPlugin({
      name: "homeApp",
      filename: "remoteEntry.js",
      remotes: {
        headerApp: "headerApp@http://localhost:3000/remoteEntry.js",
      },
      exposes: {},
      shared: {
        ...dependencies,
        react: {
          singleton: true,
          requiredVersion: dependencies["react"],
        },
        "react-dom": {
          singleton: true,
          requiredVersion: dependencies["react-dom"],
        },
      },
    }),
  ],
};
  • remotes: Define los módulos remotos que tu aplicación utilizará desde otros módulos. La propiedad remotes es un objeto donde la clave es el nombre del módulo remoto y el valor es la dirección URL del archivo remoteEntry.js del módulo remoto.En nuestro ejemplo, headerApp es el nombre del módulo remoto y http://localhost:3000/remoteEntry.js es la URL donde se encuentra el archivo remoteEntry.js del módulo remoto.

 

Una vez configurado esto ya podemos hacer uso del componente ./Header expuesto por nuestra aplicación Header en nuestra aplicación Home.

import "./App.css";
import React from "react";
import Header from "headerApp/Header";

function App() {
  return (
    <div className="wrapper">
      <Header />
      <Gallery />
    </div>
  );
}
...

Lanzamos nuestras aplicaciones de nuevo y nos dirigimos a http://localhost:3001. Ya lo tenemos! Hemos creado un pequeño ejemplo de microfrontends.

Aplicación microfrontend completa

Conclusiones

En este tutorial, a través de un pequeño ejemplo, hemos explorado la arquitectura de microfrontends con Module Federation y como esta nos permite dividir nuestras aplicaciones en módulos autónomos, dotándonos de un desarrollo más ágil y escalable en entornos complejos.

La flexibilidad de esta estrategia la hace valiosa en múltiples escenarios. Pero en mi experiencia me ha tocado aplicarla en dos escenarios.

En entornos empresariales con múltiples equipos de desarrollo, esta estrategia nos permite un enfoque colaborativo sin perder productividad.

Otro caso claro es cuando tenemos que modernizar aplicaciones legadas. Este enfoque nos permite ir migrando el código de manera paulatina sin tener que rehacer todo el sistema.

En resumen, esta estrategia puede ser una solución poderosa en ciertos contextos como los que hemos mencionado. Sin embargo, su implementación debe ser evaluada en función de las necesidades y objetivos particulares de cada proyecto.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad