Routing
Kurz gesagt: Routing verbindet die Adresse mit der Seite: Vue Router zeigt die Komponente der Route, Links laden nicht neu, und Parameter wie /product/:id kommen aus der Adresse.
Problem
Ein Link, der ins Leere führt
Der Shop hat einen Katalog und eine Seite für jedes Produkt. Öffne „Frischmilch“ und geh zurück.
Konsole
Es sieht gut aus. Aber welche Seite du siehst, steckt nur im Zustand der Komponente (productId). Die Adresse im Browser ist für den Katalog und für jedes Produkt dieselbe. Deshalb:
- du kannst einem Freund keinen Link zur Milch schicken: Er bekommt den Katalog;
- „Neu laden“ bringt dich zurück zum Katalog, weil der Zustand verloren ist;
- „Zurück“ im Browser führt nicht zum Katalog, sondern aus dem Shop heraus;
- die Produkte sind Buttons, keine Links: „In neuem Tab öffnen“ funktioniert nicht.
(Die Sandbox hat keine Adressleiste, deshalb zeigt sich das in ihr nicht. Auf einer echten Seite siehst du es sofort.)
Die Idee
Die Adresse ist der Zustand
Jede Seite braucht ihre eigene Adresse: / für den Katalog, /product/5 für die Milch, /cart für den Warenkorb. Dann sagt die Adresse, welche Seite gezeigt wird, nicht der Zustand im Speicher.
Die App bleibt eine Seite (eine Single-Page-Anwendung): Der Browser lädt das HTML einmal. Bei einem Klick auf einen Link hält die Bibliothek das Neuladen an, ändert die Adresse über die History-API des Browsers und zeichnet eine andere Komponente. Das ist die Aufgabe des Routers. Er hat eine Routentabelle: ein Adressmuster und eine Komponente dafür.
Vue Router bekommt die Tabelle als Array. Der Router wird in einer eigenen Datei angelegt und mit use zur App hinzugefügt:
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.jscreateApp(App).use(router).mount('#app');In App.vue ist der Platz für die Seite <RouterView />. Dort zeichnet der Router die Komponente, die zur Adresse passt. Alles drumherum (das Menü oben) erscheint auf jeder Seite.
:id in /product/:id ist ein Parameter: Er steht für ein Segment der Adresse. Eine Route deckt alle Produkte ab.
Sieh es dir an
Sieh, wie die Adresse die Seite wählt
<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>Diese Seite gibt es nicht</h1>Schritt für Schritt, in Worten
- Der Router hat eine Tabelle: ein Adressmuster und die Komponente dafür. Die Adresse ist jetzt /, der Katalog.
- Ein Klick auf „Frischmilch“. Der Router verhindert das Neuladen der Seite und ändert nur die Adresse zu /product/5.
- Er vergleicht die Adresse mit jeder Zeile. „/product/:id“ passt: :id steht für ein beliebiges Segment. Die Zeile für alles andere passt auch, ist aber allgemeiner und verliert.
- Aus dem Treffer kommt der Parameter: id = "5". Er ist Text, weil die Adresse Text ist. ProductPage liest route.params.id, und Number(…) macht daraus 5.
- ProductPage findet Produkt 5 und zeichnet seine Seite. Dieselbe Komponente zeichnet jedes Produkt: Nur der Parameter ist anders.
- Jetzt ist die Adresse /sale, und diese Seite gibt es noch nicht. Keine Zeile passt außer der letzten, also zeichnet der Router NotFound.
- Die Adresse ist der Zustand der Seite. Du kannst sie teilen, neu laden und zu ihr zurückkehren, und die Tabelle liefert immer dieselbe Seite.
Der Router weiß nichts über Produkte. Er vergleicht den Text der Adresse mit den Mustern und reicht den Parameter an die Komponente weiter. Was 5 bedeutet, entscheidet die Komponente.
Die Idee
In der Sandbox und in einem echten Projekt
Der Router einer echten Seite liest und schreibt die Adresse in der Adressleiste des Browsers. Das ist createWebHistory(). Die Sandbox in den Lektionen ist eine Seite in der Lektionsseite und hat keine eigene Adresse. Deshalb gibt es hier einen klaren Fehler:
Konsole
In der Sandbox nehmen wir createMemoryHistory(): einen Router, der die Adresse im Speicher hält. Alles andere ist gleich: die Tabelle, die Links, die Parameter. Die Adresse zeigen wir selbst, unten auf der Seite.
In einem echten Projekt ändert sich nur die History:
import { createRouter, createWebHistory } from 'vue-router';
export const router = createRouter({ history: createWebHistory(), routes,});Und noch etwas, das die Sandbox nicht zeigt: der Server. Wenn jemand /product/5 direkt öffnet oder auf „Neu laden“ klickt, fragt der Browser den Server nach dieser Adresse. Eine solche Datei gibt es nicht. Der Server muss für jede Adresse, die keine Datei ist, index.html liefern, und dann wählt der Router die Seite. Der Vite-Entwicklungsserver macht das von selbst. Bei nginx erledigt es die Zeile try_files:
location / { try_files $uri /index.html;}Probier es aus
Eine Seite für jedes Produkt
Die Tabelle hat den Katalog und den Warenkorb, und die Produkte führen zu /product/1, /product/5 und /product/7, aber eine solche Route gibt es nicht. Füge für die Produktseite eine Route mit Parameter hinzu. ProductPage zeigt jetzt immer das Brot: Lass sie die id aus der Adresse nehmen. Die Prüfung öffnet die Milch, dann den Käse unter „Siehe auch“, dann das Brot aus dem Katalog und sieht sich die Überschrift und die Adresse an.
Konsole
Lösung
main.jsimport { createApp } from 'vue';
import App from './App.vue';
import { router } from './router.js';
createApp(App).use(router).mount('#app');
router.jsimport { createRouter, createMemoryHistory } from 'vue-router';
import Catalog from './Catalog.vue';
import ProductPage from './ProductPage.vue';
import CartPage from './CartPage.vue';
export const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: Catalog },
{ path: '/product/:id', component: ProductPage },
{ path: '/cart', component: CartPage },
],
});
App.vue<script setup>
import Header from './Header.vue';
import Address from './Address.vue';
</script>
<template>
<Header />
<main>
<RouterView />
</main>
<Address />
</template>
cart.jsimport { ref } from 'vue';
// Der Warenkorb: ein ref für die ganze App. Jede Komponente, die es importiert, liest dasselbe.
export const items = ref([]);
export function add(product) {
items.value.push(product);
}
Header.vue<script setup>
import { RouterLink } from 'vue-router';
import { items } from './cart.js';
</script>
<template>
<nav>
<RouterLink to="/">Katalog</RouterLink>
<RouterLink to="/cart">Warenkorb ({{ items.length }})</RouterLink>
</nav>
</template>
Catalog.vue<script setup>
import { RouterLink } from 'vue-router';
import { PRODUCTS, money } from './products.js';
</script>
<template>
<h1>Katalog</h1>
<ul>
<li v-for="p in PRODUCTS" :key="p.id">
<RouterLink :to="`/product/${p.id}`">{{ p.name }}</RouterLink>, {{ money(p.price) }}
</li>
</ul>
</template>
ProductPage.vue<script setup>
import { computed } from 'vue';
import { RouterLink, useRoute } from 'vue-router';
import { PRODUCTS, money } from './products.js';
import { add } from './cart.js';
const route = useRoute();
const product = computed(() => PRODUCTS.find((p) => p.id === Number(route.params.id)));
const others = computed(() => PRODUCTS.filter((p) => p.id !== product.value?.id));
function buy() {
add(product.value);
}
</script>
<template>
<section v-if="product">
<h1>{{ product.name }}</h1>
<p>{{ money(product.price) }}</p>
<button type="button" @click="buy">In den Warenkorb</button>
<h2>Siehe auch</h2>
<ul>
<li v-for="p in others" :key="p.id">
<RouterLink :to="`/product/${p.id}`">{{ p.name }}</RouterLink>
</li>
</ul>
</section>
<p v-else>Kein solches Produkt</p>
</template>
CartPage.vue<script setup>
import { computed } from 'vue';
import { money } from './products.js';
import { items } from './cart.js';
const total = computed(() => items.value.reduce((sum, p) => sum + p.price, 0));
</script>
<template>
<h1>Warenkorb</h1>
<p v-if="items.length === 0">Der Warenkorb ist leer</p>
<ul>
<li v-for="(p, i) in items" :key="i">{{ p.name }}, {{ money(p.price) }}</li>
</ul>
<p>Gesamt: {{ money(total) }}</p>
</template>
NotFound.vue<script setup>
import { RouterLink } from 'vue-router';
</script>
<template>
<h1>Diese Seite gibt es nicht</h1>
<p>
<RouterLink to="/">Zurück zum Katalog</RouterLink>
</p>
</template>
Address.vue<script setup>
import { useRoute } from 'vue-router';
// Die Sandbox hat keine Adressleiste: Wir zeigen die Adresse hier.
const route = useRoute();
</script>
<template>
<footer>Adresse: {{ route.path }}</footer>
</template>
products.jsexport const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
];
export const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
nav { display: flex; gap: 16px; background: #f3efe6; padding: 8px 16px; }
nav a { color: #1d4f8a; }
nav a[aria-current='page'] { font-weight: 700; text-decoration: none; }
main { padding: 0 16px; }
footer { border-top: 1px solid #ddd; padding: 4px 16px; color: #555; font-family: ui-monospace, monospace; }
button { font: inherit; }
In router.js ist die Route { path: '/product/:id', component: ProductPage }. In ProductPage.vue steht der Parameter in useRoute().params.id. Das Produkt ist computed: Von „Frischmilch“ zu „Weißkäse“ behält Vue dieselbe Komponente und ändert nur route.params. Eine einfache Konstante in <script setup> wird einmal berechnet, und die Überschrift bleibt die alte. Die Prüfung erkennt das.
Der Parameter ist Text: "5", während id in den Daten eine Zahl ist. p.id === "5" ist für jedes Produkt false. Vergleiche mit Number(id).
Probier es aus
Finde den Fehler
Jede Produktseite sagt „Kein solches Produkt“, und die Produkte sind in den Daten. Welche Zeile ist schuld?
Wähle die Zeile mit dem Fehler.
route.params.id ist Text ("5"), und p.id ist eine Zahl. Das strikte === ist nie wahr. Es muss p.id === Number(route.params.id) heißen.Die Idee
Links und Navigation aus dem Code
Links. Eine Seite der App erreichst du mit <RouterLink to="/cart">. In der Seite ist es ein normales <a href="/cart">: „In neuem Tab öffnen“ funktioniert, und ein Screenreader liest es als Link. Bei einem normalen Klick hält der Router das Neuladen an (preventDefault) und ändert nur die Adresse. Schreibst du <a href> selbst, lädt der Browser die ganze Seite noch einmal, und der Warenkorb im Speicher ist weg.
Für das Menü gibt es RouterLink mit der Klasse, die der Router vergibt: Der Link zur aktuellen Seite bekommt aria-current="page" und kann einen eigenen Stil haben. So wissen das Auge und der Screenreader, wo du bist.
Navigation aus dem Code. Manchmal muss die Seite nach einer Aktion wechseln und nicht nach einem Klick auf einen Link: „In den Warenkorb“ legt das Produkt hinein und bringt dich zum Warenkorb.
import { useRouter } from 'vue-router';
const router = useRouter();
function buy() { add(product.value); router.push('/cart');}useRouter() liefert den Router und wird in <script setup> aufgerufen, nicht im Handler. router.push ändert die Adresse wie ein Klick auf einen Link. useRoute() (ohne r) liefert die aktuelle Adresse und ihre Parameter.
Einen Link benutzt du, wenn ein Mensch wählt, wohin er geht. Aus dem Code navigierst du, wenn die Seite wegen des Ergebnisses einer Aktion wechselt.
Probier es aus
Ergänze
Woher bekommt die Produktseite die id aus der Adresse?
<script setup>
const route = ();
const product = computed(() =>
PRODUCTS.find((p) => p.id === Number(route.params.id)),
);
</script>useRoute: liefert die aktuelle Route. Die Parameter stehen in route.params, für /product/:id ist das { id: "5" }.Die Idee
Diese Seite gibt es nicht
Wenn die Adresse zu keiner Route passt, zeichnet der Router nichts. Der Mensch sieht eine leere Fläche unter dem Menü und weiß nicht, was passiert ist. Deshalb fängt die letzte Route alles andere ab und zeichnet „Diese Seite gibt es nicht“ mit einem Link zum Katalog.
{ path: '/:pathMatch(.*)*', component: NotFound },pathMatch ist der Name eines Parameters, und (.*) heißt „beliebiger Text, Schrägstriche eingeschlossen“. Vue Router ordnet die Routen danach, wie genau sie sind, deshalb gewinnt diese nur, wenn keine andere passt. Das alte path: '*' aus Vue Router 3 funktioniert nicht mehr: Der Router wirft einen Fehler.
Eine Seite für ein Produkt, das es nicht gibt (/product/99), ist ein anderer Fall: Die Route passt, aber das Produkt fehlt. Das entscheidet die Komponente, also sagt ProductPage „Kein solches Produkt“.
Genauer, für Neugierige
Verschachtelte Routen. Seiten teilen oft einen Rahmen: das Konto mit einem Menü links und den Unterseiten „Bestellungen“ und „Adressen“. Die Route hat children, und das Elternelement hat sein eigenes <RouterView /> für das Kind.
Eine Query in der Adresse. Suche und Filter sind ebenfalls Zustand, den man gut in die Adresse legt: /?q=milch&category=2. Sie wird aus route.query gelesen und mit router.push({ query: { q: 'milch' } }) geschrieben.
Dieselbe Komponente, ein neuer Parameter. Von /product/5 zu /product/7 erzeugt Vue keine neue ProductPage, sondern ändert route.params. Was aus dem Parameter berechnet wird, gehört also in computed, und eine Anfrage an die API bei einer Änderung des Parameters in watch(() => route.params.id, …).
Benannte Routen. { path: '/product/:id', name: 'product', component: ProductPage } erlaubt router.push({ name: 'product', params: { id: 5 } }): Die Adresse wird aus der Tabelle gebaut, nicht von Hand.
Guards. Die Seite „Zur Kasse“ kann eine Anmeldung verlangen. Der Router kann vor dem Wechsel prüfen und zur Anmeldung umleiten: mit router.beforeEach.
Übung
Der Warenkorb und 404
Das Menü hat „Angebote“ (/sale), aber diese Seite gibt es noch nicht, und die Fläche unter dem Menü bleibt leer. Und „In den Warenkorb“ legt das Produkt hinein, aber der Mensch bleibt auf der Produktseite. Füge eine Route für alles andere zu NotFound hinzu (die Komponente ist fertig) und lass den Button nach dem Hinzufügen zu /cart gehen. Die Prüfung öffnet „Angebote“, geht mit „Zurück zum Katalog“ zurück, kauft Brot und Käse und sieht sich den Warenkorb, die Adresse und die Summe an.
Konsole
Lösung
main.jsimport { createApp } from 'vue';
import App from './App.vue';
import { router } from './router.js';
createApp(App).use(router).mount('#app');
router.jsimport { createRouter, createMemoryHistory } from 'vue-router';
import Catalog from './Catalog.vue';
import ProductPage from './ProductPage.vue';
import CartPage from './CartPage.vue';
import NotFound from './NotFound.vue';
export const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: Catalog },
{ path: '/product/:id', component: ProductPage },
{ path: '/cart', component: CartPage },
{ path: '/:pathMatch(.*)*', component: NotFound },
],
});
App.vue<script setup>
import Header from './Header.vue';
import Address from './Address.vue';
</script>
<template>
<Header />
<main>
<RouterView />
</main>
<Address />
</template>
cart.jsimport { ref } from 'vue';
// Der Warenkorb: ein ref für die ganze App. Jede Komponente, die es importiert, liest dasselbe.
export const items = ref([]);
export function add(product) {
items.value.push(product);
}
Header.vue<script setup>
import { RouterLink } from 'vue-router';
import { items } from './cart.js';
</script>
<template>
<nav>
<RouterLink to="/">Katalog</RouterLink>
<RouterLink to="/cart">Warenkorb ({{ items.length }})</RouterLink>
<RouterLink to="/sale">Angebote</RouterLink>
</nav>
</template>
Catalog.vue<script setup>
import { RouterLink } from 'vue-router';
import { PRODUCTS, money } from './products.js';
</script>
<template>
<h1>Katalog</h1>
<ul>
<li v-for="p in PRODUCTS" :key="p.id">
<RouterLink :to="`/product/${p.id}`">{{ p.name }}</RouterLink>, {{ money(p.price) }}
</li>
</ul>
</template>
ProductPage.vue<script setup>
import { computed } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import { PRODUCTS, money } from './products.js';
import { add } from './cart.js';
const route = useRoute();
const router = useRouter();
const product = computed(() => PRODUCTS.find((p) => p.id === Number(route.params.id)));
const others = computed(() => PRODUCTS.filter((p) => p.id !== product.value?.id));
function buy() {
add(product.value);
router.push('/cart');
}
</script>
<template>
<section v-if="product">
<h1>{{ product.name }}</h1>
<p>{{ money(product.price) }}</p>
<button type="button" @click="buy">In den Warenkorb</button>
<h2>Siehe auch</h2>
<ul>
<li v-for="p in others" :key="p.id">
<RouterLink :to="`/product/${p.id}`">{{ p.name }}</RouterLink>
</li>
</ul>
</section>
<p v-else>Kein solches Produkt</p>
</template>
CartPage.vue<script setup>
import { computed } from 'vue';
import { money } from './products.js';
import { items } from './cart.js';
const total = computed(() => items.value.reduce((sum, p) => sum + p.price, 0));
</script>
<template>
<h1>Warenkorb</h1>
<p v-if="items.length === 0">Der Warenkorb ist leer</p>
<ul>
<li v-for="(p, i) in items" :key="i">{{ p.name }}, {{ money(p.price) }}</li>
</ul>
<p>Gesamt: {{ money(total) }}</p>
</template>
NotFound.vue<script setup>
import { RouterLink } from 'vue-router';
</script>
<template>
<h1>Diese Seite gibt es nicht</h1>
<p>
<RouterLink to="/">Zurück zum Katalog</RouterLink>
</p>
</template>
Address.vue<script setup>
import { useRoute } from 'vue-router';
// Die Sandbox hat keine Adressleiste: Wir zeigen die Adresse hier.
const route = useRoute();
</script>
<template>
<footer>Adresse: {{ route.path }}</footer>
</template>
products.jsexport const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
];
export const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
nav { display: flex; gap: 16px; background: #f3efe6; padding: 8px 16px; }
nav a { color: #1d4f8a; }
nav a[aria-current='page'] { font-weight: 700; text-decoration: none; }
main { padding: 0 16px; }
footer { border-top: 1px solid #ddd; padding: 4px 16px; color: #555; font-family: ui-monospace, monospace; }
button { font: inherit; }
Die Route ist { path: '/:pathMatch(.*)*', component: NotFound }. In ProductPage.vue: const router = useRouter(); in <script setup> und router.push('/cart') in buy nach add(product.value). Der Warenkorb liegt in cart.js: ein ref für die ganze App, deshalb sehen die Anzahl im Menü und CartPage ihn sofort.
Die Prüfung sieht sich auch das Menü an: Auf der Warenkorbseite hat der Link „Warenkorb (1)“ aria-current="page". Das macht der Router von selbst.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Zustand · Abgeleiteter Zustand · Gemeinsamer Zustand: nach oben · Gemeinsamer Zustand
- Geht weiter in: Geschwindigkeit: wann eine Komponente neu rendert · Von der Sandbox zum Projekt · Das Schaufenster des Shops
- Aufgaben zum Thema: Eine ganze App