Publicado el 13.01.2021 a las 05:42
Hoy en día, en todo proyecto de Angular que se inicie de cierta envergadura, se debería de utilizar el patrón lazy load para mejorar la experencia de usuario aumentado la velocidad de carga de la aplicación
El lazy loading (o carga asíncrona o diferida o perezosa) es un patrón de diseño que se utiliza para aumentar la velocidad de carga de una aplicación y consiste en retrasar la inicialización de algunos componentes u objetos hasta el momento de su utilización. Este proceso mejora el rendimiento de las aplicaciones, puesto que al iniciar la aplicación sólo se inicializará los componentes del módulo principal (app.module.ts), y el resto de componentes se ubican en módulos diferentes que irán cargando a medida que los vayamos necesitando.
Por ejemplo, imagina que tenemos una aplicación para vender bicicletas, cuando cargamos la aplicación, sólo se cargará el componente para mostrar las bicicletas en ventas, pero los componentes referidos a la autenticación (login, register, forgot_password...) se cargarán en un módulo por ejemplo llamado auth y que no se cargarán hasta que no llamemos a alguno de los componentes que hay dentro (login, reister, forgot_password...). De la misma forma podríamos tener un módulo para las ventas donde tendríamos los componentes de la cesta, del listado de pedidos de un usuario...
Para usar lazy load en Angular, hacemos llamado de un módulo mediante el sistema de rutas de Angular y este módulo a su vez tiene rutas hijas que se encargan de cargar el componente solicitado por el usuario.
ng new LazyLoad
Contestamos que sí a las preguntas de si queremos ayuda para depurar bugs y en si deseamos usar el router de Angular.
Elegimos la opción de usar estilos CSS. No será objeto de este ejemplo hacer una aplicación con muchos estilos.
Creamos los diferentes módulo que va a tener el proyecto, en nuestro caso van a ser 3, home, about y contact
ng g m modules/home --routing
ng g m modules/about --routing
ng g m modules/contact --routing
la g significa genera, la m significa módulo y el flag del final --routing lo que le indica a Angular es que genere un fichero para la rutas del módulo que crea.
El resultado del punto anterior será que en la carpeta src/app se ha creado una carpeta llamada modules, y dentro de esa carpeta hay 3 carpetas más, home, about y contact. Y dentro de cada una de esas carpetas encontraremos 2 ficheros de typescript, uno será el módulo (home.module.ts) y el otro será el fichero de rutas (home-routing.module.ts)
Vamos a crear 2 componentes (componente01 y componente02) por cada módulo
ng g c modules/home/componente01 --skipTests
ng g c modules/home/componente02 --skipTests
ng g c modules/about/componente01 --skipTests
ng g c modules/about/componente02 --skipTests
ng g c modules/contact/componente01 --skipTests
ng g c modules/contact/componente02 --skipTests
El flag --skipTests es para decirle a Angular CLI que no me genere el fichero de pruebas o tests unitarios.
Con las instrucciones anteriores, tendremos en cada uno de los módulos 2 nuevas carpetas, una carpeta para el componente01 y otra para el componente02.
La página 404 hace referencia al código de respuesta 404, puedes ver más acerca de los códigos de estado en este artículo
Este componente lo creo por si el usuario introuce una ruta que no tengo recogido en la barra de dirección del navegador
ng g c shared/Page404 --skip-tests
Angular CLI, automáticamente lo importará al app.module.ts
En el fichero de routing de cada uno de los módulos, añadiremos las rutas hijas para carga del componente01 y del componente02 de cada uno de los módulos.
Los fichero de routing tienen que quedar:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { Componente02Component } from './componente02/componente02.component'; import { Componente01Component } from './componente01/componente01.component'; const routes: Routes = [ { path: "", children: [ { path: "uno", component: Componente01Component }, { path: "dos", component: Componente02Component }, { path: "**", redirectTo: "uno"} ], }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class HomeRoutingModule { }
Es hora de programar el fichero principal de rutas app-routing.module.ts
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { Page404Component } from './shared/page404/page404.component'; const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', loadChildren: () => import('../app/modules/home/home.module').then(m => m.HomeModule) }, { path: 'contact', loadChildren: () => import('../app/modules/contact/contact.module').then(m => m.ContactModule) }, { path: 'about', loadChildren: () => import('../app/modules/about/about.module').then(m => m.AboutModule) }, { path: '**', component: Page404Component }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
Atención: es importante que en los imports del app.module.ts no estén los diferentes módulos que queremos que se carguen en lazy load, ya que si sí están, se cargarán todos los módulos al iniciar la aplicación. A mí me paso en una ocasión y me llevó como 2 horas encontrar el problema.
Borrar todo el contenido del app.component.html y cambiar por:
<div> <h1> Lazy Loading </h1> <ul> <li> <a routerLink="/home">Home</a> <ul> <li><a routerLink="/home/uno">Componente 1</a></li> <li><a routerLink="/home/dos">Componente 2</a></li> </ul> </li> <li> <a routerLink="/about">About</a> <ul> <li><a routerLink="/about/uno">Componente 1</a></li> <li><a routerLink="/about/dos">Componente 2</a></li> </ul> </li> <li> <a routerLink="/contact">Contact</a> <ul> <li><a routerLink="/contact/uno">Componente 1</a></li> <li><a routerLink="/contact/dos">Componente 2</a></li> </ul> </li> </ul> </div> <router-outlet></router-outlet>
Con lo anterior creamos una página HTML sin estilos para poder navegar entre los diferente módulos y entre sus diferentes componentes
Para comprobar la carga asíncrona
En el caso de que tengamos un módulo protegido por un guard, hay programar el método canLoad() en el guard.
Imagina que en el ejmplo anterior, quieres proteger el acceso al módulo about con un guard llamando AuthGuard porque quieres que el usuario esté autenticado para poder acceder al módulo.
El archivo de rutas quedaría:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { Page404Component } from './shared/page404/page404.component'; const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', loadChildren: () => import('../app/modules/home/home.module').then(m => m.HomeModule) }, { path: 'contact', loadChildren: () => import('../app/modules/contact/contact.module').then(m => m.ContactModule) }, { path: 'about', canActivate: [ AuthGuard ], canLoad:[ AuthGuard ], loadChildren: () => import('../app/modules/about/about.module').then(m => m.AboutModule) }, { path: '**', component: Page404Component }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
Y el guard(auth.guard.ts) sería algo como por ejemplo:
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators'; import { UsuarioService } from './../services/usuario.service'; import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, CanLoad, Route, UrlSegment, UrlTree, } from '@angular/router'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate, CanLoad { constructor(private usuarioService: UsuarioService, private router: Router) {} canLoad( route: Route, segments: UrlSegment[] ): | boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> { return this.usuarioService.validarToken().pipe( tap((estaAutenticado) => { if (!estaAutenticado) { this.router.navigateByUrl('/home'); } }) ); } canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { return this.usuarioService.validarToken().pipe( tap((estaAutenticado) => { if (!estaAutenticado) { this.router.navigateByUrl('/home'); } }) ); } }
Si lo deseas puedes clonar el repositorio del ejemplo de mi GitHub
Cualquier duda o comentario puedes enviarla por aquí
Hasta luego 🖖