Índice
- Introducción
- ¿Qué son los microfrontends?
- ¿Qué es Module Federation?
- Nuestro primer microfrontend
- Conclusiones
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.
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
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
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 ficheroApp.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 archivoremoteEntry.js
del módulo remoto.En nuestro ejemplo,headerApp
es el nombre del módulo remoto yhttp://localhost:3000/remoteEntry.js
es la URL donde se encuentra el archivoremoteEntry.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.
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.