Rutas

Rutas

Las rutas sirven para dividir y abstraer mejor por rutas de dominio la aplicación.

Al generar la app puedes elegir que se agreguen los archivos para el routing de una vez, y queda de la siguente manera:

// app-routing.module.ts
const routes: Routes = [
  {
    path: '',
    redirectTo: 'home', // sin slash, de lo contrario va a la ruta principal
    pathMatch: 'full'
  },
  {
    path: 'home',
    component: HomeComponent
  },
  {
    path: 'products',
    component: ProductsComponent
  },
  {
    path: 'product/:productId',
    component: ProductDetailComponent
  },
  {
    path: '**',
    component: NotFoundComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
// app.module.ts
import { AppRoutingModule } from './app-routing.module';
@NgModule({
  // ..
  imports: [
    AppRoutingModule
  ],
})
export class AppModule { }

Observa la primera regla que tiene el path vacío y un redirectTo para redireccionar al componente home cuando no se ingresa ninguna ruta. Utiliza también la opción pathMatch para asegurar que la ruta sea exacta. Podría quedar solamente path: '', y sin otra regla con redirect hacía home y funcionaria igual.

También para las páginas no encontradas se usa path: '**' .Es muy importante que esta regla para manejo de rutas no definidas se encuentre ubicado en el último lugar del array. Angular analiza las rutas en el mismo orden en que las defines. Si esta regla se encuentra en primer lugar, puede anular las demás y darte algunos problemas.

Finalmente, tienes que importar la directiva <router-outlet> en el componente raíz de tu aplicación.

<!-- app.component.html -->
<router-outlet></router-outlet>

Para poder movernos entre rutas sin recargar nuestra página debemos agregar a nuestras anclas ‘<a>’ la directiva routerLink envés del atributo href para que Angular determine que no haga una recarga de la página.

<div><a routerLink="/home">Home</a></div>
<!-- Antes <a href="/home">Home</a> -->

Ancla activa routerLinkActive

Puedes definir una clase para cuando una ruta coincida con el routerLink al agregar la directiva routerLinkActive.

<a routerLink="/home" routerLinkActive="active">Home</a>

Podemos hacer uso de un arreglo y encerrar routerLink como si fuera un property binding

<a [routerLink]="['/products/', product.id]">Ver detalle</a>

O también utilizar el string interpolation

<a routerLink="/products/{{product.id}}">Ir al producto</a>

Capturando parámetros de URL

Los parámetros se crean así en el archivo routing:

// app-routing.module.ts
const routes: Routes = [
...
  {
    path: 'product/:productId',
    component: ProductDetailComponent
  },
...

Observa que ambas rutas apuntan al mismo componente, eso está bien. La diferencia estará en que la segunda ruta posee :productId y podrás capturar el parámetro utilizando ese mismo nombre.

Inyección de servicios necesarios

En el componente correspondiente, inyecta el servicio ActivatedRoute y también importa Params para tipar tus datos y manipularlos más fácilmente. Ambos imports provenientes de @angular/router.

import { ActivatedRoute, Params } from '@angular/router';

@Component({
  selector: 'app-catalogo',
  templateUrl: './catalogo.component.html',
  styleUrls: ['./catalogo.component.scss']
})
export class CatalogoComponent {
  constructor(private route: ActivatedRoute) { }
}

Captura de parámetros síncronos

El mejor lugar para capturar parámetros de URL, sean síncronos o no, es utilizando los hooks de ciclo de vida de Angular, más concretamente ngOnInit().

ngOnInit(): void {
  const categoryId = this.route.snapshot.paramMap.get('productId');
  console.log(categoryId);
}

this.route.snapshot.params: Esta propiedad es una instantánea (snapshot) de los parámetros de la ruta en un momento determinado. Esto significa que si se accede a esta propiedad en un momento en particular, se obtendrán los valores de los parámetros de la ruta en ese momento. Si los parámetros cambian posteriormente, esta propiedad no se actualizará.

snapshot.paramMap también proporciona algunas funciones útiles para trabajar con los parámetros de la ruta, como por ejemplo:

  • get(): Devuelve el valor de un parámetro especificado por su clave.

  • getAll(): Devuelve todos los valores de un parámetro especificado por su clave.

  • has(): Comprueba si un parámetro está presente en la ruta.

  • keys(): Devuelve una lista de todas las claves de los parámetros de la ruta.

Por otro lado, snapshot.params simplemente proporciona un objeto con pares clave-valor y no tiene funciones adicionales para trabajar con los parámetros.

Captura de parámetros asíncronos

Una URL puede cambiar y a veces es conveniente estar escuchando de forma activa los cambios en la misma. Para que los Observables nos ayuden a estar atentos a estos cambios, Angular también nos permite suscribirnos a los cambios en los parámetros de URL de la siguiente manera.

ngOnInit(): void {
  this.route.paramMap
    .subscribe((params: Params) => {
      const productId = params.get('productId');
      console.log(productId);
    });
}

Query Params

Los parámetros de ruta, por ejemplo /catalogo/:categoryId, son obligatorios. Sin el ID la ruta no funcionaría. Por otro lado, existen los parámetros de consulta que los reconocerás seguidos de un ? y separados por un &, por ejemplo /catalogo?limit=10&offset=0.

1. Creando rutas con parámetros

<a routerLink="/catalogo" [queryParams]="{ category: 'electronica' }" routerLinkActive="active">Electrónica</a>

La directiva queryParams recibe un objeto y creará la ruta /catalogo?category=electronica

2. Capturar parámetros en las rutas

Para capturar estos datos en el componente, es aconsejable realizarlo en el hook de ciclo de vida ngOnInit().

Suscribiéndote a queryParams, podrás capturar y hacer uso de esta información.

// modules/website/component/catalogo/catalogo.component.ts
import { ActivatedRoute, Params } from '@angular/router';

@Component({
  selector: 'app-catalogo',
  templateUrl: './catalogo.component.html',
  styleUrls: ['./catalogo.component.scss']
})
export class CatalogoComponent implements OnInit {

  constructor(private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.route.queryParams
      .subscribe((params: Params) => {
        console.log(params.category);
      });
  }
}

Con un punto se puede hacer refencia a la ruta actual.

<a routerLink="." [queryParams]="{ category: 'electronica' }">Electrónica</a>

Optimización de tiempo y recursos

Para reducir el peso de las aplicaciones, aparece el concepto de Lazy Loading y el CodeSplitting que plantean la división del código fuente Javascript en pequeños módulos y solo cargar aquellos que el usuario necesite, cuando realmente los necesite.

Code Splitting

Es la técnica que permite dividir el código y sus recursos en varias partes (chunks) que se pueden descargar en demanda y no solo en un solo archivo como se hace tradicionalmente, mejorando la velocidad de carga de las páginas.

Lazy loading

Para poder implementar lazy loading es necesario modularizar nuestra aplicación.

Modularizacion

Un módulo encapsula varios elementos de una aplicación. Por lo general se modulariza cada grupo de componentes u otros elementos relacionados de nuestra aplicación.

Angular logra esto utilizando con @NgModule, cada módulo es como una isla que agrupa como se mencionó antes ciertas características en específico.

En otras palabras, no se pueden tener módulos que sirvan para toda la aplicación, cada vez que quieras trabajar con algún componente o característica en algún modulo en particular deberás importarlo, un ejemplo sería cuando usamos el módulo de ReactiveFormsModule.

Cuáles son los módulos en Angular

Por defecto, Angular posee un solo módulo en el archivo app.module.ts. Todos tus componentes, servicios, pipe, etc. se importan aquí. Utiliza un decorador llamado @ngModule() con un aspecto similar al siguiente:

// app.module.ts
@NgModule({
  imports: [],         // Imports de otros módulos.
  declarations: [],    // Imports de los componentes del módulo.
  exports: [],         // Exports de componentes u otros para ser utilizados en otros módulos.
  providers: [],       // Inyección de servicios.
  bootstrap: []        // Import del componente principal de la aplicación.
})
export class AppModule { }

Un módulo en Angular se define utilizando el decorador @NgModule, que recibe un objeto de metadatos con las siguientes propiedades principales:

  1. Declarations: Esta propiedad contiene un arreglo de componentes, directivas y pipes que pertenecen a este módulo.

  2. Imports: Aquí se especifican otros módulos cuyos componentes, directivas y pipes son necesarios en este módulo.

  3. Exports: Define los componentes, directivas y pipes que pueden ser utilizados por otros módulos que importen este módulo.

  4. Providers: Proporciona los servicios que deben ser disponibles de forma global o para el ámbito de este módulo.

  5. Bootstrap: Especifica el componente raíz que Angular debe iniciar para arrancar la aplicación (principalmente en el módulo raíz AppModule).

Al modularizar una aplicación, cada módulo tendrá sus componentes exclusivos, servicios o los archivos que fuesen a necesitar.

Tipos de Módulos en Angular

Podemos identificar varios tipos de módulos.

  • Root Module: Módulo por defecto de Angular que inicia toda la aplicación (App Module).

  • Core Module: Son servicios que pueden ser usados en diferentes módulos y componentes, recordar que los servicios se inyectan con provideIn : ‘root’ y se puede usar en cualquier parte de la aplicación (Globales).

  • Routing Module: Son módulos especiales para la definición de rutas.

  • Feature Domain Module: Son los módulos propios de tu aplicación.

  • Shared Module: Posee servicios o componentes compartidos por toda la aplicación, Por ejemplo, componentes pipes y directivas que se quieran usar en toda la aplicación.

Vistas Anidadas

Trabajar con Angular en una aplicación modularizada da la posibilidad de que, cada módulo, tenga a su vez N cantidad de páginas hijas.

La técnica de vistas anidadas, nos sirve para incluir ciertos componentes a otros componentes que se especifiquen, un ejemplo sería que solo las rutas que utilicen el componente Layout tendrian incorporado el componente Header

Veámos un ejemplo.

1. Crear módulos necesarios

Podríamos decir que cada grupo de rutas podría ser un módulo, entonces creamos el modulo de products.

Suponiendo que tenemos una estructura como esta:

Crearemos un modulo para la seccion de website

ng generate module pages/website --routing

2. Preparación del routing

A continuación, prepara tu app-routing.module.ts para Lazy Loading y CodeSplitting importando los módulos de la siguiente manera:

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NotFoundComponent } from './pages/website/not-found/not-found.component';

const routes: Routes = [
  {
    path: '',
    loadChildren: () => import('./pages/website/website.module').then(m => m.WebsiteModule)
  },
  {
    path: '**',
    component: NotFoundComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Observa que, a excepción del componente NotFound, solo estamos importando los módulos de una manera especial. Con loadChildren, cada módulo será enviado bajo demanda, ya que ahora se cargan de manera asíncrona.

3. Renderizar módulos

El componente principal de tu aplicación será el encargado de renderizar cada módulo. Para esto, asegúrate de que solo posea el <router-outlet>, porque es todo lo que necesitas para lograrlo.

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>',
  styleUrls: ['./app.component.scss']
})
export class AppComponent { }

Incluso puedes borrar el archivo app.components.html y colocar el <router-outlet> dentro de la propiedad template en el decorador @Component() para simplificar tu código.

4. Componentes base

Creamos un componente Layout

ng g c components/layout

En el layout del módulo website, tiene su propio <router-outlet> además del componente para la barra de navegación.

<app-nav></app-nav>
<router-outlet></router-outlet>

Este componente de barra de navegacion no va a aparecer en otras rutas si no se especifica, por ejempo en la ruta con el componente de NotFound no deberá aparecer.

5. Routing de cada módulo

Finalmente, cada <router-outlet> de cada módulo renderizará los componentes que posea dichos módulos. Para esto, prepara el routing de cada módulo de la siguiente manera.

// pages/website/website-routing.module.ts
const routes: Routes = [
  {
    path: '',
    component: LayoutComponent,
    children: [
      {
        path: '',
        redirectTo: 'home', // sin slash, de lo contrario va al rote principal
        pathMatch: 'full'
      },
      {
        path: 'home',
        component: HomeComponent
      },
      {
        path: 'products',
        component: ProductsComponent
      },
      {
        path: 'product/:productId',
        component: ProductDetailComponent
      },
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class WebsiteRoutingModule { }

Presta atención a la propiedad children que construye las nuevas reglas para las rutas.

De esta manera, puedes tener un <router-outlet> dentro de otro <router-outlet> para renderizar páginas hijas de cada módulo y tener un layout personalizado por nada uno de ellos. Además de estar optimizado el rendimiento de tu aplicación gracias al Lazy Loading y CodeSplitting.

Podemos ver que la correr nuestra aplicación se genera aparte un archivo para el modulo de website

SharedModule

Un módulo compartido donde guardarás los componentes, pipes, directivas o servicios que dos o más de tus otros módulos necesitarán y los puedes incluir en tus otros modulos.

Nota: Un componente solo puede pertenecer a un módulo, no a dos.

All Modules y Custom Strategy

Al haber activado la técnica de Lazy Loading, puedes personalizar el envío de estos módulos al cliente con diferentes estrategias.

Precarga de módulos bajo demanda

Por defecto, la aplicación enviará al cliente solo el módulo que necesita. Si ingresas al módulo website, solo se cargará su respectivo archivo JS.

Si el usuario solicita otro módulo, este se cargará solo cuando sea necesario.

Esto puede causarte problemas, ya que si el módulo solicitado es algo pesado o la conexión es lenta, tardará varios segundos en estar listo y no será buena la experiencia de usuario.

Precarga de todos los módulos

Puedes decirle a tu aplicación que, por defecto, precargue todos los módulos con la siguiente configuración:

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes, PreloadAllModules } from '@angular/router';

const routes: Routes = [
...

@NgModule({
  imports: [RouterModule.forRoot(routes, {
    preloadingStrategy: PreloadAllModules
  })],
  exports: [RouterModule]
})
export class AppRoutingModule { }ty

Importando PreloadAllModules desde @angular/router, lo pasas como parámetro al import en el decorador @NgModule(). El browser va a descargar primero los archivos base, es decir, los que tienen que ver con la carga inicial y luego que este libre el browser comenzará a precargar los módulos restantes.

  • Es ideal para aplicaciones que no sean muy grandes o no ocupen muchos módulos.

  • En el caso de tener muchos módulos es mejor usar una estrategia personalizada para evitar sobrecargar el hilo principal. De esta forma se establece que módulos se van a precargar.

Estrategia personalizada de precarga

Precargar todos los módulos a la vez, puede ser contra producente. Imagina que tu aplicación posea 50 o 100 módulos. Sería lo mismo que tener todo en un mismo archivo main.js.

Para solucionar esto, puedes personalizar la estrategia de descarga de módulos indicando qué módulos si se deben precargar y cuáles no.

1. Agrega metadata a cada ruta

Agrégale a cada regla en el routing de tu aplicación, metadata para indicarle a cada módulo si debe ser precargado, o no.

// app-routing.module.ts
const routes: Routes = [
  {
    path: 'cms',
    loadChildren: () =>
      import('./pages/cms/cms.module').then((m) => m.CmsModule),
      data: { preload: true },
  },
  {
    path: '',
    loadChildren: () =>
      import('./pages/website/website.module').then((m) => m.WebsiteModule),
  },
  {
    path: '**',
    component: NotFoundComponent,
  },
];

Con la propiedad data: { preload: true }, le indicas al servicio CustomPreloadingStrategy si el módulo debe ser precargado cuando el browser halla descargado ya todos los archivos base.

2. Crea un servicio con estrategia personalizada

Crea un servicio al cual llamaremos CustomPreloadingStrategy con la siguiente lógica.

// shared/services/custom-preloading-strategy.service.ts
import { Injectable } from '@angular/core';
import { Route, PreloadingStrategy } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class CustomPreloadingStrategyService implements PreloadingStrategy {

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    if (route.data && route.data.preload)
      return load();
    else
      return of(null);
  }

}

El servicio implementa PreloadingStrategy y sobreescribiendo el método preload(), hace uso de la metadata para desarrollar tu propia lógica de renderizado de módulos.

3. Importa tu estrategia

Finalmente, importa tu estrategia personalizada en el routing.

// app-routing.module.ts
import { CustomPreloadingStrategyService } from '../shared/services/custom-preloading-strategy.service';
// ...
@NgModule({
  imports: [RouterModule.forRoot(routes, {
    preloadingStrategy: CustomPreloadingStrategyService,
  })],
  exports: [RouterModule]
})
export class AppRoutingModule { }

De esta manera, ya puedes personalizar qué módulos serán enviados al cliente y cuáles no, mejorando así el rendimiento de tu aplicación.

Guardianes

Hay veces que queremos que determinadas áreas de nuestra aplicación web estén protegidas y solo puedan ser accedidas si el usuario ésta logueado por ejemplo, o incluso que solo puedan ser accedidas por determinados tipos de usuarios.

  • CanLoad: Sirve para evitar que la aplicación cargue los módulos Lazy si el usuario no está autorizado a hacerlo.

  • CanDeactivate: Mira si el usuario puede salir de una página, es decir, podemos hacer que aparezca un mensaje, por ejemplo, de confirmación, si el usuario tiene cambios sin guardar.

  • CanActivateChild: Mira si el usuario puede acceder a las páginas hijas de una determinada ruta.

  • CanActivate: Mira si el usuario puede acceder a una página determinada.

1. Creando el primer guard

Al utilizar este comando, nos hará una pregunta sobre qué interfaz quieres que implemente por defecto:

ng g g admin

Al auto generar el código, verás tu primer Guard con el siguiente aspecto.

// modules/shared/guards/admin.guard.ts
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AdminGuard implements CanActivate {

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return true;
  }

}

Un Guard puede devolver un booleano, una promesa con un booleano o un observable, también con un booleano. Dependiendo la lógica que tengas que aplicar para el caso sea síncrona o asíncrona.

2. Importando el guard

Ahora importa el nuevo Guard el routing de tu aplicación.

// app-routing.module.ts
import { AdminGuard } from './modules/shared/guards/admin.guard';

const routes: Routes = [
  {
    path: '',
    loadChildren: () => import('./modules/website/website.module').then(m => m.WebsiteModule),
    data: { preload: true },
  },
  {
    path: 'cms',
    loadChildren: () => import('./modules/cms/cms.module').then(m => m.CmsModule),
    canActivate: [ AdminGuard ],
    data: { preload: true },
  }
];

Agrégale a las rutas que quieras segurizar canActivate: [ AdminGuard ]. De esta manera, ya puedes implementar la lógica que necesites para cada Guard. En este caso, permitir el acceso al módulo CMS, por ejemplo, solo para usuarios administradores.

Last updated