Carga perezosa

La carga perezosa, también llamada carga diferida, o lazy load, es uno de los puntos fuertes del sistema de enrutamiento en Angular 5. De hecho, es una técnica muy empleada en muchos entornos de desarrollo, pero que adquiere un especial sentido en el trabajo con SPA’s, sobre todo si pensamos que nuestra aplicación pueda ser empleada por usuarios de dispositivos móviles, u otras conexiones más lentas de lo habitual.

Básicamente, se trata de lo siguiente. Cuando cargamos una SPA, se cargan en el navegador del cliente todos los componentes que la forman, y que se leen a partir del archivo de enrutamiento. Si el usuario se conecta con un ordenador medianamente potente, desde una conexión de fibra, esto puede no ser relevante. Sin embargo, pensemos en aquellos usuarios que se conectan desde una conexión más lenta (cobre, o móvil, por ejemplo). Cargar toda la aplicación en el navegador del cliente puede suponer una demora importante. Y, si resulta que nuestra aplicación tiene, digamos, quince vistas, y el usuario se conecta para consultar una sola, le estamos demorando para que se cargue en su navegador una importante cantidad de contenidos que no va a usar.

La carga perezosa consiste en que sólo se cargue la vista de inicio de la aplicación, o las vistas que sepamos, por experiencia, que son más consultadas. El resto de las vistas no se cargarán en el navegador del cliente a menos que este las solicite expresamente (pulsando un enlace, por ejemplo). De este modo, las vistas que no son invocadas por el cliente no se cargan, y no consumen recursos de la conexión.

LO QUE SE CARGA Y LO QUE NO SE CARGA

Empecemos por el principio. Sabemos que cuando se pone en marcha el servidor de Angular, el webpack lleva a cabo varias tareas. Te hablé sobre el webpack en este artículo, por si quieres repasarlo. Su misión es transpilar el código TypeScript a JavaScript, organizarlo en paquetes (archivos .js) y mandarlo todo al navegador. Parte de esos archivos .js son funcionalidades propias de Angular, por lo que estas siempre se cargarán al arranque de la aplicación, sí o sí. Sin embargo, hay un JavaScript llamado main.bundle.js, que es la compilación de los módulos y componentes que se cargan directamente, es decir, que no usan carga perezosa. Los módulos y componentes que usan carga perezosa no se incluyen en este script, sino que son transpilados aparte, y sólo se envían al navegador si son solicitados.

EL ESCENARIO

Seguimos trabajando a partír de la aplicación que ya tenemos «a medio hacer». En este artículo le vamos a añadir un enlace más a la barra de navegación para acceder al módulo de almacén (llamado StoringModule). Sin embargo, al contrario que los anteriores, este lo cargaremos en diferido, es decir, sólo si el usuario pulsa el enlace. En caso contrario, no se cargará en el navegador, y no consumirá recursos. Realmente, este será un menú desplegable que dará acceso a los tres componentes del módulo StoringModule.

Lo que vamos a hacer esta vez, sin embargo, es empezar al revés. Veremos como funciona en el navegador, y luego veremos como lo hemos hecho para que funcione, así que, por ahora, vamos a olvidarnos de la programación, y vamos a ver el resultado. Y para eso, vamos a contar con la inestimable ayuda de la consola de Chrome, que nos proporciona muchísima información técnica muy útil.

En esta ocasión, además, voy a retocar el package.json para que el comando ng serve no incluya las opciones --aot ni -o, a fin de controlar manualmente la carga, y ver lo que sucede. Una vez cambiado esto, arranco el webpack (ya sabes, npm start) y, cuando terminan todas las operaciones internas (la terminal me muestra el mensaje webpack: Compiled successfully.) abro el navegador, dejo que se me cargue la página que tenga de inicio por defecto en la configuración de Chrome (en mi caso es el buscador de Google) y abro la consola. De las opciones que aparecen en la parte superior, pulso sobre Network, y pulso dos veces (no doble clic, sino dos clics independientes) en el botón redondo rojo de la parte superior izquierda, para limpiar el historial de comunicaciones de red. La consola del navegador, en este momento, tiene un aspecto similar al que ves a continuación.

Ahora me voy a la barra de direcciones y tecleo localhost:4200. Tras un breve lapso de tiempo, se me carga la página de inicio de mi aplicación Angular. La consola, en la pestaña Network, tiene ahora el siguiente aspecto:

La pestaña Network refleja las peticiones que se hacen al servidor y lo que se recibe. Observa los datos marcados con flechas rojas. El primero nos dice que se ha lanzado una petición que ha descargado main.bundle.js, que es el JavaScript de nuestra aplicación (los módulos y componentes que hemos creado). Este script contiene TODOS los componentes que no se cargan en diferido, es decir, los que están importados en app-routing.module.ts, en las siguientes líneas:

En la imagen de la consola ves la flecha roja inferior que nos dice que se han hecho 11 peticiones (una detras de otra), para cargar la aplicación. Las otras diez peticiones corresponden a operaciones del propio navegador, y a la carga de los fichero básicos de cualquier aplicación Angular (inline.bundle.js, polyfills.bundle.js, etc.).

Ahora pulso los enlaces de Clientes, Proveedores e Inicio, las veces que quiero (sin tocar aún nada de Almacén), y ya no se lanzan peticiones al servidor. Como estos componentes son de carga inmediata, ya están cargados, y no necesitamos mandar más peticiones el servidor, por lo que la operativa es más rápida. Cuando luego te cuente como se hace la programación, podrás probarlo tú en tu propio ordenador.

Sin embargo, ahora despliego el menú de Almacén y pulso cualquiera de las tres opciones que tengo (Listado, Entradas o Salidas). Todo el módulo de almacén funciona con Lazy Load, por lo que, esta vez, sí se lanza otra petición al servidor. Tras pulsar cualquiera de las opciones de Almacén, la pestaña Network de la consola tiene el siguiente aspecto:

Observa las dos flechas rojas. Vemos que ahora ya tenemos acumuladas 12 peticiones, en lugar de 11. La petición nueva es a un script llamado storing.module.chunk.js. La partícula chunk se la añade Angular 5 a los scripts correspondientes a los módulos que se cargan en diferido. Lo que importa es, precisamente, esto. Si yo no hubiera pedido nada de las opciones de Almacén, este módulo no se habría cargado, evitando transferir los datos del servidor al cliente. Si yo tuviera una conexión lenta, y no necesitara ver nada de Almacén, me podría ahorrar tiempo de carga. Ahora ya está en mi navegador. Ya no importa lo que pulse. No se van a hacer más peticiones de módulos al servidor.

ATENCIÓN. Como ves, la carga diferida es un proceso pensado para no cargar aquellos módulos que el usuario no solicita. Sin embargo, tienes que tener en cuenta una cosa. Si alguno de los módulos que tenemos debe cargar datos dinámicos de una BD remota en tiempo real (por ejemplo, mediante una API), cada vez que lo llamemos, aunque ya esté en memoria, si se añadirá una petición más para los datos. Sin embargo, eso es algo que aún no vamos a ver. En su momento, ya hablaremos de conectividad con API’s del servidor para datos en tiempo real. Cada cosa a su tiempo.

MÓDULOS CON ENRUTAMIENTO INTERNO

Bien. Ya te he contado como funciona, desde el punto de vista del usuario, la carga diferida frente a la carga inmediata. Y, como ya sabemos como enlazar con los componentes y módulos que se cargan en inmediato, ahora vamos a ver como programamos la carga diferida.

Lo primero que tienes que saber es que, para cargar componentes en lazy load, los módulos donde estos se encuentren han tenido que crearse con su propio sistema de routing. Si un módulo se ha creado sin enrutamiento propio (es decir, sin usar el flag --routing en la creación, tal cómo comentamos en este artículo), ya tenemos un problema. Esto es así, porque, desde nuestros enlaces (pongamos el caso de la barra de navegación, o cualquier otro sistema de enlaces) no podemos acceder a los componentes del módulo. En lazy load se accede a la carga del módulo (la generación del archivo tipo .chunk.js), pero no a un componente específico. Por lo tanto es imprescindible que el módulo tenga su propio enrutador.

Y aquí se presenta un problema. ¿Que hago si un módulo ha sido creado sin enrutador y ahora lo necesito para la carga diferida?

MÓDULO CON ENRUTADOR Y SIN ENRUTADOR

Cuando creas un módulo este se coloca en una carpeta específica dentro de la estructura de directorios de Angular(1). Si el módulo se crea sin enrutamiento, en esta carpeta aparece un archivo, con el nombre del módulo, seguido de la partícula .module y la extensión .ts. Si el módulo se crea con enrutamiento propio, se crean dos archivos. Uno se llama igual que en el caso anterior y el otro se llama con el nombre del módulo, seguido de -routing.module.ts. Cómo ya hemos creado varios módulos, te supongo más o menos familiarizado con esto.

Creamos un módulo sin enrutamiento, así:

ng g m module01

Esto nos crea un módulo llamado Module01Module, con un archivo llamado module01.module.ts. Si ahora necesitamos añadirle un sistema de enrutamiento propio, deberemos crear un archivo llamado module01-routing.module.ts, cuyo listado es el siguiente:

El archivo base es siempre el mismo, así que solo tendrías que copiar este archivo en el directorio del módulo que creaste sin enrutamiento y al que se lo quieres añadir. Lo único que cambia es el nombre de la clase que se exporta (línea 10), que debe empezar con el nombre del módulo donde se crea este sistema de enrutamiento.

También quiero llamar tu atención sobre una cosa. Si lo miras, verás que este archivo es muy parecido al app-routing.module.ts, salvo por un detalle en la línea 7. En el decorador del archivo del enrutamiento raíz (el que hemos usado en el artículo anterior), se importaba RouterModule con el método forRoot(), lo que quería decir que ese enrutamiento era para la raíz del sitio o, si lo prefieres, global al sitio. En los enrutamientos de los módulos que crees tú se usa forChild(). Es el equivalente, pero para enrutamientos de módulos que están, jerárquicamente, «por debajo» de AppModule. En una aplicación Angular puede haber todos los enrutamientos que necesites que usen el método forChild(), pero sólo uno (el de AppModule) que use el método forRoot().

Además, hay que decirle al archivo *.module.ts que vamos a usar enrutamiento. En nuestro ejemplo, el archivo module01.module.ts debe ser modificado para poder usar el enrutamiento interno. El original (cuando se creo sin enrutamiento interno) es así:

Para añadirle el enrutamiento interno se queda así:

(1) Puedes crear un módulo, o un componente, sin que se organice en una carpeta propia usando, para la generación, el modificador --flat. Sin embargo, esto te puede llevar a tener un batiburrillo de archivos desorganizados, que no sepas donde está cada cosa. Yo lo desaconsejo encarecidamente.

LA BARRA DE NAVEGACIÓN

Para ver como funciona la carga diferida, vamos a echarle un vistazo rápido a la barra de navegación. Le hemos añadido una opción que hemos llamado Almacén que, por sí, no carga nada. Solo despliega un menú con tres opciones. Lo que queremos es que, cuando se pulse una de esas tres opciones por primera vez, se produzca la carga diferida del módulo de almacén (StoringModule) con el componente que corresponda a la opción elegida. El archivo navbar.component.html queda así:

Observa especialmente las líneas resaltadas. Ves que las tres opciones del submenú del almacén van a enlaces que empiezan con la partícula almacen. El primero sólo tiene esa partícula, y los otros dos tienen rutas que «cuelgan» de esa partícula. Y ¿a donde nos llevan esos enlaces?

EL ARCHIVO DE ENRUTAMIENTO RAÍZ

En el archivo de enrutamiento raíz (app-routing.module.ts) hemos creado un nuevo objeto route, un poco especial, como ves a continuación:

Fíjate que, a pesar de que en la barra de navegación tenemos tres opciones de almacén, en el archivo de enrutamientos sólo hemos creado una ruta. Observa que tiene, en la propiedad path, el valor ‘almacen‘, que es la parte común con la que empiezan las tres opciones del menú en la barra de navegación. También vemos que no tiene propiedad component, por lo que, a la carga de la aplicación, no se carga nada para acceder al almacén, que es lo que pretendíamos. En su lugar, le hemos añadido la propiedad loadChildren. Esto hace que, cuando se pulse un enlace que apunte a almacen, o a «algo» que empiece por almacen/, será cuando se cargue el módulo especificado. Como valor de loadChildren ponemos la ruta relativa hasta el módulo que queremos cargar, el signo # y el nombre del módulo que queremos cargar.

Observa también que StoringModule no es importado junto con los demás módulos en las líneas de importaciones de la parte superior del script.

Es decir. Cuando se llame a un routerLink que sea almacen, o que empiece por almacen/, será cuando se cargue el módulo StoringModule, no antes. Sin embargo, fíjate que esto es sólo una ruta, pero tenemos tres enlaces. Esto no discierne a que componente del módulo queremos llamar. Y aquí es donde entra en juego el enrutador interno de StoringModule.

EL ENRUTADOR INTERNO

Efectivamente, una vez que se carga el módulo en memoria, hace falta discernir cual de los tres enlaces que tenemos que apuntan a componentes de este módulo ha sido pulsado. Eso nos lo da el enrutador interno del módulo. Por eso te decía antes que para cargar un módulo en diferido, este tiene que tener un enrutador interno. El storing-routing.module.ts nos queda así:

Empieza por ver las líneas 4, 5 y 6, donde se importan los tres componentes que se emplean en este módulo. A continuación tenemos las routes, que se crean siguiendo el mismo esquema que en el enrutador raíz, mediante objetos que definen un path y el component al que apunta. Hay dos cosas que, seguramente, te llaman la atención: una es que el primer path recibe una cadena vacía, lo que parece que puede entrar en colisión con un path similar que hay en el enrutador principal; la otra es que no hay un path: '**' para redireccionar en el caso de páginas no existentes. Hablemos de estos dos puntos.

LA RUTA «VACÍA»

Tienes que entender una cosa. Como estamos en un enrutador interno de un módulo, que jerárquicamente está «por debajo» del módulo raíz, la ruta path: '' no se refiere a que esté vacía a partir de localhost:4200, sino a partir de localhost:4200/almacen. Es decir. Para llegar aquí ya hemos tenido que acceder a almacen, por lo que una ruta vacía, en este enrutador es, digámoslo así, «relativamente» vacía. Observa las otras dos rutas. La ruta path: 'entradas' coincide con el routerLink de la barra de navegación almacen/entradas y path: 'salidas' coincide con el routerLink de la barra de navegación almacen/salidas.

Cuando usamos un enrutamiento interno, las rutas siempre son relativas a la ruta principal que cargó el módulo (en este ejemplo, el prefijo almacen).

LA RUTA PARA 404

En los enrutamientos internos de módulos no se pone una ruta para páginas no encontradas porque, al estar esto previsto en el enrutamiento raíz, que es global a todo el sitio, esta salida nos funciona estemos donde estemos. No necesitamos más salidas de NotFound.

CONCLUYENDO

Ya sabemos como hacer la carga diferida de módulos, y para qué se usa. Hemos desgranado el proceso paso a paso. No obstante, si lo necesitas, tienes la aplicación, en el estado en el que estamos ahora, en este enlace. Está sin el directorio node_modules, que es el vendor de Angular. Ese es común a toda la aplicación y, si te descargas la del enlace y la copias en un directorio donde hayas creado una aplicación nueva, tendrás que ejecutar, en la terminal, npm update, para que te instale jQuery y Bootstrap, para que te quede como la mía.

   

Deja un comentario