Diseno de Micro Frontends - Desarrollo Escalable de UI por Equipos Independientes

Avanzado | 2025.12.02

Que es Micro Frontend

Micro Frontend es un patron de arquitectura que aplica los conceptos de microservicios al frontend. Divide aplicaciones grandes en aplicaciones pequenas que pueden desplegarse de forma independiente.

flowchart TB
    subgraph Monolithic["Frontend Monolitico"]
        subgraph Single["Single Application"]
            M1["Product<br/>Module"]
            M2["Cart<br/>Module"]
            M3["Account<br/>Module"]
        end
        Note1["Un solo despliegue, un solo equipo"]
    end

    subgraph MicroFE["Micro Frontend"]
        subgraph Shell["Container App (Shell)"]
            Header["Shared Header"]
            subgraph Apps["Aplicaciones Independientes"]
                A1["Product App<br/>(Equipo A)"]
                A2["Cart App<br/>(Equipo B)"]
                A3["Account App<br/>(Equipo C)"]
            end
        end
        Note2["Despliegue independiente, equipos independientes, libertad de eleccion tecnologica"]
    end

Casos para Considerar su Adopcion

CasoRazon
Equipos grandes (10+ personas)Asegurar independencia entre equipos
Migracion gradual de sistemas legacyPosibilidad de coexistencia de tecnologias nuevas y antiguas
Diferentes ciclos de lanzamientoDespliegue independiente por funcionalidad
Organizacion orientada al dominioDivision de equipos por dominio de negocio
Diversidad de stack tecnologicoCoexistencia de React, Vue, Angular

Patrones de Integracion

1. Module Federation (Webpack 5)

flowchart TB
    subgraph Host["Host App (Shell)"]
        Config["webpack.config.js<br/>remotes: {<br/>  products, cart, account<br/>}"]
    end

    Host --> Products
    Host --> Cart
    Host --> Account

    subgraph Products["Products Remote"]
        P1["exposes:<br/>- List<br/>- Detail"]
    end

    subgraph Cart["Cart Remote"]
        C1["exposes:<br/>- Cart<br/>- Summary"]
    end

    subgraph Account["Account Remote"]
        A1["exposes:<br/>- Profile<br/>- Settings"]
    end
// host-app/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        products: 'products@http://localhost:3001/remoteEntry.js',
        cart: 'cart@http://localhost:3002/remoteEntry.js',
        account: 'account@http://localhost:3003/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

// products-app/webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'products',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/components/ProductDetail',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};
// host-app/src/App.tsx
import React, { Suspense, lazy } from 'react';

// Cargar componentes remotos con import dinamico
const ProductList = lazy(() => import('products/ProductList'));
const Cart = lazy(() => import('cart/Cart'));
const AccountSettings = lazy(() => import('account/Settings'));

function App() {
  return (
    <div>
      <Header />
      <Routes>
        <Route
          path="/products/*"
          element={
            <Suspense fallback={<Skeleton />}>
              <ProductList />
            </Suspense>
          }
        />
        <Route
          path="/cart"
          element={
            <Suspense fallback={<Skeleton />}>
              <Cart />
            </Suspense>
          }
        />
        <Route
          path="/account/*"
          element={
            <Suspense fallback={<Skeleton />}>
              <AccountSettings />
            </Suspense>
          }
        />
      </Routes>
    </div>
  );
}

2. Single-SPA

// root-config.ts
import { registerApplication, start } from 'single-spa';

// Registro de micro frontends
registerApplication({
  name: '@myorg/products',
  app: () => System.import('@myorg/products'),
  activeWhen: ['/products'],
});

registerApplication({
  name: '@myorg/cart',
  app: () => System.import('@myorg/cart'),
  activeWhen: ['/cart'],
});

registerApplication({
  name: '@myorg/account',
  app: () => System.import('@myorg/account'),
  activeWhen: ['/account'],
});

// Componentes compartidos (siempre activos)
registerApplication({
  name: '@myorg/navbar',
  app: () => System.import('@myorg/navbar'),
  activeWhen: () => true,
});

start();
// products-app/src/myorg-products.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './App';

const lifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: App,
  errorBoundary(err, info, props) {
    return <div>Error in products app</div>;
  },
});

export const { bootstrap, mount, unmount } = lifecycles;

3. Native Federation (Compatible con Vite/Rspack)

// vite.config.ts
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    federation({
      name: 'host-app',
      remotes: {
        products: 'http://localhost:3001/assets/remoteEntry.js',
        cart: 'http://localhost:3002/assets/remoteEntry.js',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
});

Gestion de Estado y Comunicacion

flowchart TB
    subgraph Events["1. Custom Events (Recomendado)"]
        EA["App A"] -->|CustomEvent| EB["App B"]
    end

    subgraph SharedState["2. Estado Compartido (Para casos complejos)"]
        SA["App A"] --> Store["Store<br/>(Global)"]
        SC["App C"] --> Store
        Store --> SB["App B"]
        Store --> SD["App D"]
    end

    subgraph URL["3. URL/Query Params"]
        URLNote["Compartir informacion de navegacion"]
    end

Custom Events

// Libreria compartida
// @myorg/events
export const CartEvents = {
  ITEM_ADDED: 'cart:item-added',
  ITEM_REMOVED: 'cart:item-removed',
  CART_UPDATED: 'cart:updated',
} as const;

export interface CartItemEvent {
  productId: string;
  quantity: number;
  price: number;
}

export function dispatchCartEvent(
  type: string,
  detail: CartItemEvent
): void {
  window.dispatchEvent(new CustomEvent(type, { detail }));
}

export function subscribeToCartEvent(
  type: string,
  handler: (event: CustomEvent<CartItemEvent>) => void
): () => void {
  window.addEventListener(type, handler as EventListener);
  return () => window.removeEventListener(type, handler as EventListener);
}
// products-app/src/ProductCard.tsx
import { CartEvents, dispatchCartEvent } from '@myorg/events';

function ProductCard({ product }) {
  const handleAddToCart = () => {
    dispatchCartEvent(CartEvents.ITEM_ADDED, {
      productId: product.id,
      quantity: 1,
      price: product.price,
    });
  };

  return (
    <div>
      <h3>{product.name}</h3>
      <button onClick={handleAddToCart}>Add to Cart</button>
    </div>
  );
}

// cart-app/src/Cart.tsx
import { useEffect, useState } from 'react';
import { CartEvents, subscribeToCartEvent } from '@myorg/events';

function Cart() {
  const [items, setItems] = useState([]);

  useEffect(() => {
    const unsubscribe = subscribeToCartEvent(CartEvents.ITEM_ADDED, (event) => {
      setItems((prev) => [...prev, event.detail]);
    });

    return unsubscribe;
  }, []);

  return (
    <div>
      <h2>Cart ({items.length} items)</h2>
      {/* ... */}
    </div>
  );
}

Compartir Estado Global

// @myorg/shared-store
import { create } from 'zustand';

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  login: (user: User) => void;
  logout: () => void;
}

// Store compartido globalmente
export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  isAuthenticated: false,
  login: (user) => set({ user, isAuthenticated: true }),
  logout: () => set({ user: null, isAuthenticated: false }),
}));

// Uso en cada micro frontend
// header-app/src/Header.tsx
import { useAuthStore } from '@myorg/shared-store';

function Header() {
  const { user, isAuthenticated, logout } = useAuthStore();

  return (
    <header>
      {isAuthenticated ? (
        <>
          <span>Welcome, {user?.name}</span>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <a href="/login">Login</a>
      )}
    </header>
  );
}

Estrategias de Enrutamiento

// shell-app/src/routing.ts
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { lazy, Suspense } from 'react';

// Carga dinamica de rutas de cada micro frontend
const ProductsRoutes = lazy(() => import('products/Routes'));
const CartRoutes = lazy(() => import('cart/Routes'));
const AccountRoutes = lazy(() => import('account/Routes'));

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      {
        path: 'products/*',
        element: (
          <Suspense fallback={<Loading />}>
            <ProductsRoutes />
          </Suspense>
        ),
      },
      {
        path: 'cart/*',
        element: (
          <Suspense fallback={<Loading />}>
            <CartRoutes />
          </Suspense>
        ),
      },
      {
        path: 'account/*',
        element: (
          <Suspense fallback={<Loading />}>
            <AccountRoutes />
          </Suspense>
        ),
      },
    ],
  },
]);

export function AppRouter() {
  return <RouterProvider router={router} />;
}

Estrategias de Despliegue

flowchart TB
    subgraph CDN["Configuracion CDN"]
        Shell["shell/<br/>└── v1.2.3/"]
        Products["products/<br/>└── v2.1.0/"]
        Cart["cart/<br/>└── v1.8.5/"]
        Account["account/<br/>└── v3.0.1/"]
    end

    subgraph Manifest["Gestion de Manifiesto"]
        JSON["products: cdn/products/v2.1.0/<br/>cart: cdn/cart/v1.8.5/<br/>account: cdn/account/v3.0.1/"]
    end

    CDN --> Manifest
    Note["Compatible con despliegue Blue-Green / Canary"]

Componentes UI Comunes

// @myorg/ui-kit (Sistema de diseno compartido)
export { Button } from './Button';
export { Input } from './Input';
export { Card } from './Card';
export { Modal } from './Modal';
export { theme } from './theme';

// Uso en cada micro frontend
// webpack.config.js
{
  shared: {
    '@myorg/ui-kit': {
      singleton: true,
      requiredVersion: '^1.0.0',
    },
  },
}

Estrategias de Testing

NivelObjetivoHerramientas
UnitarioComponentes individualesVitest, Jest
IntegracionMicro FE individualTesting Library
E2EIntegracion completaPlaywright, Cypress
ContratoContratos de APIPact

Enlaces de Referencia

← Volver a la lista