Ein erster Blick auf die Vue 3 Composition API in der Praxis

Avatar of Mateusz Rybczonek
Mateusz Rybczonek am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

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/*.js und 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/*.js und 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.