Fragmentos, React y Widgets

The English version of this article can be found here: Fragments, React and Widgets.

Los Fragmentos son simples, son trozos de HTML, CSS y JavaScript que se pueden añadir a una página y fácilmente desarrollados por programadores FrontEnd.

Esta fue, más o menos, la primera definición que dimos cuando empezamos el proyecto Modern Site Building. Pero también sabemos que hoy en día esa definición está llena de excepciones: los fragmentos también tienen ficheros de configuración en JSON, pueden renderizarse con FreeMarker o incluso ser generados con una clase Java... Se han vuelto mucho más personalizables y complejos, y tienen muchas más funcionalidades.

Esto no significa que hayamos cambiado la idea original de fragmento, todavía pueden usarse como piezas simples, pero puede que cueste más encontrar las diferencias entre éstos y los Widgets, ya que algunas funcionalidades se solapan. En este post pretendo hacer hincapié en las diferencias que aún existen entre ambos conceptos.

Allons-y!

Editable fields

Los campos editables (o editable fields) son seguramente la principal diferencia entre Fragmentos y Widgets. Como los Fragmentos están enfocados a mostrar contenido dentro de una página necesitamos alguna herramienta que nos permita definir lo que denominamos "campos editables", partes del marcado HTML que pueda ser manipulado por usuarios finales (también conocidos como maquetadores) para que puedan crear rápidamente Páginas de Contenido sin depender de recursos externos.

Inicialmente añadimos una nueva etiqueta HTML lfr-editable con un atributo type con el tipo de editable que se está usando. Elegimos esta sintaxis porque es marcado HTML válido, así los desarrolladores podían seguir usando sus propios editores para crear Fragmentos. Luego tanto en el FrontEnd (modo edición) como en el BackEnd (modo vista) procesábamos este marcado como FreeMarker para generar el HTML final reemplazando todos los valores de los editables.

Tras un tiempo, y al tener a más personas usando fragmentos, descubrimos que combinar la nueva etiqueta lfr-editable con CSS podía ser un dolor de cabeza: en modo vista estabamos reemplazando esta etiqueta con un div, teniendo diferentes marcados en modo vista y en modo edición y rompiendo algunos estilos CSS. Había algunos casos en los que la etiqueta lfr-editable era más que suficiente, así que decidimos añadir una solución extra que complementara a la existente. Añadiendo nuevos atributos data-lfr-editable-id y -type, los desarrolladores podían marcar cualquier elemento HTML como un editable sin tener que añadir más etiquetas.

Exponiendo FreeMarker
En el primer borrador, usar FreeMarker para procesar Fragmentos era una funcionalidad oculta que nosotros necesitábamos para encontrar y procesar los campos editables, pero después de un tiempo nos dimos cuenta de que usar FreeMarker en el HTML era realmente útil (por ejemplo usando bucles para crear algo más dinámico). Por este motivo decidimos exponer algunas variables extra (como fragmentNamespace) para que los usuarios pudieran utilizarlo como una herramienta más al crear fragmentos.

Configuración

Aunque tanto Fragmentos como Widgets tienen paneles de configuración, su proceso de renderizado es completamente diferente. Los paneles de configuración de los Widgets pueden contener literalmente cualquier cosa utilizando un fichero JSP, dejando total libertad (pero mucho trabajo) a los desarrolladores de Widgets. En el caso de los Fragmentos decidimos permitir una configuración más limitada con una estructura ya definida, lo cual nos permite mostrar una vista personalizada para cada campo de configuración.

La ventaja de este enfoque es que la configuración de un fragmento puede definirse sencillamente escribiendo un fichero JSON, haciendo nosotros el resto del trabajo de renderizado y guardado de información. La desventaja es que puede ser un poco más limitado al a hora de personalizarse. Estamos constantemente añadiendo nuevos tipos de configuración al esquema para evitar este problema, pero es posible que un desarrollador no sea capaz de encontrar algún tipo concreto de configuración.

Pasamos estos valores de configuración tanto al HTML (cuando se usa FreeMarker) como al JavaScript, exponiendo el objeto configuration con los valores especificados por el usuario.

<a class="btn btn-${configuration.buttonSize}" href="#">
  Go Somewhere
</a>

Ver Fragmento Botón en GitHub.

const video = fragmentElement.querySelector("video");
video.autoplay = configuration.autoPlay;

Ver Fragmento Vídeo en Github.

CSS

En el caso de los fragmentos, nuestro procesado del CSS es bastante sencillo: no hacemos nada™. Queríamos empezar con algo sencillo, y añadir una capa de SCSS (o cualquier otro lenguaje) sobre el CSS hubiera incrementado mucho la complejidad del proceso de compilación. Así que hoy por hoy solo damos algunas clases autogeneradas que permitan a los desarrolladores definir estilos encapsulados en el fragmento.

Este comportamiento puede cambiarse, por supuesto, si los Fragmentos se crean fuera de portal. Nada impide que un desarrollador se cree su propio flujo de preprocesado antes de importarlos, siempre y cuando acabe teniendo un único fichero CSS por Fragmento.

JavaScript

La mayoría de los fragmentos no deberían necesitar apenas JavaScript. Solo los más complejos necesitan algunas líneas de código para hacerlos funcionar (por ejemplo el Fragmento contribuido "Tabs" necesita un poco de JavaScript para cambiar de una pestaña a otra, aquí está el código en GitHub).

En el caso de necesitarlo, y para prevenir definir accidentalmente variables globales, envolvemos todo el JavaScript del Fragmento en una IIFE. Así que un fragmento con este código:

let count = 0;
function handleClick() { alert("Count: " + (count++)); }
fragmentElement.addEventListener("click", handleClick);

Se renderizará así:

(function () {
  const fragmentElement = /* Black magic */;
  const configuration = /* Extra black magic */;

  let count = 0;
  function handleClick() { alert("Count: " + (count++)); }
  fragmentElement.addEventListener("click", handleClick);
})();

Igualmente se pueden declarar variables globales manualmente accediendo al objeto globalThis o cualquier otra variable disponible en el ámbito global, pero los desarrolladores de Fragmentos deberían tener en cuenta que:

  • En una página puede haber simultáneamente varios fragmentos o varias instancias del mismo fragmento.
  • El JavaScript del Fragmento puede ejecutarse más de una vez, especialmente en modo edición.

Usando React en un Fragmento

Hace tiempo, creamos el Fragment Toolkit para permitir desarrollar Fragmentos fuera de portal. La idea inicial era tener alguna herramienta que hiciera comunicarse con la API (para importar/exportar Fragmentos) algo fácil. Mantiene una estructura de ficheros con todos los Fragmentos y Colecciones, y manda/recibe ficheros zip con ellos. Esto es algo que se puede hacer manualmente, pero tener una herramienta de consola permite automatizar el proceso.

Desde la versión 1.8.0, el Fragment Toolkit también es capaz de manejar "Fragmentos de React", permitiendo a los desarrolladores usar JSX en el JavaScript de los Fragmentos. Esto funciona porque internamente utilizamos el Liferay NPM Bundler para transpilar el código antes de mandarlo a portal, así pueden reutilizar la versión de React que ya existe (por esto solo está disponible a partir de Liferay 7.3+).


Todos los Fragmentos de React en portal dependen de la misma instancia de React.

En este caso, el proceso de renderizado de un Fragmento es ligeramente diferente: el HTML se muestra antes de que cargue el JavaScript, y el componente de React reemplaza el contenido una vez cargado. Como resultado estos Fragmentos no pueden tener campos editables, dropzones u otro marcado HTML dinámico. Esto entraría en conflicto con el código React que se genera. Sin embargo los desarrolladores de Fragmentos de React sí que pueden seguir usando ficheros de configuración, ya que ésta se manda al JavaScript del Fragmento.

export default function({ configuration, fragmentElement }) {
  return <div>Hello World</div>;
}

Las configuraciones de Liferay NPM Bundler y Webpack también pueden modificarse, añadiendo plugins y loaders para personalizar el proceso de compilación como se quiera. No soportamos oficialmente ningún tipo de configuración personalizada, pero debería funcionar sin problemas, tal y como muestra este experimento:

const config = require('generator-liferay-fragments').getBundlerConfig();

module.exports = {
  ...config,
  webpack: {
    ...config.webpack,
    module: {
      ...config.webpack.module,
      rules: [
        ...config.webpack.module.rules,
        {
          use: ['style-loader', 'css-loader', 'sass-loader'],
          test: /\.s[ac]ss$/i,
        }
      ]
    }
  }
};

Es posible que en este punto surja la pregunta: Si empiezo a añadir código personalizado a mis fragmentos... ¿Qué hará el bundler con él? ¿Que pasa si intento usar alguna otra librería externa? Bueno, todo este proceso de compilación ocurre fuera de portal, así que el código que se guarda en base de datos ya está compilado, por eso los Fragmentos de React no pueden editarse ni exportarse una vez importados.

El bundler de Liferay no es capaz de desduplicar o reutilizar estas dependencias, así que junta todo en un único fichero JavaScript para cada fragmento, incluso si estas dependencias tienen la misma versión. Además, si utilizas varias instancias del mismo Fragmento en una página, el código se mostrará duplicado.


Cuidado al crear Fragmentos con dependencias externas, el fichero JavaScript final puede ser enorme.

Una posible solución temporal podría ser añadir estas dependencias compartidas a portal y reutilizarlas en todos los Fragmentos como variables globales (o usar un sistema de gestión de dependencias personalizado). Tenemos algunas investigaciones en curso que podrían resolver este tipo de situaciones, pero hoy por hoy no hay una solución oficial.

Poder compilar fragmentos antes de utilizarlos en portal puede paracer una solución genial, especialmente si ya tienes componentes de React reutilizables, pero nunca se debe olvidar que el código generado puede ser mucho mayor al Fragmento original. Si se termina con gran parte de la página renderizada como un único Fragmento, es posible que sea el momento de separarlo en más Fragmentos pequeños.

Como siempre, seguimos intentando mejorar el desarrollo de Fragmentos para que estas piezas de interfaz sigan siendo simples, mantenibles y reutilizables.

¡Muchas Gracias!

Enlaces relacionados (en inglés)

Blogs

Muy buen post. Para personalizar más todavía un custom fragment, me gustaría saber cómo agregarle un icon personalizado o usar alguno de los que trae LR.