En esta página
Vue Router 4 — navegación en SPA
¿Qué es Vue Router?
Vue Router es el enrutador oficial de Vue. Permite crear Single Page Applications (SPA) donde la navegación entre páginas ocurre en el cliente sin recargas completas del servidor. Vue Router 4 está diseñado específicamente para Vue 3 y aprovecha la Composition API.
Instalación y configuración básica
Vue Router ya viene incluido si seleccionaste "Router" al crear el proyecto con create-vue. Si necesitas instalarlo manualmente:
npm install vue-router@4La configuración del router vive en src/router/index.ts y se registra como plugin en main.ts:
// main.ts
import { createApp } from 'vue'
import router from './router'
import App from './App.vue'
createApp(App).use(router).mount('#app')RouterView y RouterLink
<RouterView> es donde Vue Router renderiza el componente de la ruta activa. <RouterLink> es el componente de enlace que reemplaza a <a>:
<!-- App.vue -->
<template>
<nav>
<!-- RouterLink genera un <a> con la clase "router-link-active" cuando está activo -->
<RouterLink to="/">Inicio</RouterLink>
<RouterLink to="/productos">Productos</RouterLink>
<RouterLink :to="{ name: 'usuario', params: { id: 42 } }">Perfil</RouterLink>
<!-- active-class personaliza la clase CSS cuando el enlace está activo -->
<RouterLink to="/admin" active-class="activo">Admin</RouterLink>
</nav>
<!-- Aquí se renderiza el componente de la ruta activa -->
<RouterView />
</template>Parámetros de ruta
Los parámetros dinámicos se definen con :nombre en la ruta:
// En el router
{
path: '/usuarios/:id',
name: 'usuario-detalle',
component: () => import('@/views/UsuarioDetalle.vue'),
props: true, // Pasa params como props al componente
}<!-- UsuarioDetalle.vue con props: true -->
<script setup lang="ts">
// El param 'id' llega como prop cuando props: true está en la ruta
const { id } = defineProps<{ id: string }>()
</script>
<!-- O accediendo via useRoute() -->
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
const id = route.params['id'] as string
</script>Parámetros opcionales y comodín
const rutas = [
{ path: '/buscar/:termino?', component: Buscar }, // ? = opcional
{ path: '/:pathMatch(.*)*', component: NotFound }, // Comodín 404
]Query strings y hash
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
// URL: /productos?categoria=electronica&orden=precio#seccion
const categoria = route.query['categoria'] as string | undefined
const orden = route.query['orden'] as string | undefined
const seccion = route.hash // '#seccion'
</script>Navegar con query:
router.push({
name: 'productos',
query: { categoria: 'electronica', orden: 'precio' },
hash: '#seccion'
})Rutas anidadas
Las rutas anidadas permiten layouts con sub-vistas:
{
path: '/cuenta',
component: CuentaLayout, // Tiene su propio <RouterView>
children: [
{ path: '', component: CuentaOverview }, // /cuenta
{ path: 'perfil', component: CuentaPerfil }, // /cuenta/perfil
{ path: 'seguridad', component: CuentaSeguridad } // /cuenta/seguridad
]
}<!-- CuentaLayout.vue -->
<template>
<div class="cuenta-layout">
<nav>
<RouterLink to="/cuenta">Overview</RouterLink>
<RouterLink to="/cuenta/perfil">Perfil</RouterLink>
<RouterLink to="/cuenta/seguridad">Seguridad</RouterLink>
</nav>
<main>
<RouterView /> <!-- Renderiza la ruta hija activa -->
</main>
</div>
</template>Navigation Guards (guardias de navegación)
Los guardias permiten controlar si una navegación puede proceder:
Guard en componente
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
const tienesCambiosGuardados = ref(false)
// Se ejecuta antes de salir de este componente
onBeforeRouteLeave((_to, _from, next) => {
if (tienesCambiosGuardados.value) {
const confirmar = window.confirm('¿Salir sin guardar los cambios?')
next(confirmar) // next(true) continúa, next(false) cancela
} else {
next()
}
})
// Se ejecuta cuando la ruta cambia pero el componente se reutiliza (ej: cambio de :id)
onBeforeRouteUpdate(async (to) => {
const nuevoId = to.params['id'] as string
await cargarProducto(nuevoId)
})
</script>Guard por ruta
{
path: '/premium',
component: PremiumView,
beforeEnter: (to, from) => {
if (!usuarioTienePlan('premium')) {
return { name: 'planes' }
}
}
}Lazy loading y chunks con nombre
const rutas = [
{
path: '/admin',
// webpackChunkName (también funciona con Vite) agrupa en un chunk
component: () => import(/* webpackChunkName: "admin" */ '@/views/AdminView.vue'),
},
{
path: '/admin/reportes',
component: () => import(/* webpackChunkName: "admin" */ '@/views/ReportesView.vue'),
},
]Navegación programática
import { useRouter } from 'vue-router'
const router = useRouter()
// Navegar a una ruta
router.push('/productos')
router.push({ name: 'producto-detalle', params: { id: '42' } })
router.push({ path: '/buscar', query: { q: 'vue' } })
// Reemplazar la entrada actual en el historial (no añade al historial)
router.replace({ name: 'inicio' })
// Navegar relativo al historial
router.back() // Equivale a history.back()
router.forward() // Equivale a history.forward()
router.go(-2) // Ir 2 pasos atrás en el historialRouterLink avanzado
<template>
<!-- exact — solo activo en la ruta exacta (no en subrutas) -->
<RouterLink to="/" :exact="true">Inicio</RouterLink>
<!-- Slot por defecto — acceso al estado isActive -->
<RouterLink to="/productos" custom v-slot="{ navigate, isActive, href }">
<a
:href="href"
:class="{ 'mi-clase-activa': isActive }"
@click="navigate"
>
Productos
</a>
</RouterLink>
</template>Meta tipado con TypeScript
Para tipar route.meta, extiende el módulo de Vue Router:
// src/router/types.d.ts
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiereAuth?: boolean
roles?: ('admin' | 'editor' | 'usuario')[]
titulo?: string
descripcion?: string
}
}Ahora to.meta.requiereAuth está completamente tipado.
Práctica
- SPA con 3 rutas: Crea un proyecto con rutas para Inicio, Blog (con lista de posts) y Detalle del post (
/blog/:slug). Usa lazy loading en todas las vistas y un<RouterLink>conactive-classen el nav. - Guard de autenticación: Implementa un guard global
beforeEachque rediriga a/loginsi la ruta tienemeta.requiereAuth: truey el usuario no está autenticado. Guarda la URL original enquery.redirectpara redirigir después del login. - Rutas anidadas con layout: Crea un layout de dashboard con navegación lateral y subrutas (
/dashboard,/dashboard/perfil,/dashboard/configuracion). Cada subvista tiene su propio componente renderizado en el<RouterView>del layout.
En la siguiente lección aprenderemos Pinia, la solución oficial de gestión de estado global para Vue.
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
const rutas: RouteRecordRaw[] = [
{
path: '/',
name: 'inicio',
// Lazy loading — el chunk solo se carga cuando se navega a esta ruta
component: () => import('@/views/InicioView.vue'),
},
{
path: '/productos',
name: 'productos',
component: () => import('@/views/ProductosView.vue'),
},
{
path: '/productos/:id',
name: 'producto-detalle',
component: () => import('@/views/ProductoDetalleView.vue'),
// Proporcionar params como props al componente
props: true,
},
{
path: '/admin',
component: () => import('@/views/AdminLayout.vue'),
meta: { requiereAuth: true, roles: ['admin'] },
// Rutas anidadas
children: [
{
path: '', // /admin
name: 'admin-dashboard',
component: () => import('@/views/AdminDashboard.vue'),
},
{
path: 'usuarios', // /admin/usuarios
name: 'admin-usuarios',
component: () => import('@/views/AdminUsuarios.vue'),
},
],
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/views/NotFoundView.vue'),
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: rutas,
scrollBehavior(to, _from, savedPosition) {
if (savedPosition) return savedPosition
if (to.hash) return { el: to.hash, behavior: 'smooth' }
return { top: 0 }
},
})
export default router
import type { Router } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
export function registrarGuards(router: Router): void {
// Guard global — se ejecuta antes de cada navegación
router.beforeEach(async (to, _from) => {
const auth = useAuthStore()
// Esperar a que la autenticación esté lista
if (!auth.listo) await auth.inicializar()
const requiereAuth = to.meta.requiereAuth === true
const rolesPermitidos = to.meta.roles as string[] | undefined
// Redirigir al login si no está autenticado
if (requiereAuth && !auth.usuario) {
return { name: 'login', query: { redirect: to.fullPath } }
}
// Verificar rol
if (rolesPermitidos && auth.usuario) {
const tieneRol = rolesPermitidos.includes(auth.usuario.rol)
if (!tieneRol) {
return { name: 'no-autorizado' }
}
}
// Continuar la navegación (devolver undefined o true)
})
// Guard global de post-navegación
router.afterEach((to) => {
document.title = (to.meta.titulo as string | undefined) ?? 'Mi App'
})
}
Inicia sesión para guardar tu progreso