Capítulo 09: Roteamento SPA com Vue Router 4 (routes, router-view, guards)
Capítulo 09: Roteamento SPA com Vue Router 4 (routes, router-view, guards)
Especialização em Front-End com Vue • Vue 3 & Nuxt 3 • Composition API, Pinia, Vue Router, SSR e TypeScript
🗺️ Mapa Conceitual do Ciclo de Navegação e Guards
flowchart TD
A["Navegação Disparada (RouterLink ou router.push)"] --> B["1. router.beforeEach (Guard Global)"]
B --> C{"Requer Autenticação / Permissão?"}
C -->|Não Autorizado| D["Redireciona para /login (next('/login'))"]
C -->|Permitido| E["2. beforeEnter (Guard por Rota)"]
E --> F["3. beforeRouteEnter (No Componente de Destino)"]
F --> G["4. router.beforeResolve (Última Barreira Global)"]
G --> H["5. Atualização da URL no Histórico do Navegador"]
H --> I["6. router.afterEach (Atualiza document.title / Analytics)"]
I --> J["Renderização do Componente no <RouterView>"]
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style J fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
🏛️ 1. Arquitetura de Roteamento SPA com Vue Router 4
O Vue Router 4 é o roteador oficial para aplicações Single Page Application (SPA) construídas em Vue 3. Ele foi integralmente reescrito em TypeScript, oferecendo integração estrita com a Composition API através dos composables useRoute() (para leitura de parâmetros e query) e useRouter() (para comandos de navegação programática).
1.1 Modos de Histórico: HTML5 vs Hash Mode
createWebHistory()(HTML5 History): Utiliza as APIs nativas do navegador (pushState,replaceState). As URLs são limpas e semânticas (ex:https://meuapp.com/dashboard/relatorios). Exige configuração no servidor web (Nginx/Apache/Cloudflare) para reescrever todas as rotas para o arquivoindex.htmlraiz (fallback de 404).createWebHashHistory()(Hash Mode): Utiliza o fragmento de hash da URL (ex:https://meuapp.com/#/dashboard). Não requer nenhuma configuração especial no servidor web, porém prejudica a indexação em motores de busca (SEO).
1.2 Lazy-Loading de Rotas e Divisão de Código
Para evitar que a aplicação baixe um bundle JavaScript monolítico gigantesco no primeiro acesso, as rotas corporativas devem adotar Dynamic Imports:
{
path: '/analytics',
name: 'Analytics',
component: () => import('../views/AnalyticsView.vue') // Chunk separado sob demanda
}
1.3 TypeScript Declaration Merging para RouteMeta
No Vue Router 4, você pode tipar metadados customizados de rota (como permissões RBAC, breadcrumbs ou títulos) estendendo a interface do módulo:
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean;
roles?: ('ADMIN' | 'OPERATOR')[];
title: string;
}
}
1.4 A Ordem Exata dos Navigation Guards
A segurança e a autorização baseiam-se na sequência canônica de interceptadores:
router.beforeEach: Guard global inicial. Ideal para checagem de tokens de autenticação.beforeEnternas rotas: Validação específica daquele endpoint.router.beforeResolve: Disparado após todos os guards de componente terem resolvido, imediatamente antes da navegação ser confirmada.router.afterEach: Disparado após a conclusão da navegação (usado para trackers como Google Analytics e atualização de<title>).
⚖️ 2. Tabela Comparativa de Estratégias de Navegação
| Mecanismo | Declaração | Caso de Uso Primário |
|---|---|---|
<RouterLink to="..."> |
Declarativo no template. | Menus de navegação, breadcrumbs, links semânticos com active-class. |
router.push(...) |
Imperativo via código (useRouter). |
Redirecionamento após submissão de formulário ou autenticação bem-sucedida. |
router.replace(...) |
Imperativo (substitui histórico). | Redirecionamento de telas de login ou 404 (não polui o botão “Voltar” do browser). |
router.go(delta) |
Imperativo numérico. | Avançar (+1) ou retroceder (-1) no histórico do navegador. |
⚠️ 3. Armadilhas Comuns em Produção (Gotchas & Anti-Patterns)
Anti-Pattern 1: Loop Infinito de Redirecionamento no beforeEach
Erro Comum:
router.beforeEach((to, from, next) => {
if (!isAuthenticated) {
next({ name: 'Login' }); // Se 'Login' também passar por este guard, cria loop infinito de crashes!
}
});
Abordagem Correta: Verifique se a rota atual já é a página de login: if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' }); else next();.
Anti-Pattern 2: Confundir useRoute() com useRouter()
useRoute(): Objeto reativo somente leitura com dados da rota ativa (params,query,path,meta).useRouter(): Instância controladora da máquina de rotas com métodos (push,replace,back).
💻 4. Código Conceitual Arquitetural Comentado
// src/router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('../views/HomeView.vue'),
meta: { title: 'Página Inicial' }
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('../views/DashboardView.vue'),
meta: { requiresAuth: true, title: 'Painel Geral' },
children: [
{
path: 'relatorios',
name: 'DashboardReports',
component: () => import('../views/ReportsView.vue'),
meta: { requiresAuth: true, title: 'Relatórios Fiscais' }
}
]
},
{
path: '/login',
name: 'Login',
component: () => import('../views/LoginView.vue'),
meta: { title: 'Autenticação' }
}
];
export const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(_to, _from, savedPosition) {
return savedPosition || { top: 0, behavior: 'smooth' };
}
});
// Guard Global de Segurança e Atualização de Título
router.beforeEach((to, _from, next) => {
const token = localStorage.getItem('access_token');
const isAuthRequired = to.matched.some((record) => record.meta.requiresAuth);
if (isAuthRequired && !token) {
return next({ name: 'Login', query: { redirect: to.fullPath } });
}
next();
});
router.afterEach((to) => {
document.title = `${to.meta.title ?? 'App'} | Portal Corporativo`;
});
🛠️ 5. Cheat-Sheet do Vue Router 4
// Leitura de parâmetros de rota no componente:
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
// Acessando :id da URL (/usuario/:id)
const userId = route.params.id;
// Acessando query strings (?filtro=ativo)
const filtro = route.query.filtro;
// Navegação programática com substituição
router.replace({ name: 'Dashboard' });