Ir al contenido
wannacode

Enrutamiento

En resumen: El enrutamiento conecta la dirección con la página: Vue Router muestra el componente de la ruta, los enlaces no recargan, y los parámetros como /product/:id salen de la dirección.

Problema

Un enlace que no lleva a ninguna parte

La tienda tiene un catálogo y una página para cada producto. Abre «Leche fresca» y vuelve.

Vue

Parece que está bien. Pero qué página ves vive solo en el estado del componente (productId). La dirección del navegador es la misma para el catálogo y para cada producto. Por eso:

  • no puedes enviarle a un amigo un enlace a la leche: recibirá el catálogo;
  • «Recargar» te devuelve al catálogo, porque el estado se pierde;
  • «Atrás» en el navegador no vuelve al catálogo, sino que sale de la tienda;
  • los productos son botones, no enlaces: «Abrir en una pestaña nueva» no funciona.

(El sandbox no tiene barra de direcciones, así que esto no se nota en él. En un sitio real lo ves enseguida.)

La idea

La dirección es el estado

Cada página necesita su propia dirección: / para el catálogo, /product/5 para la leche, /cart para el carrito. Entonces la dirección dice qué página mostrar, no el estado en memoria.

La aplicación sigue siendo una sola página (una single-page application): el navegador carga el HTML una vez. Con un clic en un enlace la biblioteca detiene la recarga, cambia la dirección con la API History del navegador y dibuja otro componente. Eso es lo que hace el router. Tiene una tabla de rutas: un patrón de dirección y un componente para él.

Vue Router recibe la tabla como un array. El router se crea en su propio archivo y se añade a la aplicación con use:

router.js
import { createRouter, createMemoryHistory } from 'vue-router';
export const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: Catalog },
{ path: '/product/:id', component: ProductPage },
{ path: '/cart', component: CartPage },
],
});
// main.js
createApp(App).use(router).mount('#app');

En App.vue el lugar para la página es <RouterView />. Ahí el router dibuja el componente que coincide con la dirección. Todo lo que lo rodea (el menú de arriba) se ve en todas las páginas.

:id en /product/:id es un parámetro: representa un segmento de la dirección. Una sola ruta cubre todos los productos.

Míralo

Mira cómo la dirección elige la página

<RouterLink :to="`/product/${p.id}`">{{ p.name }}</RouterLink>
const routes = [
{ path: '/', component: Catalog },
{ path: '/product/:id', component: ProductPage },
{ path: '/cart', component: CartPage },
{ path: '/:pathMatch(.*)*', component: NotFound },
];
<!-- ProductPage.vue -->
const route = useRoute();
const product = computed(() =>
PRODUCTS.find((p) => p.id === Number(route.params.id)),
);
<h1>{{ product.name }}</h1>
<!-- NotFound.vue -->
<h1>Esa página no existe</h1>
Paso a paso, en palabras
  1. El router tiene una tabla: un patrón de dirección y el componente para él. La dirección ahora es /, el catálogo.
  2. Un clic en «Leche fresca». El router impide que la página se recargue y solo cambia la dirección a /product/5.
  3. Compara la dirección con cada fila. «/product/:id» coincide: :id representa cualquier segmento. La fila para todo lo demás también coincide, pero es más general y pierde.
  4. Del resultado sale el parámetro: id = "5". Es texto, porque la dirección es texto. ProductPage lee route.params.id, y Number(…) lo convierte en 5.
  5. ProductPage encuentra el producto 5 y dibuja su página. El mismo componente dibuja cada producto: solo cambia el parámetro.
  6. Ahora la dirección es /sale, y esa página todavía no existe. No coincide ninguna fila salvo la última, así que el router dibuja NotFound.
  7. La dirección es el estado de la página. Puedes compartirla, recargarla y volver a ella, y la tabla siempre da la misma página.

El router no sabe nada de productos. Compara el texto de la dirección con los patrones y le pasa el parámetro al componente. Qué significa 5 lo decide el componente.

La idea

En el sandbox y en un proyecto real

El router de un sitio real lee y escribe la dirección en la barra de direcciones del navegador. Eso es createWebHistory(). El sandbox de las lecciones es una página dentro de la página de la lección y no tiene dirección propia. Por eso aquí da un error claro:

Vue

En el sandbox usamos createMemoryHistory(): un router que guarda la dirección en memoria. Todo lo demás es igual: la tabla, los enlaces, los parámetros. La dirección la mostramos nosotros, al pie de la página.

En un proyecto real solo cambia el historial:

import { createRouter, createWebHistory } from 'vue-router';
export const router = createRouter({
history: createWebHistory(),
routes,
});

Y algo más que el sandbox no muestra: el servidor. Cuando alguien abre /product/5 directamente o pulsa «Recargar», el navegador le pide esa dirección al servidor. Ese archivo no existe. El servidor debe devolver index.html para toda dirección que no sea un archivo, y entonces el router elige la página. El servidor de desarrollo de Vite lo hace solo. En nginx lo hace la línea try_files:

location / {
try_files $uri /index.html;
}

Pruébalo

Una página para cada producto

La tabla tiene el catálogo y el carrito, y los productos llevan a /product/1, /product/5 y /product/7, pero esa ruta no existe. Añade una ruta con parámetro para la página del producto. ProductPage ahora siempre muestra el pan: haz que tome el id de la dirección. La comprobación abre la leche, luego el queso desde «Ver también», luego el pan desde el catálogo y mira el encabezado y la dirección.

Vue

Pruébalo

Encuentra el error

Todas las páginas de producto dicen «No existe ese producto», y los productos están en los datos. ¿Qué línea tiene la culpa?

Elige la línea con el error.

La idea

Enlaces y navegación desde el código

Enlaces. A una página de la aplicación se llega con <RouterLink to="/cart">. En la página es un <a href="/cart"> normal: «Abrir en una pestaña nueva» funciona y un lector de pantalla lo lee como enlace. Con un clic normal el router detiene la recarga (preventDefault) y solo cambia la dirección. Si escribes <a href> tú mismo, el navegador carga todo el sitio otra vez, y el carrito en memoria desaparece.

Para el menú existe RouterLink con la clase que le da el router: el enlace a la página actual recibe aria-current="page" y puede tener su propio estilo. Así tanto el ojo como el lector de pantalla saben dónde estás.

Navegar desde el código. A veces la página debe cambiar después de una acción, no después de un clic en un enlace: «Añadir al carrito» añade el producto y te lleva al carrito.

import { useRouter } from 'vue-router';
const router = useRouter();
function buy() {
add(product.value);
router.push('/cart');
}

useRouter() da el router y se llama en <script setup>, no en el manejador. router.push cambia la dirección como un clic en un enlace. useRoute() (sin r) da la dirección actual y sus parámetros.

Usas un enlace cuando una persona elige adónde ir. Navegas desde el código cuando la página cambia por el resultado de una acción.

Pruébalo

Completa

¿De dónde saca la página del producto el id de la dirección?

<script setup>
const route = ();
const product = computed(() =>
  PRODUCTS.find((p) => p.id === Number(route.params.id)),
);
</script>

La idea

Esa página no existe

Si la dirección no coincide con ninguna ruta, el router no dibuja nada. La persona ve un espacio vacío debajo del menú y no sabe qué pasó. Por eso la última ruta recoge todo lo demás y dibuja «Esa página no existe» con un enlace al catálogo.

{ path: '/:pathMatch(.*)*', component: NotFound },

pathMatch es el nombre de un parámetro, y (.*) significa «cualquier texto, barras incluidas». Vue Router ordena las rutas según lo exactas que son, así que esta gana solo cuando no coincide ninguna otra. El antiguo path: '*' de Vue Router 3 ya no funciona: el router lanza un error.

Una página para un producto que no existe (/product/99) es otro caso: la ruta coincide, pero falta el producto. Eso lo decide el componente, así que ProductPage dice «No existe ese producto».

Más precisamente, para curiosos

Rutas anidadas. Las páginas suelen compartir un marco: la cuenta con un menú a la izquierda y las subpáginas «Pedidos» y «Direcciones». La ruta tiene children, y el padre tiene su propio <RouterView /> para el hijo.

Una query en la dirección. La búsqueda y los filtros también son estado que conviene poner en la dirección: /?q=leche&category=2. Se lee de route.query y se escribe con router.push({ query: { q: 'leche' } }).

El mismo componente, un parámetro nuevo. De /product/5 a /product/7 Vue no crea una ProductPage nueva, cambia route.params. Por eso lo que se calcula a partir del parámetro va en computed, y una petición a la API cuando cambia el parámetro va en watch(() => route.params.id, …).

Rutas con nombre. { path: '/product/:id', name: 'product', component: ProductPage } permite router.push({ name: 'product', params: { id: 5 } }): la dirección se construye desde la tabla, no a mano.

Guards. La página «Finalizar compra» puede exigir iniciar sesión. El router puede comprobarlo antes del cambio y redirigir al inicio de sesión: con router.beforeEach.

Ejercicio

El carrito y el 404

El menú tiene «Ofertas» (/sale), pero esa página aún no existe y el espacio debajo del menú queda vacío. Y «Añadir al carrito» añade el producto, pero la persona se queda en la página del producto. Añade una ruta para todo lo demás que lleve a NotFound (el componente está listo) y haz que el botón vaya a /cart después de añadir. La comprobación abre «Ofertas», vuelve con «Volver al catálogo», compra pan y queso y mira el carrito, la dirección y el total.

Vue

Test

Ponte a prueba

1. La tabla tiene «/», «/product/:id» y una ruta para todo lo demás. La dirección es /product/12. ¿Qué dibuja el router?
2. ¿Por qué el menú de la tienda usa Link / RouterLink y no un simple <a href="/cart">?
3. ProductPage.vue tiene const product = PRODUCTS.find((p) => p.id === Number(route.params.id)); sin computed. Desde «Leche fresca» pulsas «Queso blanco» en «Ver también». ¿Qué pasa?
4. En la tienda real abres /product/5 y pulsas «Recargar». El servidor responde 404. ¿Qué falta?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·