💻 Exemplos: Capítulo 09: Roteamento SPA com Vue Router 4 (routes, router-view, guards)
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
- Dynamic Module Import:
component: () => import(...)divide os componentes em chunks separados (0.js,1.js), diminuindo o bundle inicial do Vite. - TypeScript Declaration Merging:
declare module 'vue-router'provê autocompletion em tempo de desenvolvimento parato.meta.requiredRole. - Controle Fino de Transições: O padrão
<RouterView v-slot="{ Component }">viabiliza animações CSS customizadas na alternância de rotas.