Volver al blog

Síguenos y suscríbete

Un componente modular de lenguaje de marcado Edge Side Includes para Compute en JavaScript

Katsuyuki Omuro

Senior Software Engineer, Developer Relations, Fastly

En el verano de 2022, mi compañero Kailan trabajó en una caja de Rust para Compute de Fastly, implementó un subconjunto del lenguaje para plantillas Edge Side Includes (ESI) y escribió una entrada del blog al respecto. Este artículo fue muy interesante, y no solo porque acabábamos de lanzar una biblioteca muy útil, sino también porque ilustraba a la perfección lo que Compute pone a nuestro alcance: un edge programable con funcionalidad modular. Ahora que la disponibilidad general de JavaScript en Compute ha cumplido más de un año, ha llegado el momento de ofrecer una solución similar a nuestros usuarios de JavaScript. La nueva biblioteca de ESI de Fastly para JavaScript, ya disponible en npm, te permite añadir potentes funcionalidades de procesamiento de lenguaje de marcado Edge Side Includes a tu aplicación.

Programabilidad y modularidad

Durante prácticamente una década, la CDN de Fastly ha sido compatible con Edge Side Includes (ESI), un lenguaje para plantillas que interpreta las etiquetas especiales incluidas en una fuente en formato HTML. Todo gira en torno a <esi:include>, una etiqueta que ordena al servidor en el edge recuperar otro documento e insertarlo en la secuencia.

index.html

<body>
<esi:include src="./header.html" />
<main>
Contenido
</main>
</body>

header.html

<header>Te damos la bienvenida a nuestro sitio web</header>

Salida

<body>
<header>Te damos la bienvenida a nuestro sitio web</header>
<main>
Contenido
</main>
</body>

Con la llegada de Compute, el edge empezó a definirse en dos nuevos términos: programabilidad y modularidad.

Poco después de que la compatibilidad de nuestra plataforma con Rust se hubiera estabilizado, publicamos una caja de Rust que implementaba ESI y añadimos programabilidad. A partir de entonces, empezó a ser posible configurar mediante código la incorporación de peticiones de backend adicionales y gestionar los fragmentos de las respuestas. Incluso se podía procesar ESI en documentos que no procedían del servidor de backend. Esta programabilidad suponía una diferencia considerable con respecto a la compatibilidad con ESI de VCL, que se limitaba a las funcionalidades concretas que ofrecíamos.

Al mismo tiempo, su elevada modularidad daba a los programadores la opción de realizar el procesamiento de ESI en cada petición o combinar el procesamiento con otros módulos que funcionaran con tipos de datos compatibles y aplicarlos, ya fuera en cualquier orden o siguiendo una condición lógica determinada.

Próxima parada: JavaScript:

Queríamos que esta biblioteca de JavaScript fuera programable, al igual que la de Rust. La compatibilidad de Fastly con JavaScript siempre se ha basado en la API Fetch y su compañera inseparable, la API Streams. Un punto a favor de la API Streams es la interfaz TransformStream, que permite pasar los datos a través de un objeto con la finalidad de aplicar una transformación, lo cual resulta ideal para ESI. Al utilizar el procesador de ESI como una implementación de TransformStream, pudimos integrarlo directamente en una aplicación de Compute de Fastly escrita en JavaScript.

Esta es la secuencia:

const transformedBody = resp.body.pipeThrough(esiTransformStream);

return new Response(
  transformedBody,
  {
    status: resp.status,
    headers: resp.headers,
  },
);

La trasformación, a la que llamamos EsiTransformStream, se aplica directamente a la secuencia, por lo que su impacto en la memoria y el rendimiento es menor. ¿Qué significa esto?

  • No es necesario almacenar en búfer la respuesta ascendente en su totalidad antes de aplicar la transformación.

  • El transformador consume la respuesta ascendente tan rápido como puede y procesa las etiquetas de ESI a medida que aparecen en la secuencia. Cada vez que el transformador termina de procesar una etiqueta de ESI, los resultados se envían inmediatamente en dirección descendente, por lo que el almacenamiento en búfer es mínimo. De esta forma, el cliente puede recibir el primer byte del resultado en cuanto está listo, sin esperar a que se procese en su totalidad.

Además, como este diseño es modular, EsiTransformStream se convierte en una herramienta más que se puede utilizar junto con otras. Por ejemplo, si quieres aplicar otras transformaciones a las respuestas, como la compresión, puedes pasarlas por las secuencias de transformación que sean necesarias, puesto que se trata de una interfaz totalmente estándar.  Incluso podrías habilitar ESI para determinadas peticiones o respuestas de forma condicional en función del encabezado, la ruta o el tipo de contenido de una respuesta.

Así es como se inicia EsiTransformStream:

const esiTransformStream = new EsiTransformStream(req.url, req.headers, {
  fetch(input, init) {
    return fetch(input, { ...init, backend: 'origin_0' });
  }
});

El constructor toma una URL, un objeto de encabezado y, de manera opcional, otro elemento como tercer parámetro. Como ya hemos dicho, la principal funcionalidad de ESI consiste en descargar plantillas adicionales para incluirlas en la secuencia resultante. Cuando aparece la etiqueta <esi:include>, se emplea fetch como mecanismo subyacente para recuperar una plantilla, y el principal cometido de estos parámetros es configurar esas llamadas:

  • La URL se utiliza para resolver rutas relativas en el src de las etiquetas <esi:include>.

  • Los encabezados se utilizan al hacer peticiones adicionales para recuperar las plantillas.

  • El objeto de configuración adicional se puede usar para anular el comportamiento de la llamada realizada y aplicar otro que permita procesar la plantilla recuperada o gestionar los errores.

En el más sencillo de los casos, solo hay que emplear el valor de fetch del objeto de configuración. Si no lo proporcionas, entonces se utilizará la función fetch global. Sin embargo, en Compute deberás especificar un backend para la llamada en caso de que se incluya una plantilla, a menos que estés utilizando la funcionalidad de backends dinámicos. En el fragmento de arriba, se asigna el backend origin_0 antes de la llamada global a fetch.

Y ya está. Con esta configuración tan sencilla, puedes tener un backend para las etiquetas de ESI y la aplicación de Compute que las procese. Aquí tienes un ejemplo más completo:

Compatibilidad con funcionalidades de ESI

Esta implementación incluye más funcionalidades de ESI que otras versiones anteriores.

Gestión de errores

En ocasiones, es posible que no pueda recuperarse un archivo asociado a una etiqueta <esi:include> porque no existe o provoca un error en el servidor. Este error se puede ignorar añadiendo el atributo onerror="continue".

<esi:include src="/templates/header.html" onerror="continue" />

Si /templates/header.html causa un error, el procesador de ESI lo ignorará sin decir nada y sustituirá toda la etiqueta <esi:include> por una cadena vacía.

También existe la posibilidad de gestionar los errores de una manera más estructurada mediante un bloque <esi:try> como este:

<esi:try>
  <esi:attempt>
    Main header
    <esi:include src="/templates/header.html" />
  </esi:attempt>
  <esi:except>
    Alternative header
  </esi:except>
</esi:try>

En primer lugar, el procesador de ESI ejecuta los contenidos de <esi:attempt>. Si una etiqueta <esi:include> causa un error, entonces ejecuta los contenidos de <esi:except>.

Conviene destacar que el bloque <esi:try> se sustituye por el bloque <esi:attempt> en caso de éxito y el bloque <esi:except> en caso de fallo. En el ejemplo de arriba, si /templates/header.html provoca un error, entonces la salida mostrará el texto «Alternative header», pero no «Main header». Tienes más información en las especificaciones del lenguaje ESI.

Condicionales

ESI también admite la ejecución condicional mediante comprobaciones en tiempo de ejecución de las variables. Aquí tienes un ejemplo:

<esi:choose> 
    <esi:when test="$(HTTP_COOKIE{group})=='admin'"> 
        <esi:include src="/header/admin.html" /> 
    </esi:when> 
    <esi:when test="$(HTTP_COOKIE{group})=='basic'">
        <esi:include src="/header/basic.html" />
    </esi:when> 
    <esi:otherwise> 
        <esi:include src="/header/anonymous.html" /> 
    </esi:otherwise>
</esi:choose>

Cuando el procesador se topa con un bloque <esi:choose>, pasa por todos los bloques esi:when y comprueba las expresiones que figuran en los atributos de prueba. El procesador ejecuta el primer bloque <esi:when> cuando el resultado de la expresión es verdadero. Si ninguna de las expresiones da verdadero, entonces ejecuta el bloque <esi:otherwise>, si es que lo hay. Todo el bloque esi:choose se sustituye por <esi:when> o <esi:otherwise> en su totalidad dependiendo del que se ejecute.

El procesador facilita un conjunto limitado de variables que se basan principalmente en cookies de peticiones. En el ejemplo de arriba, se comprueba el valor de una cookie de HTTP con el nombre «group». Nuestra implementación se ciñe a las especificaciones del lenguaje ESI. Consúltalas para obtener más información.

Lista de funcionalidades y etiquetas admitidas

Esta implementación admite las siguientes etiquetas de las especificaciones del lenguaje ESI:

  • esi:include
  • esi:comment
  • esi:remove
  • esi:try / esi:attempt / esi:except
  • esi:choose / esi:when / esi:otherwise
  • esi:vars

Según las especificaciones, la etiqueta <esi:inline> es opcional, por lo que no se incluye en la implementación.

Las variables de ESI se admiten en los atributos de las etiquetas de ESI, y las expresiones de ESI se admiten en el atributo de prueba de <esi:when>. También se admite el comentario <!--esi ...-->.

Comportamiento personalizado, infinitas posibilidades

Aunque este conjunto de funcionalidades no está nada mal, lo mejor de la programabilidad es que abre las puertas a un sinfín de posibilidades. ESI se utiliza principalmente para plantillas, pero da mucho más de sí. Veamos un ejemplo:

Imagina que tienes una marca de tiempo en tu documento y quieres que se muestre como una fecha relativa, como «hace 2 días». Hay varias formas de conseguirlo, pero la mejor desde el punto de vista de la memoria y el rendimiento sería buscar y remplazar en las secuencias. Una buena opción para conseguirlo sería programar la biblioteca de ESI.

Podrías definir marcas de tiempo para que se codificaran en el documento del backend utilizando una etiqueta de ESI a medida en un formato como el siguiente:

<esi:include src="/__since/<unix-timestamp>" />

Este fragmento podría representar las 00:00 del 1 de enero de 2024 (hora del Pacífico):

<h1>This document was written <esi:include src="/__since/1704034800" />.</h1>

A continuación, configuramos EsiTransformStream para que añada un documento sintético de sustitución cada vez que detecte ese patrón en la URL:

const esiTransformStream = new EsiTransformStream(req.url, req.headers, {
    fetch(input, init) {
      const url = new URL(input instanceof Request ? input.url : String(input));

      // If custom URL pattern, return synthetic Response
      const sinceMatch = /^\/__since\/(\d+)$/.exec(url.pathname);
      if (sinceMatch != null) {
        const timestampString = sinceMatch[1];
        const timestamp = parseInt(timestampString, 10);
        return new Response(
          toRelativeTimeSince(timestamp), // This function builds a relative date
          {
            headers: { 'Content-Type': 'text/plain', },
            status: 200,
          }
        );
      }

      // For all other esi:include tags, add the backend and call the global fetch
      return fetch(input, { ...init, backend: 'origin_0' });
    }
  });

De esta forma, cuando el procesador detecte el fragmento de ejemplo de arriba, producirá un resultado similar al siguiente, dependiendo del número de días que hayan transcurrido al ejecutarlo:

<h1>This document was written 22d ago.</h1>

Como Fastly puede almacenar en caché el documento del backend, las futuras peticiones producirán un acierto en caché y se seguirá mostrando la fecha relativa actualizada.

Aquí puedes verlo en tiempo real en la herramienta Fiddle:

¡Haz la prueba!

@fastly/esi ya está disponible en npm, así que puedes añadirlo a cualquier programa de JavaScript. Puedes usarlo en el edge en tus programas de Compute de Fastly e incluso fuera de esta plataforma, siempre que tu entorno sea compatible con la API Fetch. Tienes el código fuente al completo en GitHub.

Si quieres ponerte en marcha, empieza clonando uno de los fragmentos de Fiddle que aparecen arriba y pruébalo en tus orígenes. No necesitas una cuenta de Fastly. Y cuando tu servicio esté listo para su publicación en nuestra red global, puedes solicitar una prueba gratuita de Compute y empezar a trastear con la biblioteca de ESI en npm.

Con Compute, el edge es programable y modular. Eso significa que puedes combinar las soluciones que mejor se adapten a ti e incluso crearlas por tu cuenta. No somos los únicos que proporcionan módulos de este tipo para Compute: cualquiera puede hacer sus aportaciones al ecosistema y beneficiarse de él. Como siempre, no dudes en pasarte por el foro comunitario de Fastly y contarnos en qué has estado trabajando.