Especialização em Front-End com Angular • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 14 — Roteamento com Functional Guards (CanActivateFn) e Lazy-Loading

Este exemplo apresenta a configuração moderna de rotas no Angular 18+, utilizando divisão de código por rota (Lazy-Loading com loadComponent), injeção de parâmetros de rota direto em Signals com withComponentInputBinding e proteção de acessos com Functional Guards (CanActivateFn).

1. Guards Funcionais e Definição de Rotas: src/app/app.routes.ts

// src/app/app.routes.ts
import { Routes, CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';

// Simulação de serviço de autenticação
export class AuthService {
  isAuthenticated = false;
  userRole: 'ADMIN' | 'OPERATOR' | 'GUEST' = 'GUEST';
}

// 1. Functional Guard: canActivateFn limpo sem classes ou interfaces legadas
export const authGuard: CanActivateFn = (route, state) => {
  const router = inject(Router);
  // Simulação de leitura de token ou serviço
  const token = localStorage.getItem('auth_token');

  if (!token) {
    console.warn(`[Router Guard] Acesso negado a ${state.url}. Redirecionando para login.`);
    return router.parseUrl('/login?redirect=' + encodeURIComponent(state.url));
  }

  // Checagem de RBAC através de data/meta da rota
  const requiredRole = route.data?.['requiredRole'];
  const userRole = localStorage.getItem('user_role') || 'GUEST';

  if (requiredRole && userRole !== requiredRole) {
    console.warn('[Router Guard] Permissão insuficiente para rota administrativa.');
    return router.parseUrl('/forbidden');
  }

  return true;
};

// 2. Configuração de Rotas com Lazy-Loading
export const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'dashboard'
  },
  {
    path: 'login',
    loadComponent: () =>
      import('./pages/login/login.component').then((m) => m.LoginComponent)
  },
  {
    path: 'dashboard',
    canActivate: [authGuard],
    loadComponent: () =>
      import('./pages/dashboard/dashboard.component').then((m) => m.DashboardComponent)
  },
  {
    path: 'cluster/:clusterId',
    canActivate: [authGuard],
    data: { requiredRole: 'ADMIN' },
    loadComponent: () =>
      import('./pages/cluster-detail/cluster-detail.component').then((m) => m.ClusterDetailComponent)
  },
  {
    path: '**',
    loadComponent: () =>
      import('./pages/not-found/not-found.component').then((m) => m.NotFoundComponent)
  }
];

2. Componente com Input Binding de Parâmetro: src/app/pages/cluster-detail/cluster-detail.component.ts

// src/app/pages/cluster-detail/cluster-detail.component.ts
import { Component, input, computed } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-cluster-detail',
  standalone: true,
  imports: [RouterLink],
  template: `
    <div class="max-w-xl mx-auto p-6 bg-white border border-slate-200 rounded-2xl shadow-sm space-y-4">
      <div class="flex items-center justify-between border-b pb-3">
        <div>
          <span class="text-xs font-mono font-bold text-red-600 uppercase">Rota Protegida (ADMIN)</span>
          <h2 class="text-xl font-bold text-slate-800">Detalhes do Cluster</h2>
        </div>
        <a routerLink="/dashboard" class="text-xs text-indigo-600 hover:underline font-semibold">
          ← Voltar
        </a>
      </div>

      <!-- O parâmetro :clusterId da URL é injetado diretamente no Signal input()! -->
      <div class="p-4 bg-slate-50 border rounded-xl space-y-2 text-xs">
        <p><strong>ID Injetado via URL:</strong> <code class="font-mono text-indigo-600 font-bold">{{ clusterId() }}</code></p>
        <p><strong>URI Canônica:</strong> {{ canonicalUri() }}</p>
        <p><strong>Nível de Acesso:</strong> Autenticado com Perfil Master</p>
      </div>
    </div>
  `
})
export class ClusterDetailComponent {
  // withComponentInputBinding() injeta :clusterId diretamente nesta input reativa!
  clusterId = input.required<string>();

  canonicalUri = computed(() => `https://clusters.corp.internal/k8s/${this.clusterId()}`);
}

🖥️ Saída / Comportamento Esperado

Cenário 1 — Usuário Não Autenticado acessa "/dashboard":
authGuard intercepta a navegação:
localStorage.getItem('auth_token') é null.
Console: "[Router Guard] Acesso negado a /dashboard. Redirecionando para login."
A URL é redirecionada para "/login?redirect=%2Fdashboard".

Cenário 2 — Usuário Autenticado acessa "/cluster/k8s-sa-east-01":
authGuard valida o token e o perfil 'ADMIN'.
Navegação concedida.
O compilador carrega o chunk cluster-detail.component.js sob demanda (lazy-loading).
withComponentInputBinding() vincula o parâmetro 'k8s-sa-east-01' da rota diretamente no signal clusterId().
A tela exibe:
"ID Injetado via URL: k8s-sa-east-01"
"URI Canônica: https://clusters.corp.internal/k8s/k8s-sa-east-01"

🔍 Destaques da Implementação

  1. CanActivateFn Funcional: Não requer criação de classes de serviço nem registro em providers, reduzindo boilerplate em 70%.
  2. withComponentInputBinding(): Dispensa a injeção de ActivatedRoute para ler parâmetros simples da URL, transformando-os em input() signals.
  3. Divisão de Chunks Eficiente: Cada rota em loadComponent gera um arquivo JS independente que só é baixado sob demanda.