Ich hatte kürzlich die Gelegenheit, die neue Vue Composition API in einem echten Projekt auszuprobieren, um zu sehen, wo sie nützlich sein könnte und wie wir sie in Zukunft nutzen könnten.
Bis jetzt haben wir beim Erstellen einer neuen Komponente die Options API verwendet. Diese API zwang uns, den Code der Komponente nach Optionen zu trennen, was bedeutete, dass wir alle reaktiven Daten an einem Ort (data), alle berechneten Eigenschaften an einem Ort (computed), alle Methoden an einem Ort (methods) und so weiter haben mussten.
Das ist zwar praktisch und lesbar für kleinere Komponenten, wird aber schmerzhaft, wenn die Komponente komplizierter wird und mehrere Funktionalitäten behandelt. Normalerweise enthält die Logik, die sich auf eine bestimmte Funktionalität bezieht, einige reaktive Daten, eine berechnete Eigenschaft, eine Methode oder einige davon; manchmal beinhaltet sie auch die Verwendung von Komponent-Lifecycle-Hooks. Das führt dazu, dass Sie beim Arbeiten an einem einzelnen logischen Anliegen ständig zwischen verschiedenen Optionen im Code springen müssen.
Das andere Problem, dem Sie bei der Arbeit mit Vue möglicherweise begegnet sind, ist die Extraktion gemeinsamer Logik, die von mehreren Komponenten wiederverwendet werden kann. Vue bietet bereits einige Optionen dafür, aber alle haben ihre eigenen Nachteile (z.B. Mixins und Scoped Slots).
Die Composition API bringt eine neue Art, Komponenten zu erstellen, Code zu trennen und wiederverwendbare Code-Teile zu extrahieren.
Beginnen wir mit der Code-Komposition innerhalb einer Komponente.
Code-Komposition
Stellen Sie sich eine Hauptkomponente vor, die einige Dinge für Ihre gesamte Vue-App einrichtet (wie Layouts in Nuxt). Sie kümmert sich um Folgendes:
- Festlegen der Sprache
- Prüfen, ob der Benutzer noch authentifiziert ist, und ihn gegebenenfalls umleiten
- Verhindern, dass der Benutzer die App zu oft neu lädt
- Verfolgen der Benutzeraktivität und Reagieren, wenn der Benutzer für einen bestimmten Zeitraum inaktiv ist
- Abhören eines Ereignisses über EventBus (oder window object event)
Dies sind nur einige der Dinge, die die Komponente tun kann. Sie können sich wahrscheinlich eine komplexere Komponente vorstellen, aber dies dient dem Zweck dieses Beispiels. Der Lesbarkeit halber verwende ich nur die Namen der Props ohne die tatsächliche Implementierung.
So würde die Komponente mit der Options API aussehen
<template>
<div id="app">
...
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
userActivityTimeout: null,
lastUserActivityAt: null,
reloadCount: 0
}
},
computed: {
isAuthenticated() {...}
locale() {...}
},
watch: {
locale(value) {...},
isAuthenticated(value) {...}
},
async created() {
const initialLocale = localStorage.getItem('locale')
await this.loadLocaleAsync(initialLocale)
},
mounted() {
EventBus.$on(MY_EVENT, this.handleMyEvent)
this.setReloadCount()
this.blockReload()
this.activateActivityTracker()
this.resetActivityTimeout()
},
beforeDestroy() {
this.deactivateActivityTracker()
clearTimeout(this.userActivityTimeout)
EventBus.$off(MY_EVENT, this.handleMyEvent)
},
methods: {
activateActivityTracker() {...},
blockReload() {...},
deactivateActivityTracker() {...},
handleMyEvent() {...},
async loadLocaleAsync(selectedLocale) {...}
redirectUser() {...}
resetActivityTimeout() {...},
setI18nLocale(locale) {...},
setReloadCount() {...},
userActivityThrottler() {...},
}
}
</script>
Wie Sie sehen können, enthält jede Option Teile aller Funktionalitäten. Es gibt keine klare Trennung zwischen ihnen, was den Code schwer lesbar macht, besonders wenn Sie nicht derjenige sind, der ihn geschrieben hat, und ihn zum ersten Mal betrachten. Es ist sehr schwer herauszufinden, welche Methode von welcher Funktionalität verwendet wird.
Betrachten wir es noch einmal, aber identifizieren wir die logischen Anliegen als Kommentare. Das wären:
- Aktivitätstracker
- Neulade-Blocker
- Authentifizierungsprüfung
- Sprache
- Event Bus Registrierung
<template>
<div id="app">
...
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
userActivityTimeout: null, // Activity tracker
lastUserActivityAt: null, // Activity tracker
reloadCount: 0 // Reload blocker
}
},
computed: {
isAuthenticated() {...} // Authentication check
locale() {...} // Locale
},
watch: {
locale(value) {...},
isAuthenticated(value) {...} // Authentication check
},
async created() {
const initialLocale = localStorage.getItem('locale') // Locale
await this.loadLocaleAsync(initialLocale) // Locale
},
mounted() {
EventBus.$on(MY_EVENT, this.handleMyEvent) // Event Bus registration
this.setReloadCount() // Reload blocker
this.blockReload() // Reload blocker
this.activateActivityTracker() // Activity tracker
this.resetActivityTimeout() // Activity tracker
},
beforeDestroy() {
this.deactivateActivityTracker() // Activity tracker
clearTimeout(this.userActivityTimeout) // Activity tracker
EventBus.$off(MY_EVENT, this.handleMyEvent) // Event Bus registration
},
methods: {
activateActivityTracker() {...}, // Activity tracker
blockReload() {...}, // Reload blocker
deactivateActivityTracker() {...}, // Activity tracker
handleMyEvent() {...}, // Event Bus registration
async loadLocaleAsync(selectedLocale) {...} // Locale
redirectUser() {...} // Authentication check
resetActivityTimeout() {...}, // Activity tracker
setI18nLocale(locale) {...}, // Locale
setReloadCount() {...}, // Reload blocker
userActivityThrottler() {...}, // Activity tracker
}
}
</script>
Sehen Sie, wie schwer es ist, all das zu entwirren? 🙂
Stellen Sie sich nun vor, Sie müssen eine Änderung an einer Funktionalität vornehmen (z. B. an der Logik des Aktivitätstrackers). Sie müssen nicht nur wissen, welche Elemente mit dieser Logik zusammenhängen, sondern selbst wenn Sie es wissen, müssen Sie immer noch zwischen verschiedenen Komponentenoptionen auf und ab springen.
Verwenden wir die Composition API, um den Code nach logischen Anliegen zu trennen. Dazu erstellen wir für jede Logik, die sich auf eine bestimmte Funktionalität bezieht, eine einzelne Funktion. Dies nennen wir eine Composition Function.
// Activity tracking logic
function useActivityTracker() {
const userActivityTimeout = ref(null)
const lastUserActivityAt = ref(null)
function activateActivityTracker() {...}
function deactivateActivityTracker() {...}
function resetActivityTimeout() {...}
function userActivityThrottler() {...}
onBeforeMount(() => {
activateActivityTracker()
resetActivityTimeout()
})
onUnmounted(() => {
deactivateActivityTracker()
clearTimeout(userActivityTimeout.value)
})
}
// Reload blocking logic
function useReloadBlocker(context) {
const reloadCount = ref(null)
function blockReload() {...}
function setReloadCount() {...}
onMounted(() => {
setReloadCount()
blockReload()
})
}
// Locale logic
function useLocale(context) {
async function loadLocaleAsync(selectedLocale) {...}
function setI18nLocale(locale) {...}
watch(() => {
const locale = ...
loadLocaleAsync(locale)
})
// No need for a 'created' hook, all logic that runs in setup function is placed between beforeCreate and created hooks
const initialLocale = localStorage.getItem('locale')
loadLocaleAsync(initialLocale)
}
// Event bus listener registration
import EventBus from '@/event-bus'
function useEventBusListener(eventName, handler) {
onMounted(() => EventBus.$on(eventName, handler))
onUnmounted(() => EventBus.$off(eventName, handler))
}
Wie Sie sehen können, können wir reaktive Daten (ref / reactive), berechnete Props, Methoden (einfache Funktionen), Watcher (watch) und Lifecycle-Hooks (onMounted / onUnmounted) deklarieren. Im Grunde alles, was Sie normalerweise in einer Komponente verwenden.
Wir haben zwei Möglichkeiten, wo wir den Code aufbewahren können. Wir können ihn innerhalb der Komponente belassen oder in eine separate Datei extrahieren. Da die Composition API noch nicht offiziell vorhanden ist, gibt es keine Best Practices oder Regeln, wie damit umzugehen ist. So wie ich es sehe: Wenn die Logik eng an eine bestimmte Komponente gekoppelt ist (d. h. sie wird nirgendwo anders wiederverwendet) und sie nicht ohne die Komponente selbst existieren kann, schlage ich vor, sie innerhalb der Komponente zu belassen. Auf der anderen Seite, wenn es sich um eine allgemeine Funktionalität handelt, die wahrscheinlich wiederverwendet wird, schlage ich vor, sie in eine separate Datei zu extrahieren. Wenn wir sie jedoch in einer separaten Datei behalten wollen, müssen wir daran denken, die Funktion aus der Datei zu exportieren und in unserer Komponente zu importieren.
So wird unsere Komponente mit den neu erstellten Composition Functions aussehen
<template>
<div id="app">
</div>
</template>
<script>
export default {
name: 'App',
setup(props, context) {
useEventBusListener(MY_EVENT, handleMyEvent)
useActivityTracker()
useReloadBlocker(context)
useLocale(context)
const isAuthenticated = computed(() => ...)
watch(() => {
if (!isAuthenticated) {...}
})
function handleMyEvent() {...},
function useLocale() {...}
function useActivityTracker() {...}
function useEventBusListener() {...}
function useReloadBlocker() {...}
}
}
</script>
Das gibt uns eine einzige Funktion für jedes logische Anliegen. Wenn wir ein bestimmtes Anliegen nutzen wollen, müssen wir die entsprechende Composition Function in der neuen setup-Funktion aufrufen.
Stellen Sie sich noch einmal vor, Sie müssen eine Änderung an der Logik des Aktivitätstrackers vornehmen. Alles, was mit dieser Funktionalität zu tun hat, befindet sich in der Funktion useActivityTracker. Jetzt wissen Sie sofort, wo Sie suchen müssen, und können zu der richtigen Stelle springen, um alle zusammenhängenden Codefragmente zu sehen. Wundervoll!
Extrahieren von wiederverwendbaren Codefragmenten
In unserem Fall sieht die Registrierung des Event Bus Listeners wie ein Codefragment aus, das wir in jeder Komponente verwenden können, die Ereignisse auf dem Event Bus abhört.
Wie bereits erwähnt, können wir die Logik, die sich auf eine bestimmte Funktionalität bezieht, in einer separaten Datei aufbewahren. Verschieben wir die Einrichtung unseres Event Bus Listeners in eine separate Datei.
// composables/useEventBusListener.js
import EventBus from '@/event-bus'
export function useEventBusListener(eventName, handler) {
onMounted(() => EventBus.$on(eventName, handler))
onUnmounted(() => EventBus.$off(eventName, handler))
}
Um sie in einer Komponente zu verwenden, müssen wir sicherstellen, dass wir unsere Funktion exportieren (benannt oder Standard) und sie in einer Komponente importieren.
<template>
<div id="app">
...
</div>
</template>
<script>
import { useEventBusListener } from '@/composables/useEventBusListener'
export default {
name: 'MyComponent',
setup(props, context) {
useEventBusListener(MY_EVENT, myEventHandled)
useEventBusListener(ANOTHER_EVENT, myAnotherHandled)
}
}
</script>
Das war's! Wir können dies nun in jeder benötigten Komponente verwenden.
Zusammenfassung
Es gibt eine laufende Diskussion über die Composition API. Dieser Beitrag hat nicht die Absicht, eine Seite der Diskussion zu fördern. Es geht vielmehr darum, zu zeigen, wann sie nützlich sein könnte und in welchen Fällen sie einen Mehrwert bringt.
Ich denke, es ist immer einfacher, das Konzept anhand eines realen Beispiels wie dem oben genannten zu verstehen. Es gibt weitere Anwendungsfälle, und je mehr Sie die neue API verwenden, desto mehr Muster werden Sie erkennen. Dieser Beitrag sind lediglich einige grundlegende Muster für den Einstieg.
Gehen wir die präsentierten Anwendungsfälle noch einmal durch und sehen wir, wo die Composition API nützlich sein kann
Allgemeine Funktionen, die eigenständig und ohne enge Kopplung mit einer bestimmten Komponente existieren können
- Die gesamte Logik, die sich auf ein bestimmtes Feature bezieht, in einer Datei
- Speichern Sie sie in
@/composables/*.jsund importieren Sie sie in Komponenten - Beispiele: Aktivitätstracker, Neulade-Blocker und Sprache
Wiederverwendbare Funktionen, die in mehreren Komponenten verwendet werden
- Die gesamte Logik, die sich auf ein bestimmtes Feature bezieht, in einer Datei
- Speichern Sie sie in
@/composables/*.jsund importieren Sie sie in Komponenten - Beispiele: Event Bus Listener-Registrierung, Window-Event-Registrierung, gemeinsame Animationslogik, gemeinsame Bibliotheksnutzung
Codeorganisation innerhalb einer Komponente
- Die gesamte Logik, die sich auf ein bestimmtes Feature bezieht, in einer Funktion
- Bewahren Sie den Code in einer Composition Function innerhalb der Komponente auf
- Der Code, der sich auf dasselbe logische Anliegen bezieht, befindet sich am selben Ort (d. h. es ist nicht erforderlich, zwischen Daten, Computed, Methoden, Lifecycle Hooks usw. zu springen)
Denken Sie daran: Dies ist alles noch in Arbeit!
Die Vue Composition API befindet sich derzeit noch in der Entwicklung und kann sich zukünftigen Änderungen unterwerfen. Nichts von dem, was in den obigen Beispielen erwähnt wird, ist sicher, und sowohl Syntax als auch Anwendungsfälle können sich ändern. Sie ist für die Veröffentlichung mit Vue Version 3.0 vorgesehen. In der Zwischenzeit können Sie sich vue-use-web ansehen, eine Sammlung von Composition Functions, die voraussichtlich in Vue 3 enthalten sein werden, aber mit der Composition API in Vue 2 verwendet werden können.
Wenn Sie mit der neuen API experimentieren möchten, können Sie die @vue/composition library verwenden.
Toller Artikel und ein gutes Beispiel dafür, wo die Composition API sehr nützlich sein kann.
Ich habe jedoch eine Frage: Im zusammengesetzten Beispiel rufen Sie dieselben Funktionen auf und deklarieren sie dann. Warum ist das so?
Hier ist die Erklärung
– Das Deklarieren einer Funktion entspricht dem Platzieren in der Methoden-Eigenschaft der Options API
– Das direkte Aufrufen einer Methode in
setupentspricht dem Aufrufen einer Methode nach dembeforeCreateHook und vor demcreatedHookWir deklarieren also die Methoden mithilfe von Funktionen und rufen sie dann auf, wenn die Komponente instanziiert wird.
An diesem Punkt des Artikels importieren wir noch nichts, alles befindet sich innerhalb der Komponente. Sie könnten es aus einer externen Datei importieren und es einfach in
setupaufrufen, wie am Ende des Artikels mituseEventBusListener.Sie werden nicht importiert, müssen also deklariert werden. Die Möglichkeit, sie in eine andere Datei zu verschieben, ohne die Nachteile von Mixins, macht es so leistungsfähig.
Ahhh…. Ich dachte, die Funktionen wären in separaten Dateien, aber sie befinden sich nur in der Komponente.
Danke! Das ergibt Sinn!
Dies ist das beste Beispiel für die Composition API, das ich bisher gesehen habe. Das ist mein "AHA!"-Moment bezüglich der Composition API.
Dieser Artikel hat die eine Sache, die mich bei meiner Erfahrung mit Vue bisher beunruhigt hat, klar artikuliert – danke dafür. Ich freue mich wirklich darauf, diese Composition API jetzt zu nutzen.
Danke, Sie haben mich dazu gebracht, die neue Composition API zu lieben.
Das Beispiel ist sehr nützlich und 100% relevant: viel Code entfernen und in Dateien aufteilen, um den Code in einer Vue-Komponente verwenden zu können.
Ja, jetzt denke ich, es ist ein MUSS.
Nochmal vielen Dank.