Der Spickzettel für alles, was aus einzelnen Komponenten eine Anwendung macht: Komponenten verbinden, navigieren, Daten holen, Zustand teilen, ausliefern. Die Komponente selbst — Template, Reaktivität, Formulare, Styling — steht auf dem ersten Blatt: Vue 3 Kern.
Maßgeblich sind die offiziellen Dokumentationen zu Vue, Vue Router und Pinia; dieses Blatt ist eine Auswahl für den Alltag.
Datenfluss: welcher Weg wofür
| Weg | Richtung | Passend für |
|---|---|---|
| Props | Eltern zu Kind | Daten und Konfiguration |
| Ereignisse | Kind zu Eltern | Absichten melden |
provide / inject | über Ebenen | Kontext für einen Teilbaum |
| Slots | Inhalt hinein | Struktur von außen |
| Store | überall | echter Anwendungszustand |
Der schnellste Weg ist selten der richtige: provide löst kein Prop-Problem,
und ein Store für Zustand, den nur zwei Komponenten betreffen, ist zu viel.
Ereignisse steigen nicht auf — nur die direkte Elternkomponente hört zu.
Modul: Komponentenkommunikation
Props deklarieren und prüfen
defineProps({
name: { type: String, required: true },
ausleihen: { type: Number, default: 0 },
zustand: { type: String, validator: (w) => ['frei', 'weg'].includes(w) },
daten: { type: Object, default: () => ({}) } // Objekt-Default als Funktion!
})
| Option | Bedeutung |
|---|---|
type | erwarteter Typ; mehrere als Array [String, Number] |
required | Pflichtangabe |
default | Vorgabewert — bei Objekt/Array Funktion |
validator | eigene Regel, gibt true zurück |
| Stelle | Schreibweise |
|---|---|
| Deklaration | camelCase (Pflicht) |
| Übergabe im Template | kebab-case üblich |
| Nutzung im Kind-Template | camelCase |
Im Produktionsbau prüft Vue nicht mehr — Validierung ist Entwicklungshilfe, kein Schutz zur Laufzeit. Ohne Doppelpunkt ist der übergebene Wert eine Zeichenkette, auch bei einer Zahl. Ein nicht deklariertes Attribut landet als Fallthrough am Wurzelelement.
Modul: Komponentenkommunikation
Ereignisse
<script setup>
const emit = defineEmits(['erhoehen'])
</script>
<template><button @click="emit('erhoehen', 1)">Mehr</button></template>
<!-- Verwendung -->
<MengenFeld @erhoehen="(n) => menge += n" />
Form von defineEmits | Nutzen |
|---|---|
| Array von Namen | knapp, keine Prüfung |
| Objekt mit Funktionen | prüft die Nutzdaten, warnt bei false |
| TypeScript-Signaturen | Typen für Namen und Argumente |
Ereignisnamen in camelCase melden, im Template kebab-case hören — Vue rechnet um. Undeklarierte Ereignisse funktionieren, landen aber zusätzlich in den Fallthrough-Attributen.
Modul: Komponentenkommunikation
Lifecycle-Reihenfolge im Baum
| Phase | Reihenfolge | Folge |
|---|---|---|
setup | außen nach innen | Eltern kennen Kinder noch nicht |
onMounted | innen nach außen | Kinder stehen zuerst |
| Aktualisierung | außen nach innen | Props erreichen Kinder danach |
onUnmounted | innen nach außen | Kinder räumen zuerst auf |
Ein Template-Ref ist erst in onMounted belegt; auf ein Kind in setup
zuzugreifen schlägt fehl. Wer in onMounted lädt, muss damit rechnen, dass
Kinder zuerst eine leere Liste sehen — sie müssen beides vertragen.
Modul: Komponentenkommunikation
provide und inject
// Anbieter: readonly nach außen, die ändernde Funktion mitgeben
provide('thema', { thema: readonly(thema), wechsle })
// Nachfahre
const { thema, wechsle } = inject('thema')
| Schreibweise | Bedeutung |
|---|---|
provide('name', wert) | Zeichenkette als Schlüssel |
provide(schluessel, wert) | Symbol aus einer Datei — kollisionssicher |
inject('name', 'vorgabe') | Vorgabewert bei Fehlen |
inject(k, () => neu(), true) | Vorgabe aus einer Fabrik |
app.provide(…) | app-weit, in main.js |
Fehlt der Anbieter, gibt es keine Warnung — nur undefined, und der Fehler
zeigt sich weit entfernt. Angemessen für Kontext (Thema, Sprache, Konfiguration
eines Teilbaums); für Anwendungszustand ist ein Store der bessere Weg.
Modul: Komponentenkommunikation
Slots
<!-- Ersatzinhalt -->
<button class="knopf"><slot>Absenden</slot></button>
<!-- Scoped Slot: Daten heraus, Darstellung bleibt außen -->
<li v-for="a in artikel" :key="a.id"><slot :artikel="a" :frei="a.verfuegbar" /></li>
<ArtikelListe v-slot="{ artikel }">{{ artikel.name }}</ArtikelListe>
<!-- Benannte Slots, mit Prüfung gegen leere Rahmen -->
<header v-if="$slots.kopf"><slot name="kopf" /></header>
| Schreibweise | Bedeutung |
|---|---|
v-slot:kopf | Langform |
#kopf | Kurzform (braucht immer ein Argument) |
#default | Standard-Slot ausdrücklich |
#kopf="{ wert }" | benannt mit Slot-Props |
#[name] | dynamischer Slotname |
Slot-Inhalt gehört zur Elternkomponente und trägt deren scoped-Kennung.
Ein Tippfehler im Slotnamen bleibt stumm — der Inhalt erscheint einfach nicht.
Modul: Komponentenkommunikation
Vue Router
const routes = [
{ path: '/', component: ArtikelListe },
{ path: '/artikel/:id', component: Detail, name: 'artikel' },
{ path: '/:pathMatch(.*)*', component: Fehlt } // Auffangroute zuletzt!
]
export default createRouter({ history: createWebHistory(), routes })
// main.js
createApp(App).use(router).mount('#app')
Die Liste wird der Reihe nach geprüft — eine Auffangroute an falscher Stelle
verschluckt alles Folgende. Ohne <RouterView /> bleibt die Seite leer, ohne
dass ein Fehler erscheint.
useRouter | useRoute | |
|---|---|---|
| Liefert | die Router-Instanz | die aktuelle Route |
| Zweck | navigieren | Parameter lesen |
| Typisch | push, replace | params, query |
| Reaktiv | nein | ja |
params | query | |
|---|---|---|
| In der Adresse | /artikel/7 | ?sortierung=name |
| Deklaration nötig | ja, im Pfad | nein |
| Typ | immer Zeichenkette | immer Zeichenkette |
| Passend für | Identität der Ressource | Filter und Sortierung |
| Klasse | Wann sie gesetzt wird |
|---|---|
router-link-active | Route passt, auch als übergeordnete |
router-link-exact-active | nur bei exakter Übereinstimmung |
Ein Link auf / trägt die aktive Klasse sonst auf fast jeder Seite. push legt
einen Historieneintrag an, replace ersetzt den aktuellen.
Parameterwechsel erzeugt nichts neu: Bei gleicher Zielkomponente verwendet
der Router die Instanz weiter, die Lifecycle-Hooks laufen kein zweites Mal.
Darauf reagiert man mit onBeforeRouteUpdate oder einem Watcher — und lädt beim
ersten Aufruf zusätzlich direkt, denn der Wächter läuft dort nicht.
| Modus | Schreibweise | Wofür |
|---|---|---|
| boolesch | props: true | params direkt als Props (nicht query) |
| Objekt | props: { a: 1 } | feste Werte |
| Funktion | props: (r) => … | umwandeln und kombinieren |
Die Funktionsform ist der Ort, an dem aus der Zeichenkette eine Zahl wird — nicht in der Komponente.
| Strategie | Adresse | Server nötig |
|---|---|---|
createWebHistory | /artikel/7 | ja, Auffangregel |
createWebHashHistory | /#/artikel/7 | nein, aber SEO-Notlösung |
createMemoryHistory | keine | nein, für Tests und SSR |
Modul: Routing und Navigation
HTTP: fetch, Axios, Fehlerzustände
Jeder Abruf hat drei Zustände — wer nur den Erfolgsfall baut, hat zwei
Drittel vergessen: laedt, daten, fehler.
const antwort = await fetch('/api/artikel')
if (!antwort.ok) throw new Error(`HTTP ${antwort.status}`) // 500 lässt fetch NICHT scheitern
return antwort.json() // selbst asynchron
fetch | Axios | |
|---|---|---|
| Installation | keine | ein Paket |
| JSON | selbst umwandeln | automatisch |
| Fehlerstatus | selbst prüfen | wirft von allein |
| Grundadresse | selbst bauen | in der Instanz |
| Abfangfunktionen | selbst schreiben | eingebaut |
Bei Axios stehen die Nutzdaten unter data, nicht direkt im Ergebnis.
| Fehlerart | Ursache | Reaktion |
|---|---|---|
| Netzwerk | offline, Zeitüberschreitung | Wiederholung anbieten |
| 400er | Anfrage falsch | Eingaben zeigen |
| 401 / 403 | nicht angemeldet | zur Anmeldung |
| 500er | Serverfehler | melden, später erneut |
| Schicht | Datei | Kennt |
|---|---|---|
| Umgebung | .env | nichts |
| Client | api/client.js | die Grundadresse |
| Ressource | api/artikel.js | den Client |
| Zustand | composables/useAbruf.js | die Ressource |
| Anzeige | views/ArtikelListe.vue | das Composable |
Kein Sprung über eine Schicht hinweg, sonst hilft die Aufteilung nicht mehr.
| Werkzeug | Zweck |
|---|---|
watch | auf Änderung reagieren |
watchEffect | Abhängigkeiten selbst erkennen |
onWatcherCleanup | vor dem nächsten Lauf aufräumen (ab Vue 3.5) |
AbortController | laufende Anfrage abbrechen |
| Entprellung | Anfragen bündeln statt jede zu senden |
Ohne Abbruch schreibt die langsamere Antwort das falsche Ergebnis — ein Wettlauf,
der lokal fast nie auftritt und wie ein Anzeigefehler aussieht. Reaktive
Argumente eines Composables über toValue normalisieren, sonst friert es auf
dem ersten Wert ein.
Modul: HTTP und API-Integration
Pinia
| Art von Zustand | Ort |
|---|---|
| Formulareingabe, auf-/zugeklappt | Komponente |
| Thema, Sprache eines Teilbaums | provide / inject |
| Angemeldeter Nutzer | Store |
| Geladene Artikel, Warenkorb | Store |
Im Zweifel unten anfangen — nach oben wandern ist leicht, zurück selten.
// Setup-Store: wie eine Komponente, Privates möglich
export const useAusleihe = defineStore('ausleihe', () => {
const offen = ref([])
const anzahl = computed(() => offen.value.length)
function hinzufuegen(a) { offen.value.push(a) }
return { offen, anzahl, hinzufuegen } // nur Zurückgegebenes ist sichtbar
})
| Options-Store | Setup-Store | |
|---|---|---|
| Aufbau | vertraut aus Vuex | wie eine Komponente |
| Privates | nicht möglich | ja |
| Composables nutzen | umständlich | direkt |
| Watcher im Store | nein | ja |
$reset() | vorhanden | selbst schreiben |
Beim Options-Store ist state eine Funktion, kein Objekt — sonst teilen sich
Instanzen den Zustand. Gegenüber Vuex entfallen die Mutationen; Actions ändern
den Zustand direkt.
// falsch: verliert die Reaktivität
const { offen, anzahl } = useAusleihe()
// richtig
const store = useAusleihe()
const { offen, anzahl } = storeToRefs(store) // Zustand und Getter
const { hinzufuegen } = store // Actions direkt, sind an nichts gebunden
| Weg der Änderung | Wofür |
|---|---|
store.feld = wert | einzelne Zuweisung |
store.$patch({ … }) | mehrere Felder in einem Schritt |
store.$patch((s) => …) | Änderungen an Listen |
store.$reset() | zurück zum Anfangszustand (nur Options-Store) |
| Action | fachlich benannter Vorgang |
Einen anderen Store innerhalb der Action aufrufen, nicht im Modulkopf — dort läuft er vor der Anmeldung von Pinia. Getter mit Argument geben eine Funktion zurück und verlieren dabei das Gedächtnis; Asynchrones gehört in eine Action.
Mehrere kleine Stores schlagen einen großen: nutzer, artikel, ausleihe,
meldungen. Ein Store hält seinen Zustand bis zum Neuladen der Seite — auch nach
dem Abmelden.
Modul: State Management mit Pinia
Eigene Direktiven
Erst die Prüfreihenfolge, dann die Direktive:
| Anliegen | Werkzeug |
|---|---|
| Markup wiederverwenden | Komponente |
| Zustandslogik teilen | Composable |
| Elementverhalten teilen | Direktive |
| Nur Aussehen | CSS-Klasse |
Eine Direktive, die Daten liefert, ist in Wahrheit ein Composable; eine, die
Markup erzeugt, eine Komponente. Sinnvolle Fälle brauchen das Element selbst:
v-fokus, v-klick-aussen, v-sichtbar, v-tooltip.
const vFokus = { mounted: (el) => el.focus() } // Variable vFokus → v-fokus im Template
const vFarbe = (el, binding) => { el.style.color = binding.value } // Kurzform = mounted + updated
app.directive('fokus', vFokus) // global: ohne v im Namen, vor mount
binding-Feld | Inhalt |
|---|---|
value | der übergebene Wert |
oldValue | vorheriger Wert, nur in beforeUpdate / updated |
arg | das Argument nach dem Doppelpunkt |
modifiers | Objekt der gesetzten Modifikatoren |
instance | die Komponente, in der die Direktive steht |
Hooks: created, beforeMount, mounted, beforeUpdate, updated,
beforeUnmount, unmounted — alle optional. Für Listener am document ist
beforeUnmount der richtige Ort, und der Handler muss benannt am Element liegen
(el._h), sonst lässt er sich nicht entfernen. Auf Komponenten wirken Direktiven
nur am Wurzelelement; bei mehreren Wurzeln werden sie ignoriert.
Modul: Eigene Direktiven
Build, Umgebung, Deployment
npm run build # nach dist/
npm run preview # dist lokal ausliefern, üblich Port 4173 — Prüfhilfe, kein Server
| Entwicklung | Produktion | |
|---|---|---|
| Prop-Validierung | prüft und warnt | entfällt |
| Warnungen | ausführlich | keine |
| Devtools | verbunden | abgeschaltet |
| Fehlermeldungen | Klartext | Fehlernummer |
.env-Datei | Wann geladen |
|---|---|
.env | immer |
.env.local | immer, nicht ins Repository |
.env.[modus] | nur im genannten Modus (hat Vorrang) |
.env.[modus].local | nur im Modus, nicht ins Repository |
Nur Variablen mit VITE_-Präfix erreichen den Anwendungscode — und das ist eine
Sicherheitsentscheidung: Alles im Frontend ist für jeden Besucher lesbar.
Echte Geheimnisse gehören auf den Server. Werte sind immer Zeichenketten, auch
true und Zahlen; Änderungen wirken erst nach einem Neustart des Dev-Servers.
vite.config.js | Wofür |
|---|---|
base | Betrieb in einem Unterverzeichnis (mit Schrägstrich am Ende) |
resolve.alias | kurze Importpfade wie @/components |
build.outDir | anderer Zielordner als dist |
server.proxy | Herkunftsprobleme beim Entwickeln umgehen (nur Dev) |
build.sourcemap | Fehlersuche im Bau — macht den Originalcode lesbar |
location / { try_files $uri $uri/ /index.html; }
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.html [L]
Nach dieser Regel meldet der Server nie mehr 404 — die Auffangroute in der Anwendung wird damit Pflicht. Der Fehler zeigt sich nie im Dev-Server, immer erst nach dem Deployment.
| Datei | Caching |
|---|---|
assets mit Hash im Namen | lange gültig, unveränderlich |
index.html | nie zwischenspeichern |
Dateien aus public | kurz, je nach Zweck |
Ein zwischengespeichertes index.html verweist auf Bündel, die es nicht mehr
gibt — die Seite bleibt weiß, ohne Serverfehler.
Prüfliste vor dem Ausliefern: Auffangregel, 404-Route,
app.config.errorHandler (vor mount), Bündelgröße angesehen, keine
Geheimnisse mit VITE_, index.html ohne Cache.
Modul: Deployment und Produktion
Typische Fallen
- Zerlegter Store ohne
storeToRefsaktualisiert sich nie wieder — der häufigste Pinia-Fehler. props: truereicht nurparamsdurch, nichtquery; die Prop muss genauso heißen wie der Parameter im Pfad.- Routen-Parameter sind Zeichenketten — ein Vergleich mit einer Zahl schlägt fehl.
fetchscheitert bei 500 nicht.okimmer selbst prüfen;json()wirft bei leerem Rumpf, und eine Antwort lässt sich nur einmal auslesen.catch, das nur auf die Konsole schreibt, ist keine Fehlerbehandlung. Ein abgebrochener Abruf löst ebenfalls eincatchaus und ist kein Fehler.- Ohne
finallybleibt die Anzeige nach einem Fehler dauerhaft im Ladezustand. - Objekt-Defaults als Literal teilen sich alle Instanzen — in eine Funktion
packen. Gilt für Props wie für
state. - Fehlendes
provideliefert stummundefined, keine Warnung. - Nach
app.mountregistriert wirkt nichts mehr: weder Komponenten noch Direktiven noch Plugins. - Der Dev-Proxy täuscht über fehlende Serverkonfiguration hinweg.
baseohne abschließenden Schrägstrich führt zu falschen Pfaden;baseund Router müssen zusammenpassen.- Ein API-Schlüssel mit
VITE_-Präfix steht im ausgelieferten JavaScript.
Module: alle sechs des Anwendungs-Teils