Capítulo 14: Roteamento Avançado (Lazy Loading, Functional Guards e Resolvers)

Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR


🗺️ Mapa Conceitual da Pipeline de Roteamento Moderno

flowchart TD
    URL["Mudança de URL no Navegador (/admin/relatorios/42)"] --> Router["Angular Router (provideRouter)"]

    subgraph PIPELINE["Pipeline de Decisão e Carregamento"]
        G1["CanMatchFn: A rota existe e corresponde ao usuário?"]
        G2["CanActivateFn: O usuário possui permissão de acesso?"]
        L["loadComponent() / loadChildren(): Download Lazy do Chunk JS"]
        R["ResolveFn: Pré-carregamento dos dados necessários"]
        B["withComponentInputBinding: Injeta :id e data direto em input()"]
    end

    Router --> G1
    G1 -->|Autorizado| G2
    G2 -->|Autorizado| L
    L --> R
    R --> B

    B --> TargetComponent["Componente Standalone Renderizado no <router-outlet />"]
    G2 -->|Negado| Redirect["Redirecionamento para /login via UrlTree"]

    style URL fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style Router fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style PIPELINE fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style TargetComponent fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style Redirect fill:#fce4ec,stroke:#e91e63,stroke-width:2px

🏛️ 1. O Roteador Standalone no Angular 18+

No Angular moderno, o roteador foi completamente reestruturado em torno de funções puras e injeção funcional. Classes legadas baseadas em interfaces (canActivate: [AuthGuard]) foram substituídas por guardas funcionais (canActivate: [authGuard]), eliminando arquivos de classe desnecessários e tornando a árvore de rotas muito mais legível.

1.1 Configuração Central no app.config.ts

A inicialização do roteador utiliza o provedor provideRouter acompanhado de recursos opcionais modernos:

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding, withViewTransitions, withPreloading, PreloadAllModules } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding(), // Liga parâmetros de URL (:id) diretamente a input()
      withViewTransitions(),       // Animações fluidas nativas da View Transitions API
      withPreloading(PreloadAllModules) // Pré-carrega chunks em background após idle
    )
  ]
};

🚀 2. Lazy Loading com loadComponent e Sub-rotas

O carregamento preguiçoso (Lazy Loading) é o pilar fundamental para garantir tempos de carregamento instantâneos e altas pontuações no Google Core Web Vitals:

// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard, adminGuard } from './core/guards/auth.guard';
import { produtoResolver } from './core/resolvers/produto.resolver';

export const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'catalogo'
  },
  {
    path: 'catalogo',
    // Carrega o componente Standalone sob demanda em um chunk JS isolado
    loadComponent: () => import('./features/catalogo/catalogo.component').then(m => m.CatalogoComponent)
  },
  {
    path: 'produto/:id',
    loadComponent: () => import('./features/detalhe-produto/detalhe-produto.component').then(m => m.DetalheProdutoComponent),
    resolve: {
      detalhes: produtoResolver // Executa antes de exibir o componente
    }
  },
  {
    path: 'admin',
    canActivate: [authGuard, adminGuard], // Proteção com guardas funcionais
    // Carrega um arquivo inteiro de sub-rotas isoladas
    loadChildren: () => import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES)
  },
  {
    path: '**',
    loadComponent: () => import('./features/not-found/not-found.component').then(m => m.NotFoundComponent)
  }
];

🛡️ 3. Guardas Funcionais: CanActivateFn e CanDeactivateFn

As guardas funcionais utilizam inject() internamente para validar permissões, redirecionar o usuário via UrlTree ou impedir a perda de dados em formulários não salvos:

3.1 Guarda de Autenticação (CanActivateFn)

// core/guards/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.isAutenticado()) {
    return true; // Acesso liberado
  }

  // Redireciona com segurança preservando a URL de destino
  return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};

3.2 Guarda de Prevenção de Perda de Dados (CanDeactivateFn)

Impede que o usuário feche a aba ou navegue para fora caso tenha alterações pendentes:

import { CanDeactivateFn } from '@angular/router';

export interface FormularioPendente {
  temAlteracoesNaoSalvas(): boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<FormularioPendente> = (component) => {
  if (component.temAlteracoesNaoSalvas()) {
    return confirm('Você possui alterações não salvas. Deseja realmente sair desta página?');
  }
  return true;
};

📥 4. Component Input Binding (withComponentInputBinding)

Com o withComponentInputBinding(), parâmetros de rota (:id), query params (?busca=angular) e dados de resolver (resolve: { ... }) são injetados diretamente em Signal Inputs do componente, eliminando a dependência direta de ActivatedRoute:

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-detalhe-produto',
  standalone: true,
  template: `
    <div class="card">
      <h2>Produto ID: {{ id() }}</h2>
      <p>Termo de Busca de Origem: {{ busca() }}</p>
      <p>Título: {{ detalhes().nome }}</p>
    </div>
  `
})
export class DetalheProdutoComponent {
  // Injetado automaticamente pelo Angular Router a partir da rota '/produto/:id'
  id = input.required<string>();

  // Injetado automaticamente a partir do query param '?busca=term'
  busca = input<string>('');

  // Injetado automaticamente a partir do produtoResolver
  detalhes = input.required<{ id: string; nome: string; preco: number }>();
}

⚖️ 5. Comparativo: Roteamento Clássico vs Funcional

Característica Roteamento Clássico (Angular <15) Roteamento Moderno (Angular 18+)
Configuração RouterModule.forRoot(routes) provideRouter(routes, ...)
Definição de Guardas Classes com @Injectable e implements CanActivate Funções puras tipadas (CanActivateFn)
Injeção de Parâmetros Injeção manual de ActivatedRoute e snapshot withComponentInputBinding() via input()
Carregamento de Módulos loadChildren: () => import().then(m => m.MeuModule) loadComponent direto para a classe Standalone
Animações de Página Angular Animations complexas no template withViewTransitions() nativo do navegador

⚠️ 6. Armadilhas Comuns e Anti-Patterns

  1. Retornar false em vez de UrlTree no Redirecionamento:
    • Problema: Retornar simplesmente false na guarda bloqueia a rota, mas deixa a URL e a tela travadas no limbo.
    • Solução: Retorne sempre router.createUrlTree(['/destino']) para que o roteador realize o redirecionamento automático de forma atômica.
  2. Esquecer de Adicionar <router-outlet />:
    • Se o template principal não possuir a tag <router-outlet />, os componentes roteados não encontrarão um ponto de montagem na tela.

📋 7. Cheat-Sheet: Resumo Executivo

// Guarda funcional
export const minhaGuarda: CanActivateFn = () => {
  return inject(AuthService).isLogado() || inject(Router).parseUrl('/login');
};

// Rota Standalone Lazy
{
  path: 'dashboard',
  canActivate: [minhaGuarda],
  loadComponent: () => import('./dash.component').then(c => c.DashComponent)
}