Das Verwalten von Zuständen ist nichts Neues in der Softwareentwicklung, aber es ist noch relativ neu für die Entwicklung von Software in JavaScript. Traditionell würden wir den Zustand im DOM selbst speichern oder ihn sogar einem globalen Objekt im Fenster zuweisen. Mittlerweile werden wir mit Wahlmöglichkeiten für Bibliotheken und Frameworks verwöhnt, die uns dabei helfen. Bibliotheken wie Redux, MobX und Vuex machen die Verwaltung von standortübergreifendem Zustand fast trivial. Das ist großartig für die Belastbarkeit einer Anwendung und funktioniert sehr gut mit einem zustandsorientierten, reaktiven Framework wie React oder Vue.
Wie funktionieren diese Bibliotheken also? Was wäre nötig, um eine eigene zu schreiben? Es stellt sich heraus, dass es ziemlich einfach ist und eine Gelegenheit bietet, einige sehr gängige Muster zu erlernen und auch einige nützliche moderne APIs kennenzulernen, die uns zur Verfügung stehen.
Bevor wir beginnen, wird empfohlen, dass Sie über fortgeschrittene JavaScript-Kenntnisse verfügen. Sie sollten sich mit Datentypen auskennen und idealerweise einige modernere ES6+-JavaScript-Funktionen beherrschen. Wenn nicht, haben wir Sie im Griff. Es ist auch erwähnenswert, dass ich nicht sage, dass Sie Redux oder MobX durch dies ersetzen sollten. Wir arbeiten an einem kleinen Projekt, um gemeinsam zu lernen, und es könnte durchaus eine kleine Anwendung antreiben, wenn Sie die Größe Ihrer JavaScript-Payload im Auge behalten.
Erste Schritte
Bevor wir uns in den Code stürzen, schauen wir uns an, was wir bauen. Es ist eine „Fertig-Liste“, die die Dinge zusammenzählt, die Sie heute erreicht haben. Sie aktualisiert verschiedene Elemente der Benutzeroberfläche wie von Zauberhand – und das ganz ohne Framework-Abhängigkeiten. Das ist aber nicht die eigentliche Magie. Hinter den Kulissen haben wir ein kleines Zustandssystem, das wartet, Anweisungen entgegennimmt und eine einzige Quelle der Wahrheit auf vorhersagbare Weise verwaltet.
Ziemlich cool, oder? Lassen Sie uns zuerst ein paar organisatorische Dinge erledigen. Ich habe ein kleines Boilerplate vorbereitet, damit dieses Tutorial kurz und bündig bleibt. Das Erste, was Sie tun müssen, ist entweder, es von GitHub zu klonen, oder ein ZIP-Archiv herunterzuladen und es zu entpacken.
Nachdem Sie das erledigt haben, müssen Sie es auf einem lokalen Webserver ausführen. Ich verwende für solche Dinge gerne ein Paket namens http-server, aber Sie können jedes beliebige verwenden. Wenn Sie es lokal ausführen, sollten Sie etwas sehen, das so aussieht:

Struktur einrichten
Öffnen Sie den Stammordner in Ihrem bevorzugten Texteditor. Dieses Mal ist der Stammordner für mich
~/Documents/Projects/vanilla-js-state-management-boilerplate/
Sie sollten eine Struktur sehen, die ein wenig so aussieht:
/src
├── .eslintrc
├── .gitignore
├── LICENSE
└── README.md
Pub/Sub
Öffnen Sie als Nächstes den Ordner src und dann den Ordner js darin. Erstellen Sie einen neuen Ordner namens lib. Darin erstellen Sie eine neue Datei namens pubsub.js.
Die Struktur Ihres js-Verzeichnisses sollte dann etwa so aussehen:
/js
├── lib
└── pubsub.js
Öffnen Sie pubsub.js, denn wir werden ein kleines Pub/Sub-Muster erstellen, das kurz für „Publish/Subscribe“ steht. Wir erstellen die Funktionalität, die es anderen Teilen unserer Anwendung ermöglicht, sich für benannte Ereignisse anzumelden. Ein anderer Teil der Anwendung kann dann diese Ereignisse veröffentlichen, oft mit einer Art relevantem Payload.
Pub/Sub ist manchmal schwer zu verstehen, also wie wäre es mit einer Analogie? Stellen Sie sich vor, Sie arbeiten in einem Restaurant und Ihre Kunden haben eine Vorspeise und einen Hauptgang. Wenn Sie jemals in einer Küche gearbeitet haben, wissen Sie, dass, wenn der Kellner die Vorspeisen abräumt, er den Köchen mitteilt, welche Tische ihre Vorspeisen abgeräumt haben. Das ist ein Signal, mit den Hauptgängen für diesen Tisch zu beginnen. In einer großen Küche gibt es einige Köche, die wahrscheinlich an verschiedenen Gerichten arbeiten. Sie sind alle abonniert auf das Signal des Kellners, dass die Kunden ihre Vorspeisen beendet haben, damit sie ihre Funktion ausführen können, nämlich die Zubereitung des Hauptgangs. Sie haben also mehrere Köche, die auf dasselbe Signal (benanntes Ereignis) warten, um verschiedene Funktionen (Callbacks) für einander auszuführen.

Hoffentlich hilft es, es so zu betrachten, damit es Sinn ergibt. Machen wir weiter!
Das PubSub-Muster durchläuft alle Abonnements und löst deren Callbacks mit diesem Payload aus. Es ist eine großartige Möglichkeit, einen ziemlich eleganten reaktiven Fluss für Ihre App zu erstellen, und wir können das mit nur wenigen Codezeilen tun.
Fügen Sie Folgendes zu pubsub.js hinzu:
export default class PubSub {
constructor() {
this.events = {};
}
}
Was wir dort haben, ist eine brandneue Klasse und wir setzen this.events standardmäßig als leeres Objekt. Das this.events-Objekt wird unsere benannten Ereignisse enthalten.
Fügen Sie nach der schließenden Klammer des Konstruktors Folgendes hinzu:
subscribe(event, callback) {
let self = this;
if(!self.events.hasOwnProperty(event)) {
self.events[event] = [];
}
return self.events[event].push(callback);
}
Dies ist unsere Subscribe-Methode. Sie übergeben eine Zeichenkette event, den eindeutigen Namen des Ereignisses, und eine Callback-Funktion. Wenn noch kein passendes Ereignis in unserer events-Sammlung vorhanden ist, erstellen wir es mit einem leeren Array, damit wir es später nicht überprüfen müssen. Dann pushen wir den Callback in diese Sammlung. Wenn es bereits existierte, würde diese Methode nichts weiter tun. Wir geben die Länge der Events-Sammlung zurück, da es für jemanden nützlich sein könnte zu wissen, wie viele Ereignisse existieren.
Jetzt, da wir unsere Subscribe-Methode haben, raten Sie mal, was als Nächstes kommt? Sie wissen es: die publish-Methode. Fügen Sie Folgendes nach Ihrer Subscribe-Methode hinzu:
publish(event, data = {}) {
let self = this;
if(!self.events.hasOwnProperty(event)) {
return [];
}
return self.events[event].map(callback => callback(data));
}
Diese Methode prüft zunächst, ob das übergebene Ereignis in unserer Sammlung vorhanden ist. Wenn nicht, geben wir ein leeres Array zurück. Kein Problem. Wenn ein Ereignis vorhanden ist, durchlaufen wir jeden gespeicherten Callback und übergeben die Daten daran. Wenn keine Callbacks vorhanden sind (was nie der Fall sein sollte), ist alles in Ordnung, da wir dieses Ereignis in der subscribe-Methode mit einem leeren Array erstellt haben.
Das war's für PubSub. Gehen wir zum nächsten Teil!
Das zentrale Store-Objekt
Nachdem wir nun unser Pub/Sub-Modul haben, haben wir unsere einzige Abhängigkeit für das Herzstück dieser kleinen Anwendung: das Store. Wir werden uns nun daran machen, es auszubauen.
Lassen Sie uns zunächst umreißen, was dies tut.
Das Store ist unser zentrales Objekt. Jedes Mal, wenn Sie @import store from '../lib/store.js sehen, importieren Sie das Objekt, das wir schreiben werden. Es wird ein state-Objekt enthalten, das wiederum unseren Anwendungszustand enthält, eine commit-Methode, die unsere >Mutations aufruft, und schließlich eine dispatch-Funktion, die unsere Actions aufruft. Unter anderem und im Kern des Store-Objekts wird ein Proxy-basiertes System Zustandsänderungen mit unserem PubSub-Modul überwachen und übertragen.
Beginnen Sie damit, in Ihrem js-Verzeichnis ein neues Verzeichnis namens store zu erstellen. Erstellen Sie darin eine neue Datei namens store.js. Ihr js-Verzeichnis sollte dann so aussehen:
/js
└── lib
└── pubsub.js
└──store
└── store.js
Öffnen Sie store.js und importieren Sie unser Pub/Sub-Modul. Tun Sie dies, indem Sie ganz oben in der Datei Folgendes hinzufügen:
import PubSub from '../lib/pubsub.js';
Für diejenigen, die regelmäßig mit ES6 arbeiten, wird dies sehr bekannt vorkommen. Das Ausführen von Code dieser Art ohne Bundler ist jedoch wahrscheinlich weniger bekannt. Es gibt bereits eine ganze Menge Unterstützung für diesen Ansatz!
Als Nächstes beginnen wir mit dem Aufbau unseres Objekts. Direkt nach dem Import fügen Sie Folgendes zu store.js hinzu:
export default class Store {
constructor(params) {
let self = this;
}
}
Dies ist alles ziemlich selbsterklärend, also fügen wir das nächste Stück hinzu. Wir fügen Standardobjekte für state, actions und mutations hinzu. Wir fügen auch ein status-Element hinzu, das wir verwenden werden, um zu bestimmen, was das Objekt zu einem bestimmten Zeitpunkt tut. Dies kommt direkt nach let self = this;
self.actions = {};
self.mutations = {};
self.state = {};
self.status = 'resting';
Direkt danach erstellen wir eine neue PubSub-Instanz, die dem Store als events-Element zugeordnet wird:
self.events = new PubSub();
Als Nächstes durchsuchen wir das übergebene params-Objekt, um zu sehen, ob actions oder mutations übergeben wurden. Wenn das Store-Objekt instanziiert wird, können wir ein Datenobjekt übergeben. Darin kann eine Sammlung von actions und mutations enthalten sein, die den Datenfluss in unserem Store steuern. Der folgende Code kommt direkt nach der letzten Zeile, die Sie hinzugefügt haben:
if(params.hasOwnProperty('actions')) {
self.actions = params.actions;
}
if(params.hasOwnProperty('mutations')) {
self.mutations = params.mutations;
}
Das sind alle unsere Standardwerte und fast alle unsere potenziellen Parameter. Werfen wir einen Blick darauf, wie unser Store-Objekt alle Änderungen verfolgt. Dazu verwenden wir einen Proxy. Was der Proxy im Wesentlichen tut, ist, im Auftrag unseres State-Objekts zu arbeiten. Wenn wir eine get-Trap hinzufügen, können wir jedes Mal überwachen, wenn das Objekt nach Daten gefragt wird. Ähnlich können wir mit einer set-Trap Änderungen an dem Objekt verfolgen. Das ist der Hauptteil, der uns heute interessiert. Fügen Sie Folgendes direkt nach den zuletzt hinzugefügten Zeilen hinzu, und wir werden besprechen, was es tut:
self.state = new Proxy((params.state || {}), {
set: function(state, key, value) {
state[key] = value;
console.log(`stateChange: ${key}: ${value}`);
self.events.publish('stateChange', self.state);
if(self.status !== 'mutation') {
console.warn(`You should use a mutation to set ${key}`);
}
self.status = 'resting';
return true;
}
});
Was hier passiert, ist, dass wir die set-Operationen des State-Objekts abfangen. Das bedeutet, dass, wenn eine Mutation etwas wie state.name = 'Foo' ausführt, diese Trap sie abfängt, bevor sie gesetzt werden kann, und uns die Möglichkeit gibt, mit der Änderung zu arbeiten oder sie sogar vollständig abzulehnen. In unserem Fall setzen wir die Änderung und protokollieren sie dann in der Konsole. Dann veröffentlichen wir ein stateChange-Ereignis mit unserem PubSub-Modul. Alles, was für dieses Ereignis abonniert ist, wird aufgerufen. Schließlich überprüfen wir den Status des Store. Wenn derzeit keine mutation läuft, bedeutet dies wahrscheinlich, dass der Zustand manuell aktualisiert wurde. Wir fügen dafür eine kleine Warnung in der Konsole hinzu, um dem Entwickler einen kleinen Rüffel zu geben.
Da passiert viel, aber ich hoffe, Sie beginnen zu sehen, wie das alles zusammenkommt und, was wichtig ist, wie wir dank Proxy und Pub/Sub den Zustand zentral verwalten können.
Dispatch und Commit
Nachdem wir nun unsere Kernelemente des Store hinzugefügt haben, fügen wir zwei Methoden hinzu. Eine, die unsere actions aufruft und dispatch heißt, und eine andere, die unsere mutations aufruft und commit heißt. Beginnen wir mit dispatch, indem wir diese Methode nach Ihrem constructor in store.js hinzufügen:
dispatch(actionKey, payload) {
let self = this;
if(typeof self.actions[actionKey] !== 'function') {
console.error(`Action "${actionKey} doesn't exist.`);
return false;
}
console.groupCollapsed(`ACTION: ${actionKey}`);
self.status = 'action';
self.actions[actionKey](self, payload);
console.groupEnd();
return true;
}
Der Prozess hier ist: Suche nach einer Aktion und, wenn sie existiert, setze einen Status und rufe die Aktion auf, während du eine Logging-Gruppe erstellst, die all unsere Logs schön und ordentlich hält. Alles, was protokolliert wird (wie ein Mutations- oder Proxy-Log), wird in der Gruppe aufbewahrt, die wir definieren. Wenn keine Aktion gesetzt ist, wird ein Fehler protokolliert und das Programm beendet. Das war ziemlich unkompliziert, und die commit-Methode ist noch unkomplizierter.
Fügen Sie dies nach Ihrer dispatch-Methode hinzu:
commit(mutationKey, payload) {
let self = this;
if(typeof self.mutations[mutationKey] !== 'function') {
console.log(`Mutation "${mutationKey}" doesn't exist`);
return false;
}
self.status = 'mutation';
let newState = self.mutations[mutationKey](self.state, payload);
self.state = Object.assign(self.state, newState);
return true;
}
Diese Methode ist ziemlich ähnlich, aber lassen Sie uns den Prozess trotzdem durchgehen. Wenn die Mutation gefunden werden kann, führen wir sie aus und erhalten unseren neuen Zustand aus ihrem Rückgabewert. Dann nehmen wir diesen neuen Zustand und verschmelzen ihn mit unserem bestehenden Zustand, um eine aktuelle Version unseres Zustands zu erstellen.
Mit diesen hinzugefügten Methoden ist unser Store-Objekt so gut wie fertig. Sie könnten diese Anwendung jetzt tatsächlich modularisieren, wenn Sie wollten, da wir die meisten benötigten Teile hinzugefügt haben. Sie könnten auch einige Tests hinzufügen, um zu überprüfen, ob alles wie erwartet funktioniert. Aber ich werde Sie nicht so im Stich lassen. Lassen Sie uns alles dazu bringen, das zu tun, was wir uns vorgenommen haben, und mit unserer kleinen App fortfahren!
Erstellen einer Basiskomponente
Um mit unserem Store zu kommunizieren, haben wir drei Hauptbereiche, die sich unabhängig voneinander basierend auf dem darin gespeicherten Zustand aktualisieren. Wir werden eine Liste der übermittelten Elemente erstellen, eine visuelle Zählung dieser Elemente und eine weitere, die visuell verborgen ist und genauere Informationen für Screenreader enthält. Diese tun alle unterschiedliche Dinge, aber sie würden alle von etwas gemeinsam Genutztem profitieren, um ihren lokalen Zustand zu steuern. Wir werden eine Basisklassenkomponente erstellen!
Zuerst erstellen wir eine Datei. Gehen Sie im Verzeichnis lib und erstellen Sie eine Datei namens component.js. Der Pfad für mich ist
~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/lib/component.js
Sobald diese Datei erstellt ist, öffnen Sie sie und fügen Sie Folgendes hinzu:
import Store from '../store/store.js';
export default class Component {
constructor(props = {}) {
let self = this;
this.render = this.render || function() {};
if(props.store instanceof Store) {
props.store.events.subscribe('stateChange', () => self.render());
}
if(props.hasOwnProperty('element')) {
this.element = props.element;
}
}
}
Lassen Sie uns diesen Code-Chunk durchgehen. Zuerst importieren wir die Store-Klasse. Dies liegt nicht daran, dass wir eine Instanz davon wollen, sondern eher zur Überprüfung einer unserer Eigenschaften im constructor. Davon abgesehen, suchen wir im constructor nach einer Render-Methode. Wenn diese Component-Klasse die Elternklasse einer anderen Klasse ist, hat diese wahrscheinlich ihre eigene Methode für render festgelegt. Wenn keine Methode festgelegt ist, erstellen wir eine leere Methode, die verhindert, dass Dinge kaputtgehen.
Danach führen wir die Überprüfung gegen die Store-Klasse durch, wie oben erwähnt. Dies tun wir, um sicherzustellen, dass die store-Eigenschaft eine Store-Klasseninstanz ist, damit wir ihre Methoden und Eigenschaften sicher verwenden können. Davon abgesehen abonnieren wir das globale stateChange-Ereignis, damit unser Objekt reagieren kann. Dies ruft die render-Funktion jedes Mal auf, wenn sich der Zustand ändert.
Das ist alles, was wir für diese Klasse schreiben müssen. Sie wird als Elternklasse verwendet, die andere Klassen von extend werden. Lassen Sie uns mit diesen fortfahren!
Unsere Komponenten erstellen
Wie ich bereits sagte, müssen wir drei Komponenten erstellen, und sie alle werden die Basis-Component-Klasse extend. Beginnen wir mit der größten: der Liste der Elemente!
Erstellen Sie in Ihrem js-Verzeichnis einen neuen Ordner namens components und darin eine neue Datei namens list.js. Für mich ist der Pfad:
~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/components/list.js
Öffnen Sie diese Datei und fügen Sie diesen gesamten Codeblock dort ein:
import Component from '../lib/component.js';
import store from '../store/index.js';
export default class List extends Component {
constructor() {
super({
store,
element: document.querySelector('.js-items')
});
}
render() {
let self = this;
if(store.state.items.length === 0) {
self.element.innerHTML = `<p class="no-items">You've done nothing yet 😢</p>`;
return;
}
self.element.innerHTML = `
<ul class="app__items">
${store.state.items.map(item => {
return `
<li>${item}<button aria-label="Delete this item">×</button></li>
`
}).join('')}
</ul>
`;
self.element.querySelectorAll('button').forEach((button, index) => {
button.addEventListener('click', () => {
store.dispatch('clearItem', { index });
});
});
}
};
Ich hoffe, dieser Code ist nach dem, was wir in diesem Tutorial gelernt haben, ziemlich selbsterklärend, aber lassen Sie ihn uns trotzdem kurz durchgehen. Wir beginnen damit, unsere Store-Instanz an die Component-Elternklasse zu übergeben, die wir erweitern. Dies ist die Component-Klasse, die wir gerade geschrieben haben.
Danach deklarieren wir unsere Render-Methode, die jedes Mal aufgerufen wird, wenn das Pub/Sub-Ereignis stateChange auftritt. In dieser render-Methode geben wir entweder eine Liste von Elementen oder einen kleinen Hinweis aus, wenn keine Elemente vorhanden sind. Sie werden auch feststellen, dass jede Schaltfläche ein Ereignis daran angehängt hat und sie eine Aktion innerhalb unseres Stores dispatcht. Diese Aktion existiert noch nicht, aber dazu kommen wir bald.
Als Nächstes erstellen wir zwei weitere Dateien. Dies sind zwei neue Komponenten, aber sie sind winzig – also fügen wir nur etwas Code ein und machen weiter.
Erstellen Sie zuerst count.js in Ihrem component-Verzeichnis und fügen Sie Folgendes ein:
import Component from '../lib/component.js';
import store from '../store/index.js';
export default class Count extends Component {
constructor() {
super({
store,
element: document.querySelector('.js-count')
});
}
render() {
let suffix = store.state.items.length !== 1 ? 's' : '';
let emoji = store.state.items.length > 0 ? '🙌' : '😢';
this.element.innerHTML = `
<small>You've done</small>
${store.state.items.length}
<small>thing${suffix} today ${emoji}</small>
`;
}
}
Sieht ziemlich ähnlich wie list aus, oder? Hier gibt es nichts, was wir noch nicht behandelt haben, also fügen wir eine weitere Datei hinzu. Fügen Sie im selben components-Verzeichnis eine Datei namens status.js hinzu und fügen Sie Folgendes ein:
import Component from '../lib/component.js';
import store from '../store/index.js';
export default class Status extends Component {
constructor() {
super({
store,
element: document.querySelector('.js-status')
});
}
render() {
let self = this;
let suffix = store.state.items.length !== 1 ? 's' : '';
self.element.innerHTML = `${store.state.items.length} item${suffix}`;
}
}
Auch hier haben wir alles behandelt, aber Sie sehen, wie praktisch eine Basis-Component ist, mit der man arbeiten kann, oder? Das ist einer der vielen Vorteile der objektorientierten Programmierung, auf der der Großteil dieses Tutorials basiert.
Schließlich lassen Sie uns überprüfen, ob Ihr js-Verzeichnis korrekt aussieht. Dies ist die Struktur, wo wir uns derzeit befinden:
/src
├── js
│ ├── components
│ │ ├── count.js
│ │ ├── list.js
│ │ └── status.js
│ ├──lib
│ │ ├──component.js
│ │ └──pubsub.js
└───── store
└──store.js
└──main.js
Verdrahten wir es
Nachdem wir nun unsere Frontend-Komponenten und unser Haupt-Store haben, müssen wir nur noch alles miteinander verdrahten.
Wir haben unser Store-System und die Komponenten, um seine Daten anzuzeigen und damit zu interagieren. Lassen Sie uns nun abschließen, indem wir die beiden getrennten Enden der App verbinden und alles zum Funktionieren bringen. Wir müssen einen Anfangszustand, einige actions und einige mutations hinzufügen. Fügen Sie in Ihrem store-Verzeichnis eine neue Datei namens state.js hinzu. Für mich sieht es so aus:
~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/store/state.js
Öffnen Sie diese Datei und fügen Sie Folgendes hinzu:
export default {
items: [
'I made this',
'Another thing'
]
};
Das ist ziemlich selbsterklärend. Wir fügen einen Standardsatz von Elementen hinzu, damit unsere kleine App beim ersten Laden vollständig interaktiv ist. Gehen wir zu einigen actions über. Erstellen Sie in Ihrem store-Verzeichnis eine neue Datei namens actions.js und fügen Sie Folgendes hinzu:
export default {
addItem(context, payload) {
context.commit('addItem', payload);
},
clearItem(context, payload) {
context.commit('clearItem', payload);
}
};
Die Aktionen in dieser App sind ziemlich minimal. Im Wesentlichen übergibt jede Aktion einen Payload an eine Mutation, die wiederum die Daten an den Store committet. Der context ist, wie wir bereits gelernt haben, die Instanz der Store-Klasse, und der payload wird von dem übergeben, der die Aktion dispatcht. Apropos Mutationen, fügen wir einige hinzu. Fügen Sie im selben Verzeichnis eine neue Datei namens mutations.js hinzu. Öffnen Sie sie und fügen Sie Folgendes hinzu:
export default {
addItem(state, payload) {
state.items.push(payload);
return state;
},
clearItem(state, payload) {
state.items.splice(payload.index, 1);
return state;
}
};
Wie die Aktionen sind auch diese Mutationen minimal. Meiner Meinung nach sollten Ihre Mutationen immer einfach sein, da sie eine Aufgabe haben: den Zustand des Stores zu mutieren. Daher sind diese Beispiele so komplex, wie sie sein sollten. Jede richtige Logik sollte in Ihren actions stattfinden. Wie Sie für dieses System sehen, geben wir die neue Version des Zustands zurück, damit die commit-Methode des Store ihre Magie entfalten und alles aktualisieren kann. Damit sind die Hauptelemente des Store-Systems vorhanden. Lassen Sie uns sie mit einer Indexdatei zusammenfügen.
Erstellen Sie im selben Verzeichnis eine neue Datei namens index.js. Öffnen Sie sie und fügen Sie Folgendes hinzu:
import actions from './actions.js';
import mutations from './mutations.js';
import state from './state.js';
import Store from './store.js';
export default new Store({
actions,
mutations,
state
});
Diese Datei importiert nur alle unsere Store-Teile und fügt sie zu einer einzigen, kompakten Store-Instanz zusammen. Fertig!
Das letzte Puzzleteil
Das Letzte, was wir zusammenfügen müssen, ist die Datei main.js, die wir ganz am Anfang dieses Tutorials in unsere index.html-Seite aufgenommen haben. Sobald wir das erledigt haben, können wir unsere Browser starten und unsere harte Arbeit genießen! Erstellen Sie eine neue Datei namens main.js im Stammverzeichnis Ihres js-Verzeichnisses. So sieht sie für mich aus:
~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/main.js
Öffnen Sie sie und fügen Sie Folgendes hinzu:
import store from './store/index.js';
import Count from './components/count.js';
import List from './components/list.js';
import Status from './components/status.js';
const formElement = document.querySelector('.js-form');
const inputElement = document.querySelector('#new-item-field');
Bisher ziehen wir nur die benötigten Abhängigkeiten ein. Wir haben unseren Store, unsere Frontend-Komponenten und ein paar DOM-Elemente zum Arbeiten. Fügen wir als Nächstes Folgendes hinzu, um das Formular interaktiv zu gestalten, direkt unter diesem Code:
formElement.addEventListener('submit', evt => {
evt.preventDefault();
let value = inputElement.value.trim();
if(value.length) {
store.dispatch('addItem', value);
inputElement.value = '';
inputElement.focus();
}
});
Was wir hier tun, ist, einen Event-Listener zum Formular hinzuzufügen und zu verhindern, dass es abgesendet wird. Dann holen wir den Wert des Textfelds und entfernen Leerzeichen. Das tun wir, weil wir prüfen wollen, ob tatsächlich Inhalt vorhanden ist, der als Nächstes an den Store übergeben werden soll. Schließlich, wenn Inhalt vorhanden ist, dispatcht wir unsere addItem-Aktion mit diesem Inhalt und lassen unseren brandneuen store das für uns erledigen.
Fügen wir etwas mehr Code zu main.js hinzu. Fügen Sie unter dem Event-Listener Folgendes hinzu:
const countInstance = new Count();
const listInstance = new List();
const statusInstance = new Status();
countInstance.render();
listInstance.render();
statusInstance.render();
Hier erstellen wir nur neue Instanzen unserer Komponenten und rufen jede ihrer render-Methoden auf, damit wir unseren Anfangszustand auf der Seite erhalten.
Mit dieser letzten Ergänzung sind wir fertig!
Öffnen Sie Ihren Browser, aktualisieren Sie ihn und genießen Sie den Glanz Ihrer neuen, zustandsverwalteten App. Fügen Sie dort etwas ein wie „Dieses fantastische Tutorial beendet“. Ziemlich raffiniert, oder?
Nächste Schritte
Es gibt vieles, was Sie mit diesem kleinen System tun könnten, das wir zusammengestellt haben. Hier sind einige Ideen, wie Sie es auf eigene Faust weiterentwickeln können:
- Sie könnten lokalen Speicher implementieren, um den Zustand beizubehalten, auch wenn Sie neu laden.
- Sie könnten das Frontend davon herausziehen und ein kleines Zustandssystem für Ihre Projekte haben.
- Sie könnten das Frontend dieser App weiterentwickeln und es fantastisch aussehen lassen. (Ich wäre wirklich daran interessiert, Ihre Arbeit zu sehen, also teilen Sie sie bitte!)
- Sie könnten mit Remote-Daten und vielleicht sogar einer API arbeiten.
- Sie könnten das Gelernte über
Proxyund das Pub/Sub-Muster nutzen und diese übertragbaren Fähigkeiten weiterentwickeln.
Zusammenfassung
Danke, dass Sie mit mir gelernt haben, wie diese Zustandssysteme funktionieren. Die großen, beliebten Systeme sind viel komplexer und intelligenter als das, was wir getan haben – aber es ist immer noch nützlich, ein Verständnis dafür zu entwickeln, wie diese Systeme funktionieren und das Geheimnis dahinter zu lüften. Es ist auch nützlich zu lernen, wie leistungsfähig JavaScript ohne Frameworks sein kann.
Wenn Sie eine fertige Version dieses kleinen Systems wünschen, schauen Sie sich dieses GitHub-Repository an. Eine Demo finden Sie auch hier.
Wenn Sie dies weiterentwickeln, würde ich es gerne sehen, also kontaktieren Sie mich auf Twitter oder posten Sie unten in den Kommentaren, wenn Sie das tun!
Danke und ich liebe Ihren Vanilla-JS-Ansatz! Ich habe einen ähnlichen Ansatz in meinen persönlichen Projekten verwendet und er hat sich ziemlich gut skaliert!
Haben Sie in der Zwischenzeit in Erwägung gezogen, die DOM-Event-API als PubSub wiederzuverwenden, damit Sie Ihre eigene Bibliothek nicht schreiben müssen? Zum Veröffentlichen können Sie
document.dispatchEvent(new CustomEvent('xxx'))aufrufen und zum Abonnieren können Siedocument.addEventListener(...)aufrufen. Dies ermöglicht es Benutzern, sich abzumelden, was nicht Teil Ihrer PubSub-Bibliothek ist.Referenz: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
Das würde sicher funktionieren. Heydon Pickering hat etwas Ähnliches in seinem winzigen und sehr cleveren Ansatz gemacht.
Die Verwendung der DOM-API verhindert möglicherweise SSR
Sehr interessante Lektüre. Ich bin ein großer Fan von Vuex und State Stores im Allgemeinen. Kudos, Andy. Ich hoffe, es gibt mehr JavaScript-Inhalte wie diesen.
Danke, Oren!
Hallo! Nettes Vorgehen, um ein State-Management-System zu realisieren. Eine Sache, die mich verwirrt, ist PubSub. Ist PubSub nicht dasselbe wie das Observer-Muster, da das Observer-Muster auch ein Publish/Subscriber-Muster ist, oder ist es anders?
Pub/Sub ist kurz für publish/subscribe, also haben Sie genau ins Schwarze getroffen :)
Tatsächlich gibt es einen kleinen Unterschied zwischen Pub/Sub und Observer: Beim Observer-Muster sind Publisher und Subscriber sich gegenseitig bewusst und kommunizieren direkt. Beim Pub/Sub-Muster kommunizieren Publisher und Subscriber indirekt über einen Event-Bus, wodurch die beiden Entitäten entkoppelt werden.
Vielen Dank für Ihren Beitrag. Wenn möglich, möchte ich diesen Artikel übersetzen und ihn mit koreanischen Entwicklern teilen, darf ich übersetzen?
Sicher, verwenden Sie, was Sie möchten. :)
Großartig! Danke :)
http://devtimothy.tistory.com/86
Hier ist eine koreanische Übersetzung dieses Artikels.:D
Wow! Danke :)
Ich bin neugierig, warum die ganze
let self = this-Indirektion?thisist überall sicher, wo Sie es verwendet haben, außer in der Proxy-set-Trap, wo Sie keine Pfeilfunktion verwenden.Ich glaube nicht, dass Indirektion wahrscheinlich das richtige Wort ist. Leute, die JavaScript lernen, können Schwierigkeiten haben zu verstehen, was
thisin bestimmten Kontexten ist, daher kann es hilfreich sein,selfoder ein Äquivalent zu verwenden, um in dieser Hinsicht zu helfen.Wir wissen, dass es in den meisten Kontexten sicher ist, aber dieser Leitfaden ist für alle.
Hallo zusammen!
Ich habe dies zu einem Projekt gemacht, also wenn Sie sich beteiligen und helfen möchten, sind Sie mehr als willkommen.
Schauen Sie sich Beedle auf GitHub an und geben Sie ihm vielleicht einen Stern?
Vielleicht könnten Sie einige der nächsten Schritte aus diesem Artikel mitarbeiten?