Der Spickzettel zur Komponente selbst: Aufbau, Template, Reaktivität, Formulare, Aussehen. Für alles, was aus Komponenten eine Anwendung macht — Props und Slots, Router, HTTP, Pinia, Deployment — gibt es ein zweites Blatt: Vue 3 Anwendung.
Maßgeblich ist und bleibt die offizielle Vue-Dokumentation; dieses Blatt ist eine Auswahl für den Alltag, keine Referenz.
Projekt anlegen und lesen
npm create vue@latest # ohne @latest zieht npm ggf. eine alte Fassung
cd leihkiste && npm install && npm run dev
Vue 3.5 verlangt Node ^22.18.0 oder >=24.12.0 — eine ältere Version bricht
erst beim Scaffolding ab, nicht bei der Installation. create-vue ist reines
Scaffolding, den Rest übernimmt Vite; die alte Vue CLI (vue create, webpack)
ist im Wartungsmodus.
| Pfad | Inhalt |
|---|---|
src/main.js | Einstiegspunkt, erzeugt und hängt die Anwendung ein |
src/App.vue | Wurzelkomponente |
src/components/ | Wiederverwendbare Komponenten |
src/views/ | Komponenten, die eine Route füllen |
src/router/, src/stores/ | Routenkonfiguration, Pinia-Stores |
src/assets/ | läuft durch den Build, bekommt einen Hash im Namen |
public/ | wird unverändert kopiert, feste URL |
views ist eine Konvention des Gerüsts, kein Vue-Begriff. Alles zwischen
createApp und app.mount() gilt app-weit — was nach mount kommt, greift
nicht mehr.
Modul: Einführung und Projektsetup
Single-File Component und Registrierung
| Block | Enthält | Pflicht |
|---|---|---|
script setup | Zustand und Funktionen | nein |
template | Struktur der Ausgabe | praktisch ja |
style | Aussehen, gern scoped | nein |
<script setup>
import { ref } from 'vue'
import ArtikelKarte from './components/ArtikelKarte.vue' // Import genügt
const anzahl = ref(0)
</script>
<template>
<ArtikelKarte />
<button @click="anzahl++">Anzahl: {{ anzahl }}</button>
</template>
Ohne script setup ist keine Variable im Template sichtbar, auch keine
importierte. Die ausgeschriebene Form macht sichtbar, warum: nur was setup()
zurückgibt, landet im Template.
| Lokal | Global (app.component) | |
|---|---|---|
| Sichtbarkeit | nur in dieser Datei | überall |
| Tree Shaking | greift | greift nicht |
| Herkunft im Code | steht im Import | muss gesucht werden |
| Vererbung an Kinder | nein | entfällt |
Lokale Registrierung vererbt sich nicht — Kindkomponenten brauchen ihren eigenen Import. Global bleibt die Ausnahme für die wenigen Bausteine, die wirklich überall vorkommen.
| Konvention | Begründung |
|---|---|
| Dateinamen in PascalCase | Datei und Tagname stimmen überein |
| Mehrwortige Komponentennamen | keine Kollision mit HTML-Elementen |
| Basis-Präfix für Bausteine | BasisKnopf ist erkennbar generisch |
views/ für Routenziele | trennt Seite vom Baustein |
| Ein Ordner je Fachbereich | Zusammengehöriges liegt beieinander |
Konventionen wirken nur, wenn sie ausnahmslos gelten — sonst sind sie Rauschen.
Modul: Komponentenbasierte Entwicklung
Template-Syntax
In einer Bindung steht genau ein Ausdruck — kein Statement, keine Ablaufsteuerung.
| Erlaubt | Nicht erlaubt |
|---|---|
anzahl + 1 | var a = 1 |
ok ? 'Ja' : 'Nein' | if (ok) { ... } |
name.toUpperCase() | mehrere Anweisungen |
formatiere(datum) | Zugriff auf eigene window-Werte |
| Langform | Kurzform | Für |
|---|---|---|
v-bind:src | :src | Attribute |
v-on:click | @click | Ereignisse |
v-slot:kopf | #kopf | Slots |
v-bind:[a] | :[a] | dynamisches Argument |
| — | :id | Gleichnamen-Kurzform für :id="id", ab 3.4 |
v-if und v-for haben keine Kurzform. v-bind="objekt" setzt mehrere
Attribute auf einmal; bei Kollision gewinnt die spätere Angabe. v-html rendert
rohes HTML — mit Nutzerinhalt ist das eine XSS-Lücke.
Modul: Templates und Datenbindung
Ereignis-Modifikatoren
| Modifikator | Wirkung |
|---|---|
.prevent | preventDefault auf dem Ereignis |
.stop | stopPropagation, kein Weiterreichen |
.once | Handler feuert genau einmal |
.self | nur wenn das Ziel das Element selbst ist |
.enter | nur bei der Taste Enter |
Verkettbar (@click.stop.prevent), und die Reihenfolge zählt: .prevent.self
ist nicht .self.prevent. Ereignisnamen sind kleingeschrieben — @keyUp
funktioniert nicht.
Modul: Templates und Datenbindung
Bedingung und Liste
v-if | v-show | |
|---|---|---|
| Im DOM bei falsch | nein | ja |
| Erstes Rendern | faul, nur bei wahr | immer |
| Umschaltkosten | höher | niedrig |
Auf template | ja | nein |
Mit v-else | ja | nein |
Häufiges Umschalten spricht für v-show, seltenes Ändern für v-if. Unter
v-if läuft der Lebenszyklus der Kinder wirklich ab — innerer Zustand geht beim
Umschalten verloren, und das ist Absicht.
<!-- v-if und v-for nie auf demselben Element: v-if gewinnt und kennt a nicht -->
<template v-for="a in artikel" :key="a.id">
<li v-if="a.verfuegbar">{{ a.name }}</li>
</template>
Ohne key ordnet Vue nach Position und verwechselt Elemente beim Umsortieren;
der Index als key ist ebenfalls nur die Position. Ein key muss primitiv sein.
Modul: Templates und Datenbindung
Reaktivität: ref, reactive, computed
| Werkzeug | Für | Besonderheit |
|---|---|---|
ref | jeden Wert | .value im Skript |
reactive | Objekte | nicht ersetzbar |
computed | Ableitungen | merkt sich Ergebnisse |
shallowRef | große Objekte | nur .value verfolgt |
readonly | Weitergabe | schützt vor Änderung |
Als Regel: ref für alles, computed für Ableitungen, der Rest bei konkretem
Bedarf.
| Stelle | ref |
|---|---|
| im Skript | .value nötig |
| im Template, oberste Ebene | wird ausgepackt |
als Feld eines reactive | wird ausgepackt |
| in einem Array | .value nötig |
in Map oder Set | .value nötig |
Die letzten beiden Zeilen sind der häufigste Stolperstein überhaupt.
computed | Methode | |
|---|---|---|
| Ergebnis gemerkt | ja | nein |
| Neuberechnung | bei Abhängigkeit | bei jedem Rendern |
| Aufruf im Template | ohne Klammern | mit Klammern |
| Argumente | nein | ja |
// Getter bleibt frei von Nebenwirkungen; sort/reverse vorher kopieren
const verfuegbareAnzahl = computed(() => artikel.value.filter(a => a.verfuegbar).length)
// Schreibbares computed — die Ausnahme, nicht der Normalfall
const vollerName = computed({
get() { return vorname.value + ' ' + nachname.value },
set(neu) { [vorname.value, nachname.value] = neu.split(' ') }
})
Modul: Composition API und Reaktivität · Templates und Datenbindung
Makros in script setup
| Makro | Zweck |
|---|---|
defineProps | Eingaben deklarieren |
defineEmits | Ereignisse deklarieren |
defineModel | v-model entgegennehmen, ab 3.4 |
defineExpose | nach außen sichtbar machen |
defineOptions | Komponentenoptionen setzen, ab 3.3 |
defineSlots | Slot-Typen angeben, ab 3.3 |
Makros werden nicht importiert, der Compiler kennt sie — und sie lassen sich
nicht in Bedingungen oder Funktionen aufrufen. Eine Komponente in script setup
ist von außen geschlossen, bis defineExpose sie öffnet.
Modul: Composition API und Reaktivität
Lifecycle-Hooks und Composables
| Hook | Zeitpunkt |
|---|---|
onBeforeMount | vor dem ersten Einhängen |
onMounted | nach dem Einhängen, DOM steht |
onBeforeUpdate / onUpdated | vor / nach der Aktualisierung |
onBeforeUnmount | vor dem Abbau |
onUnmounted | nach dem Abbau, Effekte gestoppt |
onErrorCaptured | bei Fehler aus einem Nachfahren |
Dazu onActivated / onDeactivated für KeepAlive. Registrierung muss
synchron im Setup erfolgen — in einem then oder nach einem await
registriert der Hook nichts.
// composables/useFilter.js — refs zurückgeben, damit das Zerlegen reaktiv bleibt
export function useFilter(artikel) {
const suche = ref('')
const treffer = computed(() => artikel.value.filter(a => passt(a, suche.value)))
return { suche, treffer }
}
Ein reactive-Objekt als Rückgabe verhindert genau das. Zustand außerhalb der
Funktion zu deklarieren teilt ihn ungewollt über alle Aufrufer.
Modul: Composition API und Reaktivität
Formulare und v-model
v-model ist kein Zwei-Wege-Kanal, sondern Bindung plus Listener:
<input :value="text" @input="e => text = e.target.value">
<input v-model="text"> <!-- gleichwertig -->
| Element | Eigenschaft | Ereignis |
|---|---|---|
input Text und textarea | value | input |
input checkbox und radio | checked | change |
select | value | change |
| Modifikator | Wirkung | Typischer Fall |
|---|---|---|
.lazy | erst bei change statt input | lange Texte |
.number | wandelt in eine Zahl | Mengenangabe |
.trim | schneidet Leerraum ab | Namen und Codes |
Bei type="number" wirkt .number automatisch. .trim greift beim Setzen,
nicht rückwirkend.
| Feld | Startwert | Ergebnis |
|---|---|---|
| eine Checkbox | false | true oder false |
| mehrere Checkboxen | leeres Array | Array der Werte |
| Radiogruppe | leerer String | Wert der Option |
select einfach | leerer String | Wert der Option |
select mit multiple | leeres Array | Array der Werte |
Ein Startwert vom falschen Typ bricht die Bindung stumm. Eigene Wahrheitswerte
setzt man über true-value / false-value.
<!-- v-model in der eigenen Komponente, ab Vue 3.4 -->
<script setup>
const menge = defineModel({ default: 1 }) // → Prop modelValue + update:modelValue
</script>
defineModel('titel') erzeugt Prop titel und Ereignis update:titel; mehrere
v-model an einer Komponente sind damit möglich. Veränderliche Vorgabewerte
gehören in eine Funktion, sonst teilen sich Instanzen ein Objekt.
Prüfungen gehören als computed neben den Zustand, nicht in einen Watcher —
dort entstehen leicht Schleifen. Neben den Werten braucht ein Formular eigene
Zustände: sendet (Sperre, sonst legt ein Doppelklick zwei Datensätze an),
abgesendet, beruehrt, serverFehler.
Modul: Formularverarbeitung
Styling: scoped, Klassen, Preprozessoren
scoped hängt jedem Selektor ein data-v-…-Attribut an — Kapselung ist das
nicht, globale Selektoren wirken weiter hinein.
| Selektor | Wirkt auf | Typischer Fall |
|---|---|---|
:deep(…) | Inneres einer Kindkomponente | fremde Komponente anpassen |
:slotted(…) | hereingereichten Inhalt | Layout des Slots |
:global(…) | die ganze Seite | Klasse am body |
Alle drei gelten nur innerhalb eines scoped-Blocks; die alten Pfeil-Formen
sind veraltet.
| Fall | Syntax |
|---|---|
| Klasse an- und ausschalten | Objekt: :class="{ frei: offen }" |
| Klassenname aus einer Variable | Array: :class="[basis, zustand]" |
| Mehrere Zustände zugleich | Objekt im computed |
Statisches class und gebundenes :class ergänzen sich, sie ersetzen sich
nicht. Auf einer Komponente landet die Klasse am Wurzelelement — bei mehreren
Wurzeln braucht es $attrs.class.
<style scoped>
p { color: v-bind(farbe); } /* wird zu einer CSS-Variablen, bleibt aktuell */
</style>
| Sprache | Paket | lang-Attribut |
|---|---|---|
| Sass und SCSS | sass-embedded oder sass | scss oder sass |
| Less | less | less |
| Stylus | stylus | stylus |
Kein Vite-Plugin und kein Eintrag in vite.config.js nötig, die Erkennung läuft
über lang. Sass-Variablen sind Bauzeit-Werte und lassen sich zur Laufzeit
nicht ändern — für Themes und dunklen Modus gewinnen deshalb CSS-Variablen.
Modul: Styling von Komponenten
Typische Fallen
- Vergessenes
.valueliefert das ref-Objekt statt des Werts — meist alsNaNsichtbar. reactiveneu zuweisen kappt die Reaktivität still; Zerlegen liefert gewöhnliche Werte ohne Verbindung.computed, dasDate.now()liest, aktualisiert nie — das ist keine reaktive Quelle.onMountednach einemawaitregistriert nichts. Zu jedemaddEventListenergehört das Gegenstück imonUnmounted.- Props direkt ändern ist verboten. Ein geändertes Prop-Objekt wirkt trotzdem nach oben — Objekte sind Referenzen.
- Ohne
.preventlädt das Formular die Seite neu und der Zustand ist weg. v-showauf<template>wirkt nicht, es gibt kein Element zum Schalten.- Ohne Doppelpunkt ist der Attributwert eine Zeichenkette, kein Ausdruck.
.vuegehört in den Importpfad, sie ist nicht optional; Komponententags in PascalCase, sonst kollidieren sie mit HTML-Elementen.- Vue-2-Filter mit dem senkrechten Strich lösen heute einen Fehler aus; Vue 2 ist seit Ende 2023 ohne Support.
:deepohnescopedist wirkungslos — und koppelt zwei Dateien fest aneinander, wo es wirkt.
Module: alle sechs des Kern-Teils