Jamstack con Angular: El siguiente paso en el desarrollo web – Parte 2

Publicado por Fernando Pacheco Ibáñez el

FrontJamstackScullyAngularDesarrollo Web

En esta segunda parte de Jamstack con Angular, haremos una demostración de cómo transformar una SPA tradicional en una aplicación Jamstack con Angular + Scully.

Para ello, disponemos de una aplicación Angular tradicional a la cual le añadiremos Scully para pre-renderizar sus páginas y analizar los resultados obtenidos.

Recuerda, que existe una primera parte dónde introducimos al mundo de Jamstack y Scully.

Partiremos del proyecto base, puedes clonarlo desde el siguiente enlace.

En la rama principal master se encuentra el código del punto del que partiremos para realizar la transformación a Jamstack. En la rama feature/scully-backup se encuentra el código de la aplicación final, con todas las transformaciones aplicadas.

Introducción a la aplicación base

La aplicación que usaremos de ejemplo es una aplicación SPA tradicional de Angular, que consta de únicamente 2 rutas:

  • Una ruta raíz “/”, que mostrará una lista de personajes de Rick y Morty.
  • Una ruta parametrizada “/:id”, que mostrará el detalle del personaje identificado con el parámetro “id”.

Para visualizar la aplicación en modo SPA en nuestro navegador, levantaremos el servidor de desarrollo de Angular usando el comando ng serve y accederemos a la url http://localhost:4200 desde nuestro navegador. En esta url podremos visualizar la lista de personajes.

Y si accedemos a la ruta http://localhost:4200/2 (por ejemplo), veremos el detalle del personaje con id 2.

Además, podemos ver en la pestaña de Network de las herramientas de desarrollo de Chrome que los documentos HTML obtenidos no tienen el contenido pre-renderizado. Sólo aparece la etiqueta wsd-root, que Angular utilizará para renderizar el resto del contenido en el lado del cliente en tiempo de ejecución.

Como apunte, si deshabilitamos la ejecución de código JavaScript en el navegador, veremos que la página no se muestra debido a que no estamos dejando que Angular ejecute su proceso de renderizado en el lado del cliente.

Añadiendo Scully

Para añadir Scully a la aplicación lanzaremos el comando ng add @scullyio/init. Este comando provocará de manera automática diferentes cambios en nuestro proyecto de Angular. Los más importantes son:

  • Nuevo archivo scully.[Project-name].conf.ts: Este archivo será nuestro archivo de configuración principal de Scully. En este archivo de configuración, indicaremos el nombre del proyecto y las rutas dinámicas a pre-renderizar por scully. Además, nos permitirá incluir plugins de Scully o propios que usaremos durante el proceso de pre-renderizado y post-renderizado.
  • Nueva carpeta scully/, donde podremos alojar el código fuente de nuestros propios plugins.
  • Nuevos scripts en el package.json:
  • npm run scully: Lanza el proceso de pre-renderizado de Scully usando la aplicación de Angular ya construida previamente (con ng build –prod) y disponible en la carpeta dist/.
  • npm run scully:serve: Lanza 2 servidores de desarrollo: uno de la aplicación SPA disponible en la carpeta **dist/ y otro con la aplicación Jamstack pre-renderizada previamente con el comando npm run scully y disponible en la carpeta dist/static/.
  • Además, también se modifica el código de app.module.ts para añadir un nuevo import: ScullyLibModule, que registrará en nuestra aplicación Angular las diferentes utilidades y servicios que Scully nos proporciona como, por ejemplo, el servicio TransferStateService.

Una primera pre-renderización

Para empezar, realizaremos un primer proceso de pre-renderizado lanzando el comando ng build –prod && npm run scully. Una vez finalizado el proceso, podemos destacar los siguientes mensajes de la consola.

El primer mensaje “No configuration for route “/:id” found. Skipping” nos indica que Scully no es capaz de resolver por sí solo los diferentes valores del parámetro id, y por lo tanto no realizará el proceso de pre-renderizado de esas rutas a menos que le indiquemos dichos valores.
El segundo mensaje “Route “” rendered into file: “.\dist\static\index.html” nos indica que la ruta Raiz (“/”) ha sido pre-renderizada correctamente y el contenido está disponible en dist/static/index.html.

Si visualizamos el contenido del archivo index.html podemos ver el contenido pre-renderizado de esta página.

Para probar la aplicación Jamstack, lanzaremos el comando npm run scully:serve, que nos habilitará un servidor en http://localhost:1668/. Al acceder a esta dirección podremos ver que el contenido se visualiza de manera inmediata, debido a que el contenido ya viene pre-renderizado desde el servidor. Además, podemos ver desde las herramientas de desarrollo de Chrome, que el contenido del documento HTML descargado está pre-renderizado.

Si accedemos directamente a la url http://localhost:1668/1, nos arrojará un error debido a que el servidor de estáticos de Scully no encuentra la ruta pre-renderizada “/1”.

Sin embargo, si primero accedemos a http://localhost:1668/ y seguidamente pulsamos en uno de los resultados de la lista de personajes de Rick y Morty, veremos que si se visualiza el contenido correctamente.

Esto se debe a que una vez la página pre-renderizada se muestre en el navegador, entra en juego el modo SPA de Angular, ya que posee todos los archivos JavaScript y CSS necesarios para que se transforme en una SPA tradicional, incluyendo la navegación a rutas no pre-renderizadas por Scully, tal y como se explicó en el anterior artículo.
Para solucionar el problema de acceso directo a las rutas dinámicas no pre-renderizadas, podemos configurar el servidor para que redirija al usuario a la página 404 que Scully nos ha generado, para que entre a funcionar la versión SPA desde un primer momento.

Además, si deshabilitamos la ejecución de código JavaScript en nuestro navegador, en esta ocasión la página se visualizará con total normalidad, puesto que el contenido ya viene pre-renderizado por el servidor y no es necesario que entre en juego Angular para crear el DOM.

Pre-renderizando rutas dinámicas con Scully

Para corregir el problema de las rutas “/:id” no pre-renderizadas por Scully, debido a que no puede resolver los valores del parámetro id, implementaremos la resolución de este parámetro en el archivo de configuración scully.[Project-name].conf.ts.
Primero, analizaremos la respuesta obtenida por la API de Rick y Morty: https://rickandmortyapi.com/api/character

De la respuesta de la API nos interesa la propiedad results, que almacena una colección de personajes de Rick y Morty. Dentro de cada elemento de la colección identificamos la propiedad id, que es la que usamos en la aplicación SPA para obtener el detalle del personaje seleccionado.
Por lo tanto, podremos usar la utilidad que Scully nos ofrece para el descubrimiento de rutas dinámicas a pre-renderizar de la siguiente forma:

Con esta implementación le estamos indicando a Scully que el parámetro id se obtendrá de la respuesta de la API http://https://rickandmortyapi.com/api/character. Además le indicamos que la colección de datos a usar está en la propiedad results, y que el valor del parámetro id lo obtenga de la propiedad id de cada elemento de la colección.

Una segunda pre-renderización

Ahora que ya hemos implementado la resolución del parámetro dinámico id, podemos volver a lanzar el comando npm run scully. En esta ocasión la consola nos indica que en esta ocasión Scully ha pre-renderizado las rutas con parámetros dinámicos.

Si accedemos a la carpeta dist/static/ encontramos todas esas páginas pre-renderizadas:

Si volvemos a levantar el servidor de estáticos de Scully con npm run scully:serve y accedemos directamente a una ruta dinámica, el contenido se mostrará con total normalidad, como pasaba con la ruta raíz “/”.

Mejora de la aplicación: Evitar duplicidad de llamadas a la API

Uno de los problemas que actualmente tenemos en la aplicación actual es que se realizan peticiones duplicadas a la API:

  • La primera llamada a la API se realiza durante el proceso de pre-renderizado, con el cual Scully crea la versión pre-renderizada de cada página.
  • La segunda llamada se realiza cuando la aplicación pre-renderizada se carga en el navegador, y entra en juego el modo SPA de Angular. Esto provocará que todos los ciclos de vida de la aplicación Angular se ejecuten nuevamente, incluyendo las peticiones a la API. Para evitar este comportamiento, podemos hacer uso del servicio de Scully TransferStateService. Este servicio insertará en cada página, donde lo usemos, un contenido JSON con los datos obtenidos de la API que Scully utilizó para pre-renderizar esa página. Una vez la aplicación se transforme en SPA, usará los datos cacheados previamente por el proceso de pre-renderizado, evitando así una segunda llamada a la API.
    Este servicio lo usaremos en la página de listado y de detalle de la siguiente forma:
    Página de listado de personajes:

Página de detalle de un personaje:

Para probar el resultado debemos volver a construir la aplicación Angular con ng build –prod y seguidamente lanzar el proceso de pre-renderizado de scully con npm run scully. Dentro de cada página pre-renderizada, que usó el TransferStateService, aparecerá un script con el contenido de la petición a la API en formato JSON.

Si ejecutamos la aplicación pre-renderizada, con el comando npm run scully:serve, podremos ver que no se realizarán peticiones a la API desde el navegador pero se sigue mostrando la información. Esto se debe a que la aplicación está usando la caché previamente generada durante el proceso de pre-renderizado de scully.

Mejora de la aplicación: Rehidratación del contenido

En numerosas ocasiones necesitaremos mostrar el contenido lo más inmediato posible, pero sin dejar de mostrar el contenido más reciente posible. A este proceso se le conoce como Rehidratación.
La rehidratación consiste en mostrar una información de la que ya disponemos, mientras que en segundo plano obtenemos la información más reciente para sustituir la que usamos a modo de caché.
Este proceso es muy útil para secciones muy específicas de nuestra aplicación, como la sección de comentarios, precios de un artículo...
Para implementar la rehidratación en nuestra aplicación, modificaremos la forma de obtener los datos en las páginas donde queramos usar esta estrategia. Para esta demostración, supondremos que la información de los personajes que usamos en nuestra aplicación cambia muy a menudo.
Página de lista de personajes:

Página de detalle de un personaje:

Con esto logramos tener un tiempo de interacción lo más bajo posible, a la vez que mostramos el contenido más reciente posible

Mejora de la aplicación: Uso del plugin minifyHtml

Uno de los plugins más usados es el plugin de minifyHtml. Durante el proceso de pre-renderizado, este plugin se encargaría de minificar cada página pre-renderizada. Esto aumentará aún más el rendimiento de nuestra aplicación.
Siguiendo la documentación del plugin disponible en https://scully.io/docs/Reference/plugins/community-plugins/minifyHtml/, instalaremos la dependencia con el comando npm i -D scully-plugin-minify-html y haremos uso del mismo en el archivo scully.[Project-name].conf.ts.

Ahora cuando lancemos el comando npm run scully generará una versión minificada de cada ruta pre-renderizada.

Rendimiento de la aplicación en Lighthouse

Por último, mostraremos el rendimiento de la aplicación pre-renderizada usando la extensión Lighthouse de Chrome, la cual nos permite analizar el Performance y otras diferentes métricas disponibles en el análisis de una aplicación web.
Para ello, generamos un nuevo Report de Performance y obtenemos el siguiente resultado:

Como podemos observar, la nota obtenida en la métrica de Performance es de un 99, fácilmente mejorable a 100 mejorando ciertos aspectos de nuestra aplicación, como reduciendo el tamaño de las imágenes utilizadas, una buena política de cache...

Espero que esta serie de post te haya servido para ver las capacidades de Jamstack con Angular.

Si te ha gustado, ¡síguenos en Twitter para estar al día de próximos posts!