Zum Inhalt
wannacode

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.

Vue

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:

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');

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
  1. Der Router hat eine Tabelle: ein Adressmuster und die Komponente dafür. Die Adresse ist jetzt /, der Katalog.
  2. Ein Klick auf „Frischmilch“. Der Router verhindert das Neuladen der Seite und ändert nur die Adresse zu /product/5.
  3. 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.
  4. 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.
  5. ProductPage findet Produkt 5 und zeichnet seine Seite. Dieselbe Komponente zeichnet jedes Produkt: Nur der Parameter ist anders.
  6. Jetzt ist die Adresse /sale, und diese Seite gibt es noch nicht. Keine Zeile passt außer der letzten, also zeichnet der Router NotFound.
  7. 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:

Vue

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.

Vue

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.

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>

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.

Vue

Test

Teste dich

1. Die Tabelle hat „/“, „/product/:id“ und eine Route für alles andere. Die Adresse ist /product/12. Was zeichnet der Router?
2. Warum benutzt das Menü des Shops Link / RouterLink und kein einfaches <a href="/cart">?
3. ProductPage.vue hat const product = PRODUCTS.find((p) => p.id === Number(route.params.id)); ohne computed. Von „Frischmilch“ klickst du unter „Siehe auch“ auf „Weißkäse“. Was passiert?
4. Im echten Shop öffnest du /product/5 und klickst auf „Neu laden“. Der Server antwortet 404. Was fehlt?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·