NexusTech Logo
Web ComponentsJavaScriptFrontendComponents / 1/5/2024

Web Components: El Futuro de la Reutilización de Código

Web Components: Componentes Reutilizables Sin Dependencias

Durante años, la reutilización de componentes ha requerido frameworks como React, Vue o Angular. Pero Web Components ofrece una alternativa nativa del navegador que cambia el juego completamente.

¿Qué son los Web Components?

Los Web Components son un conjunto de APIs estándar de HTML5 que permite crear componentes personalizados, reutilizables y encapsulados:

  1. Custom Elements: Define tus propias etiquetas HTML
  2. Shadow DOM: Encapsulación de estilos y estructura
  3. HTML Templates: Plantillas reutilizables eficientes
  4. HTML Imports (Deprecated): Importación de componentes

Ventajas Fundamentales

1. Cero Dependencias

<!-- Sin Web Components -->
<script src="react.min.js"></script>
<script src="react-dom.min.js"></script>
<script src="button-component.js"></script>
<!-- 3+ archivos, cientos de KB -->

<!-- Con Web Components -->
<script src="button-component.js"></script>
<!-- 1 archivo, típicamente <5KB -->

2. Reutilizable Universalmente

<!-- El mismo componente funciona en cualquier lugar -->
<my-button theme="primary">Click aquí</my-button>

<!-- En HTML puro -->
<!DOCTYPE html>
<my-button>Botón</my-button>

<!-- En React -->
<MyButton>Botón</MyButton> {/* ¡También funciona! */}

<!-- En Vue -->
<my-button>Botón</my-button>

<!-- En Angular -->
<my-button>Botón</my-button>

3. Encapsulación Real

<!-- Los estilos dentro del componente NO afectan al exterior -->
<style>
  button { color: red; }
</style>

<my-button>
  <!-- Internamente, los botones permanecen sin estilo -->
</my-button>

Crear un Web Component Básico

Paso 1: Estructura Base

// button-component.js
class MyButton extends HTMLElement {
  constructor() {
    super();
    
    // Crear Shadow DOM
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        button {
          padding: 10px 20px;
          border: none;
          border-radius: 4px;
          background: #007bff;
          color: white;
          cursor: pointer;
          font-size: 14px;
        }
        
        button:hover {
          background: #0056b3;
        }
      </style>
      
      <button>
        <slot></slot>
      </button>
    `;
    
    // Listeners
    this.shadowRoot.querySelector('button')
      .addEventListener('click', () => this.handleClick());
  }

  handleClick() {
    // Emitir evento personalizado
    this.dispatchEvent(new CustomEvent('button-clicked', {
      detail: { message: this.textContent },
      bubbles: true,
    }));
  }
}

// Registrar el elemento personalizado
customElements.define('my-button', MyButton);

Paso 2: Uso en HTML

<!DOCTYPE html>
<html>
<head>
  <script src="button-component.js"></script>
</head>
<body>
  <my-button>Haz clic</my-button>
  
  <script>
    document.querySelector('my-button')
      .addEventListener('button-clicked', (e) => {
        console.log('¡Botón clickeado!', e.detail.message);
      });
  </script>
</body>
</html>

Componentes Avanzados con Propiedades

Card Reutilizable

class CardComponent extends HTMLElement {
  // Atributos observados
  static get observedAttributes() {
    return ['title', 'subtitle', 'image'];
  }

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

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render();
    }
  }

  render() {
    const title = this.getAttribute('title') || 'Sin título';
    const subtitle = this.getAttribute('subtitle') || '';
    const image = this.getAttribute('image') || '';

    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }
        
        .card {
          border-radius: 8px;
          overflow: hidden;
          box-shadow: var(--card-shadow);
          background: white;
          transition: transform 0.3s;
        }
        
        .card:hover {
          transform: translateY(-4px);
        }
        
        .card-image {
          width: 100%;
          height: 200px;
          object-fit: cover;
        }
        
        .card-content {
          padding: 16px;
        }
        
        .card-title {
          font-size: 18px;
          font-weight: bold;
          margin: 0 0 8px 0;
        }
        
        .card-subtitle {
          color: #666;
          font-size: 14px;
          margin: 0;
        }
      </style>
      
      <div class="card">
        ${image ? `<img src="${image}" alt="${title}" class="card-image">` : ''}
        <div class="card-content">
          <h3 class="card-title">${title}</h3>
          ${subtitle ? `<p class="card-subtitle">${subtitle}</p>` : ''}
          <slot></slot>
        </div>
      </div>
    `;
  }
}

customElements.define('card-component', CardComponent);

Uso

<card-component 
  title="Producto Destacado"
  subtitle="Calidad premium"
  image="https://ejemplo.com/imagen.jpg">
  <p>Este es un producto increíble.</p>
</card-component>

<!-- Con CSS personalizado -->
<style>
  card-component {
    --card-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  }
</style>

Comunicación entre Componentes

Patrón de Eventos

// emit-component.js
class EmitterComponent extends HTMLElement {
  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <button>Enviar dato</button>
    `;
    
    this.shadowRoot.querySelector('button')
      .addEventListener('click', () => {
        this.emitData();
      });
  }

  emitData() {
    this.dispatchEvent(new CustomEvent('data-changed', {
      detail: { value: 'Hola desde el componente' },
      bubbles: true,
      composed: true, // Importante: atraviesa Shadow DOM
    }));
  }
}

customElements.define('emitter-component', EmitterComponent);
// Uso: Comunicación bidireccional
document.querySelector('emitter-component')
  .addEventListener('data-changed', (e) => {
    console.log('Dato recibido:', e.detail.value);
    // Hacer algo con el dato
  });

Ventajas vs Desventajas

✅ Ventajas

VentajaImpacto
Cero dependenciasMenor bundle size, menos vulnerabilidades
Reutilizable universalmenteCódigo compartible entre proyectos
Encapsulación realEstilos no se filtran, arquitectura limpia
Estándar webSoportado nativamente en navegadores modernos
RendimientoDirect DOM manipulation, sin virtual DOM overhead
Facilidad de aprendizajeSolo JavaScript vanilla, HTML y CSS

⚠️ Desventajas

DesventajaSolución
Navegadores antiguosPolyfills disponibles
Debugging complejoDevTools mejoradas constantemente
Falta de ecosistemaCreciendo: Lit, Stencil, FAST
SSR limitadoMejorado en navegadores recientes
Curva de aprendizajeDocumentación en mejora

Framework Helpers: Lit

Para Web Components complejos, Lit simplifica el desarrollo:

import { LitElement, html, css } from 'lit';

class SmartButton extends LitElement {
  static styles = css`
    button {
      padding: 10px 20px;
      background: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
  `;

  static properties = {
    disabled: { type: Boolean },
    label: { type: String },
  };

  constructor() {
    super();
    this.disabled = false;
    this.label = 'Click me';
  }

  render() {
    return html`
      <button ?disabled=${this.disabled}>
        ${this.label}
      </button>
    `;
  }
}

customElements.define('smart-button', SmartButton);

Caso de Uso Real: Biblioteca de Componentes Empresarial

Estructura del Proyecto

my-component-library/
├── src/
│   ├── button/
│   │   ├── button.js
│   │   └── button.css
│   ├── card/
│   │   ├── card.js
│   │   └── card.css
│   └── modal/
│       ├── modal.js
│       └── modal.css
├── package.json
└── README.md

package.json

{
  "name": "@mycompany/components",
  "version": "1.0.0",
  "main": "dist/index.js",
  "files": ["dist"],
  "scripts": {
    "build": "webpack",
    "test": "vitest"
  }
}

Uso en Múltiples Proyectos

# En cualquier proyecto
npm install @mycompany/components

# O desde CDN
<script src="https://cdn.example.com/components.js"></script>
<!-- Funciona en React, Vue, Angular, o HTML puro -->
<my-button>Haz clic</my-button>
<my-card title="Producto"></my-card>
<my-modal id="login"></my-modal>

Soporte de Navegadores

NavegadorSoporte
Chrome✓ 67+
Firefox✓ 63+
Safari✓ 10.1+
Edge✓ 79+
IE 11✗ (requiere polyfills)
// Detectar soporte y cargar polyfills si es necesario
if (!window.customElements) {
  const script = document.createElement('script');
  script.src = 'https://cdn.jsdelivr.net/npm/@webcomponents/webcomponentsjs@2/webcomponents-loader.js';
  document.head.appendChild(script);
}

Conclusión

Los Web Components representan el futuro de la reutilización de código en la web. No reemplazan a los frameworks, sino que ofrecen una alternativa más ligera y universal para casos específicos:

  • Pequeños componentes reutilizables
  • Bibliotecas de UI compartidas entre organizaciones
  • Integración en múltiples contextos
  • Aplicaciones con requisitos mínimos de dependencias

La era de los componentes sin framework ha llegado.


Próximos pasos:

Recursos:

Sigue leyendo

Profundiza en temas de arquitectura y rendimiento.