Vue Component und Composable Design Patterns
Jozo
Jozo
2025/03/18
12 min read

Vue Component und Composable Design Patterns: Ein Startup-Leitfaden

Dieser Leitfaden bietet einen praktischen Überblick über Vue Component und Composable Design Patterns, mit Tipps und Beispielen, zugeschnitten für Entwickler in deinem Startup. Er nutzt Erkenntnisse aus verschiedenen Quellen, um dir zu helfen, saubere, wartbare und skalierbare Vue-Anwendungen zu schreiben.

Component Design Patterns

1. Components Pattern

Das Extrahieren wiederverwendbarer Komponenten aus bestehenden Komponenten vereinfacht Code und verbessert Wiederverwendbarkeit. Dies fördert das Single Responsibility Principle und macht deine Codebasis modularer und wartbarer.

Tipp: Identifiziere und extrahiere versteckte Komponenten innerhalb deines bestehenden Codes. Suche nach wiederholten UI-Elementen oder Logik, die gekapselt werden kann.

Beispiel:

<!-- Vorher: Komplexes Formular -->
<template>
  <div>
    <label for="name">Name:</label>
    <input type="text" id="name" v-model="name">
    <label for="email">Email:</label>
    <input type="email" id="email" v-model="email">
    <button @click="submitForm">Submit</button>
  </div>
</template>
<!-- Nachher: Verwenden wiederverwendbarer Komponenten -->
<template>
  <div>
    <InputField label="Name" v-model="name" type="text" />
    <InputField label="Email" v-model="email" type="email" />
    <SubmitButton @click="submitForm" />
  </div>
</template>

2. Clean Components

Strebe nach Komponenten, die nicht nur funktionieren, sondern gut funktionieren, unter Berücksichtigung von Code-Lesbarkeit, Wartbarkeit und Testbarkeit. Clean Components sind einfach zu verstehen, zu modifizieren und zu debuggen.

Tipp: Schreibe Komponenten, die leicht zu verstehen und zu wartbar sind. Nutze klare Naming-Konventionen, konsistente Formatierung und gut definierte Verantwortlichkeiten.

Beispiel:

<!-- Schlecht: Komponente mit gemischten Concerns -->
<template>
  <div>
    <button @click="handleClick">{{ buttonText }}</button>
    <div v-if="showDetails">{{ details }}</div>
  </div>
</template>
<script setup>
import { ref } from 'vue';

const buttonText = ref('Show Details');
const showDetails = ref(false);
const details = ref('');

async function handleClick() {
  showDetails.value = !showDetails.value;
  if (showDetails.value) {
    details.value = await fetchData();
  }
}
</script>
<!-- Gut: Komponente mit fokussierter Verantwortlichkeit -->
<template>
  <div>
    <ShowDetailsButton @click="toggleDetails" :text="buttonText" />
    <DetailsDisplay v-if="showDetails" :details="details" />
  </div>
</template>
<script setup>
import { ref } from 'vue';
import ShowDetailsButton from './ShowDetailsButton.vue';
import DetailsDisplay from './DetailsDisplay.vue';

const showDetails = ref(false);
const details = ref('');
const buttonText = ref('Show Details');

async function toggleDetails() {
  showDetails.value = !showDetails.value;
  if (showDetails.value) {
    details.value = await fetchData();
  }
}
</script>

3. Mehrere Komponenten in einer Datei

Für kleine, in sich geschlossene Komponenten, bedenke, sie in der gleichen Datei zu behalten. Dies kann die Anzahl der Dateien in deinem Projekt reduzieren und die Entwicklungsgeschwindigkeit verbessern, besonders für Komponenten, die eng gekoppelt sind.

Tipp: Vermeidest unnötige Dateien für einfache Komponenten. Nutze diesen Ansatz für Komponenten, die nur an einem Ort verwendet werden.

Beispiel:

<template>
  <div>
    <MyButton @click="handleClick">Click Me</MyButton>
  </div>
</template>

<script setup>
import MyButton from './MyButton.vue';

function handleClick() {
  alert('Button clicked!');
}
</script>
<template>
  <button @click="$emit('click')">
    <slot></slot>
  </button>
</template>
<script setup>
defineEmits(['click']);
</script>

4. Controlled Props Pattern

Dieses Pattern erlaubt es dir, den internen Zustand einer Komponente von der Parent zu überschreiben. Dies ist nützlich, wenn du einen Komponenten-Zustand von außen erzwingen musst, wie die Steuerung der Sichtbarkeit eines Modals oder der Auswahl in einem Dropdown.

Tipp: Nutze dieses Pattern, wenn du einen Komponenten-Zustand von außen erzwingen musst. Gib Props zur Komponente um seinen internen Zustand zu kontrollieren.

Beispiel:

<!-- Modal.vue -->
<template>
  <div v-if="isOpen" class="modal">
    <div class="modal-content">
      <slot></slot>
      <button @click="closeModal">Close</button>
    </div>
  </div>
</template>
<script setup>
defineProps({
  isOpen: {
    type: Boolean,
    default: false
  }
});

const emit = defineEmits(['close']);

function closeModal() {
  emit('close');
}
</script>
<!-- Parent Component -->
<template>
  <div>
    <button @click="showModal = true">Open Modal</button>
    <Modal :isOpen="showModal" @close="showModal = false">
      <p>Modal Content</p>
    </Modal>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';

const showModal = ref(false);
</script>

Composable Design Patterns

1. Options Object Pattern

Nutze ein Objekt, um Parameter in Composables zu übergeben. Dies ermöglicht Flexibilität und Skalierbarkeit. Es ist die bevorzugte Methode für die Übergabe vieler Optionen zu einem Composable.

Tipp: Dieses Pattern wird in VueUse verwendet und wird stark empfohlen, wenn du das Verhalten eines Composable konfigurieren musst.

Beispiel:

// useFetch.js
import { ref, onMounted } from 'vue';

export function useFetch(url, options = {}) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const { method = 'GET', headers = {}, body = null } = options;

  async function fetchData() {
    loading.value = true;
    try {
      const response = await fetch(url, {
        method,
        headers,
        body: body ? JSON.stringify(body) : null
      });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      data.value = await response.json();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  }

  onMounted(() => {
    fetchData();
  });

  return { data, loading, error, fetchData };
}

// In einer Komponente:
import { useFetch } from './useFetch';
export default {
  setup() {
    const { data, loading, error } = useFetch('/api/data', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: { key: 'value' }
    });
    return { data, loading, error };
  }
}

2. Inline Composables

Erstelle Composables direkt innerhalb der Komponenten-Datei um neue Dateien zu vermeiden. Dies ist besonders nützlich für Composables, die sehr spezifisch zu einer einzelnen Komponente sind und nicht anderswo wiederverwendet werden sollen. Nutze Inline-Composables für kleine, Komponenten-spezifische Logik. Dies hält zusammenhängenden Code zusammen und kann deine Komponenten-Struktur vereinfachen.

Tipp: Nutze Inline-Composables für kleine, Komponenten-spezifische Logik, die nicht anderswo wiederverwendet werden muss. Dieser Ansatz hält zusammenhängenden Code zusammen und vereinfacht deine Komponenten-Struktur.

Beispiel:

Sagen wir, du hast das in deiner Komponente:

<template>
  <div>{{ formattedDate }}</div>
</template>

<script setup>
import { ref, computed } from 'vue'

const useFormattedDate = () => {
  const rawDate = ref(new Date())
  const formattedDate = computed(() => {
    const options = { year: 'numeric', month: 'long', day: 'numeric' }
    return rawDate.value.toLocaleDateString(undefined, options)
  })
  return { formattedDate }
}

const { formattedDate } = useFormattedDate()
</script>

Das ist vorerst OK, aber wenn du diese Datums-Formatierung anderswo brauchst, bist du raus. Hier ist die umgestalte Version:

// src/composables/useFormattedDate.ts
import { ref, computed } from 'vue'

export function useFormattedDate() {
  const rawDate = ref(new Date())
  const formattedDate = computed(() => {
    const options = { year: 'numeric', month: 'long', day: 'numeric' }
    return rawDate.value.toLocaleDateString(undefined, options)
  })
  return { formattedDate }
}
<!-- In deiner Komponente -->
<template>
  <div>{{ formattedDate }}</div>
</template>

<script setup>
import { useFormattedDate } from './composables/useFormattedDate'

const { formattedDate } = useFormattedDate()
</script>

Jetzt, kannst du useFormattedDate in jeder Komponente wiederverwendung. Problem gelöst.

Für längere Inhalte, siehe die englische Version des Blogs für vollständige Muster Übersicht.