Composable — Kapselung wiederverwendbarer Logik
In diesem Kapitel lernst du die Best Practice von Vue3 kennen — die Verwendung von Composable-Funktionen zum Kapseln wiederverwendbarer Logik, wodurch der Code sauberer und leichter wartbar wird.
Was ist ein Composable?
Mit zunehmendem Funktionsumfang wird die JS-Logik in Komponenten länger. Sowohl die Startseite als auch die Detailseite müssen Artikeldaten laden, sodass der Code zweimal dupliziert wird.
Composable (Composable-Funktion)Sie extrahiert wiederverwendbare Logik in unabhängige JS-Funktionen, und jede Komponente, die sie benötigt, kann sie dort aufrufen.
Im Kern ist eseine gewöhnliche Funktion, die reaktive Daten und Methoden zurückgibt.。
Warum braucht man Composable?
Vergleiche die Codestruktur vor und nach der Änderung:
| Ohne Composable (vorher) | Mit Composable (nachher) |
|---|---|
| Jede Komponente schreibt ihre eigene Fetch-Logik | Ein usePosts() wird überall wiederverwendet |
| Datenladen und UI-Logik sind miteinander vermischt | Datenlogik ist unabhängig, Komponenten kümmern sich nur um die UI |
| Das Ändern eines Teils der Logik erfordert das Ändern mehrerer Dateien | Ändere eine Stelle, alle Komponenten aktualisieren sich synchron |
| Der Komponenten-Code bläht sich schnell auf, wenn das Geschäft wächst | Der Komponenten-Code hat eine einzige Verantwortlichkeit, und seine Größe ist kontrollierbar |
Wenn Sie die Mixins von Vue2 verwendet haben, lässt sich Composable als eine klarere und sicherere Alternative verstehen – es gibt keine Namenskonflikte, und die Datenquelle ist transparent.
Namenskonventionen und Regeln
| Regel | Beschreibung | Beispiel |
|---|---|---|
withusePräfix | Auf einen Blick erkennen, dass dies ein Composable ist | usePosts、useDarkMode |
Ablegen incomposables/Verzeichnis sichern | Leicht zu finden und zu warten | src/composables/usePosts.js |
| Reaktive Daten zurückgeben | ref oder reactive zurückgeben, den Aufrufer reaktiv halten | return { articles, isLoading } |
| Eine Funktion erledigt nur eine Sache | Einzelverantwortung, einfach zu kombinieren | usePosts kümmert sich nur um Daten, useDarkMode nur um Themes |
usePosts — Kapselung der Artikeldatenlogik
Dieses Composable ist zuständig für:Daten laden + Kategoriefilterung + Suchfilterung。
Beispiel
import { ref, computed, onMounted } from 'vue'
export function usePosts() {
const articles = ref([]) // Alle Artikeldaten
const isLoading = ref(true) // Ladezustand
const error = ref(null) // Fehlermeldung
const activeCategory = ref('All') // Aktuell ausgewählte Kategorie
const keyword = ref('') // Suchbegriff
// Alle Kategorien extrahieren (Duplikate entfernen)
const categories = computed(() => {
const cats = articles.value.map(a => a.category)
return ['All', ...new Set(cats)]
})
// Gefilterte Artikel: zuerst nach Kategorie, dann nach Stichwort
const filteredArticles = computed(() => {
let result = articles.value
// Nach Kategorie filtern
if (activeCategory.value !== 'All') {
result = result.filter(a => a.category === activeCategory.value)
}
// Nach Stichwort filtern (Titel oder Zusammenfassung abgleichen)
if (keyword.value.trim()) {
const kw = keyword.value.trim().toLowerCase()
result = result.filter(a =>
a.title.toLowerCase().includes(kw) ||
a.summary.toLowerCase().includes(kw)
)
}
return result
})
// Einen einzelnen Artikel anhand der ID finden
function getArticleById(id) {
return articles.value.find(a => a.id === id)
}
// Kategorie wechseln
function setCategory(cat) {
activeCategory.value = cat
}
// Daten laden
async function fetchPosts() {
isLoading.value = true
error.value = null
try {
const res = await fetch('/posts.json')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
articles.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
isLoading.value = false
}
}
// Automatisch laden, wenn die Komponente gemountet ist
onMounted(() => {
fetchPosts()
})
// Daten und Methoden zurückgeben, die Komponenten verwenden können
return {
articles,
isLoading,
error,
activeCategory,
keyword,
categories,
filteredArticles,
getArticleById,
setCategory,
fetchPosts
}
}
Jetzt wird die Homepage-Komponente sehr knapp, rufe einfach usePosts auf:
Beispiel
<script setup>
import { usePosts } from '../composables/usePosts.js'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'
// Alle artikelbezogenen Daten und Methoden mit einer Zeile Code abrufen
const {
isLoading,
error,
activeCategory,
keyword,
categories,
filteredArticles,
setCategory,
fetchPosts
} = usePosts()
</script>
<template>
<div class="home">
<div class="search-bar">
<input
v-model="keyword"
type="text"
placeholder="Titel oder Zusammenfassung des Artikels suchen..."
/>
</div>
<CategoryFilter
:="categories"
active-category="activeCategory"
@update-category="setCategory"
/>
<p v-if="isLoading">Wird geladen...</p>
<p v-else-if="error">Laden fehlgeschlagen: {{ error }}
<button @click="fetchPosts">Erneut versuchen</button>
</p>
<p v-else-if="filteredArticles.length === 0">Keine passenden Artikel</p>
<div v-else class="article-grid">
<BlogCard
v-for="article in filteredArticles"
:key="article.id"
:id="article.id"
:title="article.title"
:summary="article.summary"
:date="article.date"
:category="article.category"
/>
</div>
</div>
</template>
Die Detailseite kann usePosts ebenfalls wiederverwenden, ohne fetch erneut zu schreiben. Beachte, dass du es separat aus Vue importieren musst.computed:
Beispiel
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { usePosts } from '../composables/usePosts.js'
const route = useRoute()
const id = Number(route.params.id)
const { isLoading, error, getArticleById } = usePosts()
// articles sind reaktive Daten, computed wird nach Abschluss des fetch automatisch neu berechnet
const article = computed(() => getArticleById(id))
</script>
<template>
<div>
<p v-if="isLoading">Wird geladen...</p>
<p v-else-if="error">Laden fehlgeschlagen: {{ error }}</p>
<div v-else-if="!article">Artikel existiert nicht</div>
<article v-else>
<h1>{{ article.title }}</h1>
<div v-html="article.content"></div>
</article>
</div>
</template>
Hinweis: Wenn der Benutzer direkt auf die URL der Detailseite zugreift (z. B. durch Aktualisieren der Seite), ruft usePosts die Daten erneut ab. Da die Startseite und die Detailseite unterschiedliche Komponenteninstanzen sind, führt jeder Aufruf von usePosts den fetch in onMounted aus, und die Daten werden nicht geteilt. Wenn Sie dieselben Daten seitenübergreifend teilen müssen, können Sie den State in einen Pinia-Store heben oder ihn einmal in einer Komponente der obersten Ebene aufrufen und über provide / inject weitergeben.
useDarkMode — Dunkelmodus mit einer Zeile Code umschalten
Die Kernidee des Dunkelmodus: auf derhtmltag, füge eine Klasse hinzu, dann verwende CSS-Variablen, um Farben zu steuern.
Die Auswahl des Benutzers wird in localStorage gespeichert und beim nächsten Besuch automatisch wiederhergestellt.
Beispiel
import { ref, watchEffect } from 'vue'
export function useDarkMode() {
// Lese die vorherige Einstellung des Benutzers aus localStorage (Standard: hell, falls keine vorhanden)
const saved = localStorage.getItem('blog-theme')
const isDark = ref(saved === 'dark')
// Theme auf DOM anwenden
function applyTheme(dark) {
if (dark) {
document.documentElement.classList.add('dark')
localStorage.setItem('blog-theme', 'dark')
} else {
document.documentElement.classList.remove('dark')
localStorage.setItem('blog-theme', 'light')
}
}
// watchEffect wird einmal sofort ausgeführt, um das anfängliche Theme anzuwenden
// Danach, wann immer sich isDark ändert, automatisch mit DOM synchronisieren
watchEffect(() => {
applyTheme(isDark.value)
})
// Dunkelmodus umschalten
function toggleDark() {
isDark.value = !isDark.value
}
return { isDark, toggleDark }
}
Um es in NavBar zu verwenden, sind nur zwei Zeilen erforderlich:
Beispiel
<script setup>
import { useDarkMode } from '../composables/useDarkMode.js'
const { isDark, toggleDark } = useDarkMode()
</script>
<template>
<header class="navbar">
<a href="/" class="logo">BEISPIEL Blog</a>
<nav>
<a href="/">Startseite</a>
<button class="theme-btn" @click="toggleDark">
{{ isDark ? '☀`Hell' : '☾ Dunkel' }}`
</button>
</nav>
</header>
</template>
Mit globalen CSS-Variablen kombinieren, um den Theme-Wechsel umzusetzen
Beispiel
/* Heller Modus (Standard) */
:root {
--bg-primary: #f5f5f5;
--bg-card: #ffffff;
--text-primary: #333333;
--text-secondary:#666666;
--border-color: #eeeeee;
}
/* Dunkelmodus (wird angewendet, wenn das html-Tag die Klasse .dark hat) */
html.dark {
--bg-primary: #1a1a2e;
--bg-card: #16213e;
--text-primary: #e0e0e0;
--text-secondary:#a0a0a0;
--border-color: #2a2a4a;
}
/* Referenzieren Sie diese Variablen in Komponenten; beim Wechseln von Themes muss kein Komponenten-Code geändert werden */
body {
background: var(--bg-primary);
color: var(--text-primary);
}
.card {
background: var(--bg-card);
border: 1px solid var(--border-color);
}
Zu diesem Zeitpunkt schaltet ein Klick auf die Umschaltfläche die Farben der gesamten Website sofort um, und Ihre Auswahl wird nach dem Aktualisieren beibehalten.
Vergleich mit Vue2 Mixin
| Merkmal | Mixin(Vue2) | Composable(Vue3) |
|---|---|---|
| Datenquelle | Undurchsichtig, Sie wissen nicht, woher die Eigenschaften stammen | Explizit per Destrukturierung aus dem Rückgabewert erhalten, die Quelle ist auf einen Blick ersichtlich |
| Namenskonflikt | Gleichnamige Eigenschaften aus mehreren Mixins überschreiben sich gegenseitig | Sie legen die Variablennamen fest, es gibt keine Konflikte |
| Typinferenz | Schlechte TypeScript-Unterstützung | Native TypeScript-Unterstützung, automatische Typinferenz |
| Logikorganisation | Nach Optionen kategorisiert (data/methods/computed) | Nach Funktionalität kategorisiert, ein Composable pro Datei |
| Logikwiederverwendung | Mixins sind schwer zu kombinieren und neigen dazu, implizite Abhängigkeiten zu erzeugen | Composables können sich gegenseitig aufrufen, flexible Komposition |