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, State Management, Routing, Testing und SSR-Patterns abdeckt. Nuxt-spezifische Anleitungen sind enthalten, wo sie von Vanilla-Vue abweichen.
Wann aktivieren
Aktivieren Sie diesen Skill, wenn:
- Das Projekt Vue.js (beliebige Version), Nuxt, Vite + Vue oder Pinia verwendet.
- Der Benutzer nach Vue-Komponentenarchitektur, Composables, Reaktivität oder State Management fragt.
- Vue Single-File Components (
.vue-Dateien) überprüft werden. - Vue Router, Pinia Stores oder Vite/Vitest-Konfiguration eingerichtet wird.
- Vue-spezifische Performance-, Sicherheits- oder SSR-Patterns diskutiert werden.
1. Projektstruktur
Empfohlene Struktur (Feature-First)
src/
├── api/ # API-Client und Endpunkt-Definitionen
├── assets/ # Statische Assets (Bilder, Schriften, Icons)
├── components/ # Geteilte/wiederverwendbare Komponenten
│ ├── base/ # Basis-UI-Primitive (Button, Input, Modal)
│ └── features/ # Featurespezifische geteilte Komponenten
├── composables/ # Wiederverwendbare Composition-API-Logik
├── layouts/ # Seitenlayouts (optional)
├── pages/ # Route-level Seitenkomponenten
├── router/ # Vue Router Konfiguration
├── stores/ # Pinia Stores
├── types/ # TypeScript-Typdefinitionen
├── utils/ # Reine Hilfsfunktionen
└── App.vue # Root-Komponente
Dateibenennung
| Konvention | Wann verwenden |
|---|---|
PascalCase.vue | Alle Komponenten (erzwungen durch vue/multi-word-component-names) |
useCamelCase.ts | Composables |
camelCase.ts | Hilfsfunktionen, API-Clients, Typen |
kebab-case-Verzeichnisse | Route-Segmente, Feature-Ordner |
2. Komponentenarchitektur
Single-File Component Reihenfolge
<script setup lang="ts">
// 1. Imports (vue → ecosystem → absolut → relativ)
// 2. Props & Emits & Slots
// 3. Composables
// 4. Lokaler Zustand (ref/reactive)
// 5. Computed-Eigenschaften
// 6. Methoden
// 7. Watcher
// 8. Lifecycle-Hooks
</script>
<template>
<!-- Template-Inhalt -->
</template>
<style scoped>
/* Gültige Styles */
</style>
Präsentations- vs. Container-Komponenten
- Container-Komponenten: Eigene Datenabrufe, Zustände und Seiteneffekte. Rendern Präsentationskomponenten.
- Präsentationskomponenten: Empfangen Props, emittieren Events. Keine API-Aufrufe, kein Store-Zugriff. Reines Rendern.
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. - Boolesche Props:
isXxx,hasXxx,canXxx. - Props niemals mutieren – stattdessen Events emittieren.
- 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 Camel-Case 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 dem Präfix
usebeginnen. - Reaktive Werte zurückgeben (
ref,computed,reactive), niemals einfache Primitive. - Reaktive Eingaben über
MaybeRef/toRef()/toValue()akzeptieren. - Seiteneffekte in
onUnmountedoder WatcheronCleanupbereinigen. - Keine Seiteneffekte auf Modulebene.
vs Mixins
Composables ersetzen Vue 2 Mixins vollständig:
- Mixins: Undurchsichtiger Datenfluss, Kollisionen bei Datenquellen, Namenskonflikte.
- Composables: Explizite Imports, klare Rückgabewerte, zusammensetzbar und tree-shakable.
4. State Management
Wann was verwenden
| Pattern | Anwendungsfall |
|---|---|
ref() / reactive() | Lokaler Komponentenzustand |
| Props + Emits | Eltern-Kind-Kommunikation |
| Provide / Inject | Theme, Konfiguration, Plugin-API |
| Pinia Store | Globaler, geteilter, komplexer Zustand |
| Serverzustand-Composable | API-Daten mit Caching (wrapper für fetch/TanStack Query) |
Pinia Setup Store (Bevorzugt)
// 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 (nicht Options Store) verwenden.
- Aktionen für Business-Level-Mutationen und
$patch()für gruppierte Updates bevorzugen. - Jede asynchrone Aktion: Ladevorgang + Erfolg + Fehler behandeln.
5. Vue Router
Route-Definitionen
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 Route-Parameter
Wenn eine Komponente gemountet bleibt, aber die Route-Parameter sich ändern:
const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));
6. Template-Patterns
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 Umschaltungen -->
<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>
<!-- Event-Handling -->
<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. Performance
| Technik | Wann verwenden |
|---|---|
v-memo | Listenelemente, die sich selten ändern |
v-once | Inhalt, der einmal gerendert wird und für immer statisch ist |
shallowRef() | Große Datenstrukturen, die vollständig ersetzt werden |
shallowReactive() | Nur Eigenschaften der obersten Ebene sind reaktiv |
v-show über v-if | Häufige Sichtbarkeitsumschaltungen |
<KeepAlive :max="10"> | Zwischenspeichern umgeschalteter Ansichten |
| Lazy Routes | () => import(...) für nicht kritische Routen |
Suspense | Asynchrones Laden von Komponenten mit Fallback |
8. Testing
Stack
- Vitest für Unit- und Komponententests
- Vue Test Utils zum Mounten und für Interaktionen
- @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("renders and emits", 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 Patterns
Auto-Imports
Nuxt importiert ref, computed, watch, useFetch, useAsyncData usw. automatisch. Verwenden Sie sie direkt ohne Import. Bei Nicht-Nuxt-Projekten immer explizit importieren.
useAsyncData / useFetch
const { data: user, pending, error, refresh } = await useAsyncData(
"user", // eindeutiger Key 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 Config
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Server-seitig
apiSecret: "",
// öffentlich (dem Client zugänglich)
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 nötig)
const { count = 0, msg = "hallo" } = defineProps<{
count?: number;
msg?: string;
}>();
// Einschränkung: Kann destrukturierte Props nicht direkt watchen
watch(() => count, (newVal) => { ... }); // OK: Getter erforderlich
useTemplateRef()
Ersetzen Sie namensgleiche einfache Refs mit 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-stable 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 Behebung |
|---|---|---|
defineProps() destrukturieren (Vue < 3.5) | Erfasst einen Snapshot, verliert Reaktivität | Zugriff über props.xxx oder toRefs() verwenden |
watch() auf destrukturierte Props (Vue 3.5+) | Compile-Time-Fehler – destrukturierte Props können nicht direkt ge watched werden | Getter-Wrapper verwenden: watch(() => count, ...) |
v-if + v-for auf demselben Element | Mehrdeutige Ausführungsreihenfolge | Berechnetes gefiltertes Array verwenden |
v-for Key = Index | Kaputter Zustand bei Neuanordnung | Stabile Datenbank-IDs verwenden |
| Props mutieren | Verstößt gegen unidirektionalen Datenfluss | Events emittieren oder v-model verwenden |
v-html mit Benutzerinhalten | XSS-Sicherheitslücke | Mit DOMPurify bereinigen |
| Mixins in Vue 3 | Undurchsichtig, kollisionsanfällig | Durch Composables ersetzen |
| Seiteneffekte auf Modulebene im Composable | Über Instanzen hinweg geteilt | Innerhalb von onMounted + onUnmounted eingerenzen |
reactive() für ersetzbaren Zustand | Ersetzung bricht Reaktivität | ref() stattdessen verwenden |
| Watcher ohne Bereinigung | Speicherlecks, Race Conditions | 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, Namensabgleich fragil | useTemplateRef() (Vue 3.5+) verwenden |
Verwandte Skills
accessibility— ARIA, semantisches HTML, Fokus-Managementfrontend-patterns— Framework-übergreifende Frontend-Architekturtypescript— TypeScript Best Practices für Vue-Projektecoding-standards— Allgemeine Code-Qualitätsstandards