← Volver al blog
·9 min de lectura

Guia completa de Client Extensions en Liferay DXP 7.4

LiferayJava

El problema con el desarrollo tradicional en Liferay

Durante anos, extender Liferay DXP significaba escribir modulos OSGi en Java, empaquetarlos como JARs, y desplegarlos dentro del contenedor de la plataforma. Este enfoque funcionaba, pero traia fricciones reales:

  • Acoplamiento fuerte: Tu codigo vivia dentro del classloader de Liferay. Una actualizacion de la plataforma podia romper tus modulos si dependian de APIs internas o no estables.
  • Ciclo de desarrollo lento: Cada cambio requeria recompilar, redesplegar y esperar a que el framework OSGi resolviera dependencias. En proyectos grandes, esto podia tomar minutos.
  • Barrera de entrada alta: Un desarrollador frontend que solo necesitaba personalizar un widget tenia que entender OSGi, ServiceBuilder, y el lifecycle de Liferay para hacer algo funcional.
  • Riesgo en produccion: Un modulo defectuoso podia desestabilizar toda la instancia porque compartia el mismo proceso Java.

Liferay reconocio estos problemas y, a partir de DXP 7.4 Update 36 (Q4 2022), introdujo las Client Extensions como el modelo de desarrollo recomendado para personalizaciones.

Que son las Client Extensions

Una Client Extension es una unidad de personalizacion que se ejecuta fuera del proceso de Liferay. En lugar de desplegar codigo dentro del servidor, defines un contrato (que tipo de extension es, donde vive, como se comunica) y Liferay se encarga de integrarlo.

El concepto clave es la separacion de runtime: tu extension puede ser una aplicacion React corriendo en su propio servidor, un microservicio que responde a webhooks, o un archivo de configuracion que Liferay consume. Lo que importa es que no comparte el classloader ni el ciclo de vida del portal.

Esto tiene implicaciones profundas:

  1. Independencia tecnologica: Puedes escribir extensiones en cualquier lenguaje o framework. React, Angular, Vue, Go, Python -- lo que tenga sentido para tu caso de uso.
  2. Despliegue independiente: Actualizar una extension no requiere reiniciar Liferay. Despliegas tu aplicacion por separado y Liferay la consume.
  3. Escalabilidad horizontal: Si una extension necesita mas recursos, la escalas independientemente sin tocar el portal.
  4. Seguridad por aislamiento: Un error en tu extension no puede tumbar la plataforma.

Tipos de Client Extensions

Liferay organiza las Client Extensions en varias categorias segun su proposito:

Frontend Client Extensions

Son las mas comunes. Permiten inyectar interfaces de usuario dentro de paginas de Liferay.

  • Custom Element: Registra un web component (o cualquier aplicacion frontend) como un widget que los editores pueden arrastrar a paginas. Es el tipo mas versatil.
  • IFrame: Embebe una aplicacion externa dentro de un iframe en la pagina. Mas simple que Custom Element pero con las limitaciones inherentes de iframes (estilo aislado, comunicacion limitada).
  • Theme CSS: Inyecta hojas de estilo personalizadas que sobrescriben el tema activo. Util para ajustes de marca sin crear un tema completo.
  • Theme Favicon: Reemplaza el favicon del sitio.
  • Theme JS: Inyecta JavaScript global en todas las paginas del sitio.

Configuration Client Extensions

Modifican el comportamiento de Liferay sin codigo:

  • OAuth User Agent: Configura autenticacion OAuth2 para que la extension pueda llamar a las APIs headless de Liferay de forma segura.
  • Instance Settings: Define configuraciones a nivel de instancia.

Batch Client Extensions

Ejecutan operaciones masivas sobre datos:

  • Batch: Importa o exporta datos en lote usando las APIs headless. Ideal para migraciones de contenido, creacion de estructuras, o configuracion inicial de un sitio.

Action Client Extensions

Responden a eventos del sistema:

  • Object Action: Se ejecuta cuando ocurre un evento en un Object Definition (crear, actualizar, eliminar). Funciona como un webhook que Liferay invoca hacia tu servicio externo.
  • Workflow Action: Se integra con el motor de workflows para ejecutar logica personalizada en transiciones de estado.
  • Notification: Envia notificaciones a traves de canales externos cuando ocurren eventos especificos.

Crear una Custom Element Client Extension con React

Vamos a construir una extension de tipo Custom Element que muestra un dashboard de estadisticas. Este es el tipo mas representativo y el que demuestra mejor el modelo de desarrollo.

Estructura del proyecto

mi-dashboard-extension/
  client-extension.yaml
  package.json
  src/
    index.js
    App.jsx
    components/
      StatCard.jsx
      Chart.jsx
  build/
    static/
      js/
        main.js
      css/
        main.css

Configuracion en client-extension.yaml

Este archivo es el corazon de cualquier Client Extension. Define el tipo, las propiedades y los recursos que Liferay necesita conocer:

assemble:
  - from: build/static
    into: static
mi-dashboard:
  cssURLs:
    - css/main.css
  friendlyURLMapping: mi-dashboard
  htmlElementName: mi-dashboard-widget
  instanceable: true
  name: Mi Dashboard de Estadisticas
  portletCategoryName: category.client-extensions
  type: customElement
  urls:
    - js/main.js
  useESM: true

Detallemos cada campo:

  • assemble: Indica como empaquetar los archivos. Aqui le decimos que copie el contenido de build/static en una carpeta static dentro del paquete final.
  • htmlElementName: El nombre del custom element que Liferay buscara en el DOM. Tu aplicacion React debe registrarse con este nombre.
  • instanceable: Si es true, se pueden colocar multiples instancias del widget en la misma pagina.
  • portletCategoryName: La categoria donde aparecera el widget en el panel de widgets del editor de paginas.
  • urls: Los archivos JavaScript que Liferay debe cargar.
  • cssURLs: Las hojas de estilo asociadas.
  • useESM: Habilita ES modules en lugar de scripts clasicos.

El punto de entrada de la aplicacion

El archivo index.js debe registrar un custom element que Liferay pueda instanciar:

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

class MiDashboardWidget extends HTMLElement {
  connectedCallback() {
    const root = createRoot(this);
    root.render(
      <React.StrictMode>
        <App
          instanceId={this.getAttribute('id')}
          companyId={Liferay.ThemeDisplay.getCompanyId()}
          siteGroupId={Liferay.ThemeDisplay.getScopeGroupId()}
        />
      </React.StrictMode>
    );

    this._root = root;
  }

  disconnectedCallback() {
    if (this._root) {
      this._root.unmount();
    }
  }
}

if (!customElements.get('mi-dashboard-widget')) {
  customElements.define('mi-dashboard-widget', MiDashboardWidget);
}

Algunos detalles importantes:

  • El nombre en customElements.define debe coincidir con htmlElementName en el YAML.
  • Liferay.ThemeDisplay es un objeto global que Liferay expone en el cliente con informacion del contexto actual (compania, sitio, usuario, idioma, etc.).
  • Se implementa disconnectedCallback para limpiar el arbol de React y evitar memory leaks cuando el widget se remueve del DOM.

Componente principal con llamadas a la API headless

import { useState, useEffect } from 'react';
import StatCard from './components/StatCard';

const API_BASE = '/o/headless-delivery/v1.0';

function App({ siteGroupId }) {
  const [stats, setStats] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchData() {
      try {
        const [postsRes, docsRes] = await Promise.all([
          Liferay.Util.fetch(
            `${API_BASE}/sites/${siteGroupId}/blog-postings?pageSize=0`,
            { method: 'GET' }
          ),
          Liferay.Util.fetch(
            `${API_BASE}/sites/${siteGroupId}/documents?pageSize=0`,
            { method: 'GET' }
          ),
        ]);

        const posts = await postsRes.json();
        const docs = await docsRes.json();

        setStats({
          totalPosts: posts.totalCount || 0,
          totalDocuments: docs.totalCount || 0,
        });
      } catch (error) {
        console.error('Error fetching stats:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, [siteGroupId]);

  if (loading) return <div className="dashboard-loading">Cargando...</div>;

  return (
    <div className="dashboard-grid">
      <StatCard
        label="Entradas de blog"
        value={stats.totalPosts}
        icon="blog"
      />
      <StatCard
        label="Documentos"
        value={stats.totalDocuments}
        icon="document"
      />
    </div>
  );
}

export default App;

Nota que usamos Liferay.Util.fetch en lugar de fetch nativo. Este wrapper incluye automaticamente las cabeceras de autenticacion (CSRF token, session) necesarias para llamar a las APIs headless de Liferay.

Flujo de despliegue

El despliegue de una Client Extension sigue estos pasos:

  1. Build del frontend: Ejecutas npm run build para generar los archivos estaticos optimizados en build/static/.

  2. Empaquetado: Liferay Workspace incluye una tarea Gradle que lee el client-extension.yaml, toma los archivos indicados en assemble, y genera un archivo .zip listo para desplegar.

# Desde el workspace de Liferay
./gradlew :client-extensions:mi-dashboard:build
  1. Despliegue: El .zip resultante se sube a Liferay a traves de la interfaz de administracion (Panel de Control > Client Extensions) o se copia al directorio osgi/client-extensions/ del servidor.

  2. Activacion: Liferay procesa el paquete, registra la extension, y la hace disponible en el editor de paginas. No requiere reinicio.

Si usas Liferay Cloud o Liferay SaaS, el despliegue se integra directamente con el pipeline CI/CD de la plataforma.

Ventajas sobre el desarrollo tradicional

Despues de trabajar con ambos modelos, las diferencias se sienten en el dia a dia:

| Aspecto | Modulos OSGi | Client Extensions | |---|---|---| | Lenguaje | Java obligatorio | Cualquiera | | Ciclo de desarrollo | Compilar + desplegar (minutos) | Hot reload local (segundos) | | Riesgo en produccion | Comparte proceso con Liferay | Aislado | | Actualizaciones de Liferay | Posibles roturas de API | Contrato estable | | Curva de aprendizaje | OSGi + ServiceBuilder + Liferay APIs | Web standards + REST APIs | | Escalabilidad | Vertical (mas recursos al portal) | Horizontal (escalar extension aparte) |

El cambio mas significativo es cultural: los equipos frontend pueden trabajar con sus herramientas habituales (Vite, webpack, testing con Jest) sin depender del toolchain de Java. Los equipos backend pueden exponer servicios como microservicios independientes que Liferay consume via Object Actions o Workflow Actions.

Limitaciones y consideraciones

Las Client Extensions no son una solucion universal. Hay casos donde los modulos OSGi siguen siendo necesarios:

  • Modificaciones profundas del core: Si necesitas alterar el comportamiento interno de Liferay (hooks a nivel de kernel, filtros de servlet, interceptores de servicio), los modulos OSGi siguen siendo la via.
  • ServiceBuilder: Para modelos de datos complejos con relaciones, vistas personalizadas y finders optimizados, ServiceBuilder sigue siendo mas potente que los Object Definitions.
  • Performance critica: Si tu logica necesita acceso directo a la base de datos o al cache de Liferay sin overhead de red, un modulo en el mismo proceso sera mas rapido.

La recomendacion de Liferay es clara: usa Client Extensions como primera opcion, y recurre a OSGi solo cuando el caso de uso lo exija.

Conclusion

Las Client Extensions representan el futuro del desarrollo en Liferay DXP. No se trata solo de un cambio tecnico, sino de un cambio de paradigma que abre la plataforma a un ecosistema mas amplio de desarrolladores y tecnologias. Si estas empezando un proyecto nuevo en Liferay 7.4+, las Client Extensions deberian ser tu punto de partida por defecto.