Komplexe Webanwendungen mit Vue.js, Teil 2
Seite 2: State Management mit Vuex
Die Grundidee hinter State Management Patterns ist das zentrale Verwalten von Zustandsinformationen in der Anwendung. Anstatt dass verschiedene Komponenten einen Teil der Daten redundant besitzen, gibt es nur noch einen Ort, auf den alle Teile der Anwendung lesend zugreifen können. Die Veränderung der Daten ist durch ein definiertes Vorgehen klar geregelt.
Das hat sich für komplexe Anwendungen bewährt. Es bedeutet zwar initial mehr Aufwand und erfordert das Erlernen neuer Konzepte, aber dafür ist die Anwendung unabhängig von ihrer Größe klar strukturiert und leicht verständlich. Es gibt nur eine zentrale Wahrheit über den Zustand der Applikation. Das vereinfacht die Fehlersuche. Da Zustandsänderungen nur über eine Schnittstelle der State Management Library möglich sind, können Debugging-Tools jede Veränderung aufzeichnen. Es ist genau nachvollziehbar, welche Veränderungen in welcher Reihenfolge erfolgt sind.
Zusätzlich zu den genannten Vorteilen führt der Einsatz einer State Management Library zu schlankeren Komponenten. Typischerweise befasst sich ein erheblicher Teil der Logik mit der Veränderung des Zustands. Den überwiegenden Teil können Entwickler aus den Komponenten in das State Management übernehmen. Die Komponenten sorgen lediglich für das Anstoßen der Veränderungen. Sie selbst bleiben dadurch kurz, gut lesbar und sind von der zentralen Logik entkoppelt. Es gibt also gute Gründe, für größere Anwendungen ein zentrales State Management einzuführen.
Für Vue.js ist die Standard-Implementierung Vuex. Sie ist eine vom Vue-Core-Team entwickelte, separat zu installierende Bibliothek. Sie implementiert das Flux-Pattern und überträgt es auf die Vue-Konzepte. Im Gegensatz zu Redux nutzt Vuex daher nicht das Prinzip von Immutability, da die Änderungsverfolgung von Vue.js nicht darauf basiert. Immutablility besagt, dass das Verändern von Objekten nicht erlaubt ist und Änderungen ein neues Objekt erfordern. Es ist zwar denkbar, andere State-Management-Bibliotheken mit Vue.js einzusetzen, aber Vuex ist die erste Wahl und am besten integriert.
Das Anlegen eines Vue.js-Projekts mit Vuex per CLI erzeugt die Datei store.js:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {},
mutations: {},
actions: {}
});
Vue.use gibt Vuex als Plug-in bekannt. Es folgt die Definition des Store, der im Beispiel nur drei leere Objekte für state, mutations und actions enthält. Um den Store nicht in allen Komponenten einzeln importieren zu müssen, können Entwickler ihn in main.js dem Vue-Konstruktor übergeben. Er steht dann automatisch in den Komponenten unter this.$store zur Verfügung.
Vuex-State
Den Zustand der Anwendung legt man im State-Objekt im Store ab. Es ist möglich, das Objekt beliebig tief zu schachteln. Es gibt nur einen Store und nur ein State-Objekt in einer Anwendung, aber Vuex unterstützt auch eine Modularisierung des State-Objekts. Der folgende Code zeigt ein einfaches Beispiel für ein State-Objekt:
{
languages: [
{ name: "JavaScript", created: 1995 },
{ name: "Rust", created: 2010 },
{ name: "Java", created: 1995 },
{ name: "Python", created: 1991 }
]
}
Der Zugriff innerhalb einer Komponente funktioniert so:
export default {
computed: {
languages() {
return this.$store.state.languages
}
}
}
Er erfolgt innerhalb einer Computed Property, damit Vue.js den Wert bei Veränderungen aktuell hält. Dadurch können alle benötigten Teile des globalen Zustands in die Komponente übertragen und in den Templates wie jedes lokale Attribut verwendet werden.
Wenn die Zustandsinformationen als Basis für weitere Operationen dienen, könnte das lokal innerhalb einer weiteren Computed Property erfolgen. Da jedoch weitere Komponenten die Information ebenfalls benötigen könnten, ist es besser, die Berechnungslogik als Teil des Stores zu realisieren. In Vuex funktioniert das mit Getter-Funktionen.
Im Beispiel könnte eine Liste der Programmiersprachen, sortiert nach Veröffentlichungsjahr, interessant sein:
export default new Vuex.Store({
state: {
// ...
},
getters: {
languagesByYear: state => {
return state.languages.sort(
(lang1, lang2) => lang1.created - lang2.created
);
}
}
});
Die Referenzierung der Getter-Funktionen innerhalb der Komponenten erfolgt ĂĽber das getters-Attribut:
computed: {
languagesByYear() {
return this.$store.getters.languagesByYear
}
}
Getter-Funktionen können auch Parameter erhalten. Um zum Beispiel dynamisch nur die Sprachen ab einem bestimmten Jahr zurückzugeben, ist folgende Variante denkbar:
getters: {
languagesByYear: state => fromYear => {
let languagesFromYear = fromYear
? state.languages.filter(lang => lang.created >= fromYear)
: state.languages;
return languagesFromYear.sort(
(lang1, lang2) => lang1.created - lang2.created
);
}
}
Anstatt direkt ein Ergebnis zurĂĽckzugeben, gibt die Getter-Funktion eine Funktion zurĂĽck. Daher muss der Zugriff in der Komponente als Funktionsaufruf mit dem Parameter erfolgen:
this.$store.getters.languagesByYear(1992)