Vue.js Patterns und Best Practices
Umfassender Leitfaden für die Vue.js 3-Entwicklung mit der Composition API (<script setup>), der Komponentendesign, Reaktivität, Zustandsverwaltung, Routing, Tests und SSR-Muster abdeckt. Nuxt-spezifische Anleitungen sind enthalten, wo sie sich von reinem Vue unterscheiden.
Wann aktivieren
Aktivieren Sie diesen Skill, wenn:
- Das Projekt Vue.js (jede Version), Nuxt, Vite + Vue oder Pinia verwendet.
- Der Benutzer nach Vue-Komponentenarchitektur, Composables, Reaktivität oder Zustandsverwaltung fragt.
- Vue Single-File-Komponenten (
.vue-Dateien) überprüft werden. - Vue Router, Pinia-Stores oder Vite/Vitest-Konfiguration eingerichtet werden.
- Vue-spezifische Leistungs-, Sicherheits- oder SSR-Muster besprochen werden.
1. Projektstruktur
Empfohlenes Layout (Feature-First)
src/
├── api/ # API-Client und Endpunktdefinitionen
├── assets/ # Statische Assets (Bilder, Schriften, Icons)
├── components/ # Geteilte/wiederverwendbare Komponenten
│ ├── base/ # Basis-UI-Primitive (Button, Input, Modal)
│ └── features/ # Funktionsspezifische geteilte Komponenten
├── composables/ # Wiederverwendbare Composition-API-Logik
├── layouts/ # Seitenlayouts (optional)
├── pages/ # Routenebenen-Seitenkomponenten
├── router/ # Vue Router-Konfiguration
├── stores/ # Pinia-Stores
├── types/ # TypeScript-Typdefinitionen
├── utils/ # Reine Hilfsfunktionen
└── App.vue # Root-Komponente
Dateibenennung
| Konvention | Verwendung |
|---|---|
PascalCase.vue | Alle Komponenten (erzwungen durch vue/multi-word-component-names) |
useCamelCase.ts | Composables |
camelCase.ts | Hilfsfunktionen, API-Clients, Typen |
kebab-case-Verzeichnisse | Routensegmente, Funktionsordner |
2. Komponentenarchitektur
Reihenfolge in Single-File-Komponenten
<script setup lang="ts">
// 1. Imports (vue → ecosystem → absolut → relativ)
// 2. Props & Emits & Slots
// 3. Composables
// 4. Lokaler Zustand (ref/reactive)
// 5. Berechnete Eigenschaften
// 6. Methoden
// 7. Watcher
// 8. Lifecycle-Hooks
</script>
<template>
<!-- Template-Inhalt -->
</template>
<style scoped>
/* Scoped Styles */
</style>
Präsentations- vs. Containerkomponenten
- Containerkomponenten: Eigene Datenabrufe, Zustände und Seiteneffekte. Rendern Präsentationskomponenten.
- Präsentationskomponenten: Empfangen Props, geben Events aus. Keine API-Aufrufe, kein Store-Zugriff. Reines Rendering.
Props Best Practices
// Typbasierte Props mit Standardwerten
interface Props {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
items: Item[];
}
const props = withDefaults(defineProps<Props>(), {
variant: "primary",
disabled: false,
});
- Immer
typeundrequired/defaultangeben, wo angemessen. - Boolean-Props:
isXxx,hasXxx,canXxx. - Props niemals mutieren – stattdessen Events ausgeben.
- Für v-model-Bindung
defineModel()(Vue 3.4+) odermodelValue+update:modelValueverwenden.
Events
const emit = defineEmits<{
submit: [];
"update:modelValue": [value: string];
select: [id: string, index: number];
}>();
- In Templates Kebab-Case verwenden (
@update:model-value). - Im Script CamelCase verwenden (
emit("update:modelValue", val)).
3. Composables (Wiederverwendbare Logik)
Struktur
// composables/useDebounce.ts
export function useDebounce<T>(value: MaybeRef<T>, delay: number): Ref<T> {
const debounced = ref(toValue(value)) as Ref<T>;
let timer: ReturnType<typeof setTimeout>;
watch(
() => toValue(value),
(newVal) => {
clearTimeout(timer);
timer = setTimeout(() => { debounced.value = newVal; }, delay);
}
);
onUnmounted(() => clearTimeout(timer));
return readonly(debounced);
}
Regeln
- Muss mit
use-Präfix beginnen. - Reaktive Werte zurückgeben (
ref,computed,reactive), niemals einfache Primitive. - Reaktive Eingaben über
MaybeRef/toRef()/toValue()akzeptieren. - Seiteneffekte in
onUnmountedoder WatcheronCleanupbereinigen. - Keine modulweiten Seiteneffekte.
vs. Mixins
Composables ersetzen Vue 2-Mixins vollständig:
- Mixins: Undurchsichtiger Datenfluss, Quellenkonflikte, Namenskonflikte.
- Composables: Explizite Imports, klare Rückgabewerte, komponierbar und treeshakable.
4. Zustandsverwaltung
Wann was verwenden
| Muster | Anwendungsfall |
|---|---|
ref() / reactive() | Lokaler Komponentenzustand |
| Props + Emits | Eltern-Kind-Kommunikation |
| Provide / Inject | Theme, Konfiguration, Plugin-API |
| Pinia Store | Globaler, geteilter, komplexer Zustand |
| Serverzustands-Composable | API-Daten mit Caching (Wrap fetch/TanStack Query) |
Pinia Setup Store (Empfohlen)
// stores/useCartStore.ts
export const useCartStore = defineStore("cart", () => {
const items = ref<CartItem[]>([]);
const isLoading = ref(false);
const totalPrice = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
);
const itemCount = computed(() =>
items.value.reduce((sum, i) => sum + i.quantity, 0)
);
async function addItem(productId: string) {
isLoading.value = true;
try {
const item = await fetchProduct(productId);
const existing = items.value.find(i => i.id === item.id);
if (existing) existing.quantity++;
else items.value.push({ ...item, quantity: 1 });
} finally {
isLoading.value = false;
}
}
return { items, isLoading, totalPrice, itemCount, addItem };
});
- Setup Store-Syntax verwenden (nicht Options Store).
- Aktionen für geschäftskritische Mutationen und
$patch()für gruppierte Updates bevorzugen. - Jede asynchrone Aktion: Lade-, Erfolgs- und Fehlerzustand behandeln.
5. Vue Router
Routendefinitionen
const routes = [
{
path: "/users/:id",
name: "user-detail",
component: () => import("@/pages/UserDetail.vue"), // lazy
props: true, // Params als Props übergeben
meta: { requiresAuth: true },
},
];
Navigations-Guards
router.beforeEach((to, from) => {
const { isLoggedIn } = useAuthStore();
if (to.meta.requiresAuth && !isLoggedIn) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
Reaktive Routenparameter
Wenn eine Komponente gemountet bleibt, sich die Routenparameter aber ändern:
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));
6. Template-Muster
Template-Syntax
<!-- v-if/v-else-if/v-else -->
<div v-if="isLoading">Lädt...</div>
<div v-else-if="error">Fehler: {{ error }}</div>
<div v-else>{{ content }}</div>
<!-- v-show für häufige Umschaltvorgänge -->
<div v-show="isOpen">Umschaltbarer Inhalt</div>
<!-- v-for mit stabilen Keys -->
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
<!-- Berechnete gefilterte Liste (nicht v-if + v-for auf demselben Element) -->
<div v-for="item in activeItems" :key="item.id">{{ item.name }}</div>
<!-- Ereignisbehandlung -->
<form @submit.prevent="handleSubmit">
<button type="submit">Speichern</button>
</form>
<!-- v-model -->
<input v-model="name" />
<CustomInput v-model="value" v-model:title="title" />
7. Leistung
| Technik | Wann verwenden |
|---|---|
v-memo | Listenelemente, die sich selten ändern |
v-once | Einmal gerenderter und immer statischer Inhalt |
shallowRef() | Große Datenstrukturen, die komplett ersetzt werden |
shallowReactive() | Nur Top-Level-Eigenschaften sind reaktiv |
v-show über v-if | Häufige Sichtbarkeitsumschaltungen |
<KeepAlive :max="10"> | Zwischengespeicherte Ansichten |
| Lazy Routes | () => import(...) für nicht kritische Routen |
Suspense | Asynchrones Laden von Komponenten mit Fallback |
8. Tests
Stack
- Vitest für Unit- und Komponententests
- Vue Test Utils zum Mounten und Interagieren
- @pinia/testing für Store-Mocking
- Playwright für E2E
Komponententest-Muster
import { mount } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import UserCard from "./UserCard.vue";
beforeEach(() => { setActivePinia(createPinia()); });
it("rendert und emittet", async () => {
const wrapper = mount(UserCard, {
props: { user: { id: "1", name: "Alice" } },
});
expect(wrapper.text()).toContain("Alice");
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("select")![0]).toEqual(["1"]);
});
9. Nuxt-spezifische Muster
Auto-Imports
Nuxt importiert ref, computed, watch, useFetch, useAsyncData usw. automatisch. Verwenden Sie sie direkt ohne Import. Verwenden Sie für Nicht-Nuxt-Projekte immer explizite Imports.
useAsyncData / useFetch
const { data: user, pending, error, refresh } = await useAsyncData(
"user", // eindeutiger Schlüssel für Caching
() => $fetch(`/api/users/${id}`),
);
const { data: posts } = await useFetch("/api/posts", {
query: { page: 1 },
key: "posts-page-1", // dedupliziert Anfragen
});
Server-Routen
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const { id } = await getValidatedRouterParams(event, z.object({
id: z.string().uuid(),
}).parse);
// ... abrufen und zurückgeben
});
Runtime-Konfiguration
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// nur serverseitig
apiSecret: "",
// öffentlich (für Client freigegeben)
public: {
apiBase: "https://api.example.com",
},
},
});
10. Vue 3.5+ Neue APIs
Reaktive Props-Destrukturierung
Vue 3.5 hat die reaktive Props-Destrukturierung stabilisiert – destrukturierte Variablen von defineProps() sind automatisch reaktiv:
// Vue 3.5+: destrukturierte Props sind reaktiv (kein toRefs erforderlich)
const { count = 0, msg = "hallo" } = defineProps<{
count?: number;
msg?: string;
}>();
// Einschränkung: destrukturierte Props können nicht direkt beobachtet werden
watch(() => count, (newVal) => { ... }); // PASS: Getter erforderlich
useTemplateRef()
Ersetzen Sie namensgleiche einfache Refenzen durch useTemplateRef() für Template-Referenzen:
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef<HTMLInputElement>("input");
// "input" entspricht dem ref="input"-Attribut im Template, nicht dem Variablennamen
Unterstützt dynamische Ref-IDs: useTemplateRef(dynamicRefId).
onWatcherCleanup()
Global importierbare Watcher-Bereinigungs-API (Vue 3.5+). Sie muss synchron innerhalb des Watcher-Callbacks aufgerufen werden:
import { watch, onWatcherCleanup } from "vue";
watch(userId, async (newId) => {
const controller = new AbortController();
onWatcherCleanup(() => controller.abort());
// ... mit Signal abrufen
});
useId()
SSR-stabile eindeutige ID-Generierung für Formularelemente und Barrierefreiheit:
import { useId } from "vue";
const id = useId();
defer Teleport
<Teleport defer> ermöglicht das Teleportieren zu Zielen, die im selben Zyklus gerendert werden:
<Teleport defer to="#container">Inhalt</Teleport>
<div id="container"></div>
Lazy Hydration (SSR)
defineAsyncComponent() unterstützt jetzt die hydrate-Strategie:
import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
loader: () => import("./Comp.vue"),
hydrate: hydrateOnVisible(),
});
Anti-Patterns
| Anti-Pattern | Warum es falsch ist | Die Lösung |
|---|---|---|
Destrukturierung von defineProps() (Vue < 3.5) | Erfasst einen Snapshot, verliert die Reaktivität | Zugriff über props.xxx oder toRefs() verwenden |
watch() auf destrukturierte Props (Vue 3.5+) | Kompilierungsfehler – destrukturierte Props können nicht direkt beobachtet werden | Getter-Wrapper verwenden: watch(() => count, ...) |
v-if + v-for auf demselben Element | Mehrdeutige Ausführungsreihenfolge | Berechnetes gefiltertes Array verwenden |
v-for key = Index | Unterbrochener Zustand bei Neuordnung | Stabile Datenbank-IDs verwenden |
| Props mutieren | Verletzt den unidirektionalen Datenfluss | Events ausgeben oder v-model verwenden |
v-html mit Benutzerinhalt | XSS-Schwachstelle | Mit DOMPurify bereinigen |
| Mixins in Vue 3 | Undurchsichtig, kollisionsanfällig | Durch Composables ersetzen |
| Modulweite Seiteneffekte in Composable | Über Instanzen hinweg geteilt | In onMounted + onUnmounted eingerenzen |
reactive() für ersetzbaren Zustand | Ersetzung bricht Reaktivität | ref() stattdessen verwenden |
| Watcher ohne Bereinigung | Speicherlecks, Wettlaufsituationen | onCleanup oder onWatcherCleanup() (Vue 3.5+) verwenden |
| Options-API in neuem Vue 3-Code | Ökosystem bewegt sich zur Composition API | <script setup> verwenden |
| Einfache Ref für Template-Referenzen | Keine dynamische Ref-Unterstützung, Namenszuordnung fragil | useTemplateRef() (Vue 3.5+) verwenden |
Verwandte Skills
accessibility— ARIA, semantisches HTML, Fokusverwaltungfrontend-patterns— Framework-übergreifende Frontend-Architekturtypescript— TypeScript-Best-Practices, angewendet auf Vue-Projektecoding-standards— Allgemeine Codequalitätsstandards