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


💻 Código de Exemplo 09 — Roteamento SPA com Lazy-Loading e Navigation Guards de Autenticação

Este exemplo demonstra a configuração corporativa do Vue Router 4 com TypeScript, incluindo carregamento sob demanda (lazy-loading), metadados de rota tipados (RouteMeta), parâmetros dinâmicos e interceptação global de segurança via router.beforeEach.

1. Definição de Rotas e Segurança: src/router/index.ts

import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';

// Declaração de tipagem estrita para RouteMeta
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean;
    requiredRole?: 'ADMIN' | 'OPERATOR' | 'USER';
    title: string;
  }
}

// Simulação de serviço de autenticação
export const authState = {
  isAuthenticated: false,
  role: 'USER' as 'ADMIN' | 'OPERATOR' | 'USER'
};

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/HomeView.vue'),
    meta: { title: 'Página Inicial' }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('../views/LoginView.vue'),
    meta: { title: 'Acesso ao Portal' }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('../views/DashboardView.vue'),
    meta: { requiresAuth: true, title: 'Painel Geral' }
  },
  {
    path: '/admin/audit',
    name: 'AuditLog',
    component: () => import('../views/AuditLogView.vue'),
    meta: { requiresAuth: true, requiredRole: 'ADMIN', title: 'Auditoria de Segurança' }
  },
  {
    path: '/resource/:id',
    name: 'ResourceDetail',
    component: () => import('../views/ResourceDetailView.vue'),
    props: true, // Injeta $route.params.id diretamente como prop no componente
    meta: { requiresAuth: true, title: 'Detalhes do Recurso' }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('../views/NotFoundView.vue'),
    meta: { title: '404 Não Encontrado' }
  }
];

export const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(_to, _from, savedPosition) {
    return savedPosition || { top: 0 };
  }
});

// Navigation Guard Global
router.beforeEach((to, _from, next) => {
  // Atualiza título da aba do navegador
  document.title = `${to.meta.title} | Portal Corporativo`;

  // Checagem de autenticação
  if (to.meta.requiresAuth && !authState.isAuthenticated) {
    return next({ name: 'Login', query: { redirect: to.fullPath } });
  }

  // Checagem de autorização por perfil (RBAC)
  if (to.meta.requiredRole && authState.role !== to.meta.requiredRole) {
    console.warn(`[Router Guard] Acesso negado à rota ${to.path}. Permissão insuficiente.`);
    return next({ name: 'Dashboard' });
  }

  next();
});

2. Componente de Layout Raiz: src/App.vue

<!-- src/App.vue -->
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { authState } from './router';

const route = useRoute();
const router = useRouter();

function toggleAuth(): void {
  authState.isAuthenticated = !authState.isAuthenticated;
  authState.role = authState.isAuthenticated ? 'ADMIN' : 'USER';

  if (!authState.isAuthenticated && route.meta.requiresAuth) {
    router.push({ name: 'Login' });
  }
}
</script>

<template>
  <div class="min-h-screen bg-slate-100 flex flex-col font-sans">
    <!-- Barra de Navegação -->
    <header class="bg-white border-b sticky top-0 z-50">
      <div class="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
        <div class="flex items-center gap-6">
          <span class="font-extrabold text-indigo-600 text-lg">⚡ VueRouter4</span>
          <nav class="flex items-center gap-4 text-sm font-medium">
            <RouterLink
              to="/"
              class="text-slate-600 hover:text-indigo-600 transition-colors"
              active-class="text-indigo-600 font-bold"
            >
              Início
            </RouterLink>
            <RouterLink
              to="/dashboard"
              class="text-slate-600 hover:text-indigo-600 transition-colors"
              active-class="text-indigo-600 font-bold"
            >
              Dashboard
            </RouterLink>
            <RouterLink
              to="/admin/audit"
              class="text-slate-600 hover:text-indigo-600 transition-colors"
              active-class="text-indigo-600 font-bold"
            >
              Auditoria (Admin)
            </RouterLink>
          </nav>
        </div>

        <div class="flex items-center gap-3">
          <span class="text-xs px-2 py-1 rounded bg-slate-100 font-mono">
            Role: <strong>{{ authState.role }}</strong>
          </span>
          <button
            @click="toggleAuth"
            class="text-xs font-semibold px-3 py-1.5 rounded-lg border transition-colors"
            :class="authState.isAuthenticated ? 'bg-red-50 text-red-700 border-red-200' : 'bg-emerald-50 text-emerald-700 border-emerald-200'"
          >
            {{ authState.isAuthenticated ? 'Fazer Logout' : 'Simular Login (Admin)' }}
          </button>
        </div>
      </div>
    </header>

    <!-- Ponto de Montagem das Páginas -->
    <main class="flex-1 max-w-6xl w-full mx-auto p-6">
      <RouterView v-slot="{ Component }">
        <transition name="fade" mode="out-in">
          <component :is="Component" />
        </transition>
      </RouterView>
    </main>
  </div>
</template>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.15s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

🖥️ Saída / Comportamento Esperado

Estado Deslogado:
Navegar para "/": Rota pública carrega HomeView. Document title: "Página Inicial | Portal Corporativo".
Clicar em "Dashboard":
Guard intercepta: to.meta.requiresAuth === true e isAuthenticated === false.
Redirecionamento automático para "/login?redirect=/dashboard".

Ao clicar em "Simular Login (Admin)":
authState.isAuthenticated torna-se true com Role 'ADMIN'.
Navegar para "/admin/audit":
Carregamento sob demanda do chunk AuditLogView.vue via lazy-loading.
Acesso concedido com transição fade suave.

Ao clicar em "Fazer Logout":
Estando em uma rota protegida, o sistema expulsa o usuário automaticamente para a tela de login.

🔍 Destaques da Implementação

  1. Dynamic Module Import: component: () => import(...) divide os componentes em chunks separados (0.js, 1.js), diminuindo o bundle inicial do Vite.
  2. TypeScript Declaration Merging: declare module 'vue-router' provê autocompletion em tempo de desenvolvimento para to.meta.requiredRole.
  3. Controle Fino de Transições: O padrão <RouterView v-slot="{ Component }"> viabiliza animações CSS customizadas na alternância de rotas.