Navigieren im React.js-Ökosystem
Veröffentlicht: 2022-03-11Die Innovationsgeschwindigkeit in JavaScript Land ist so hoch, dass manche Leute sogar denken, dass es kontraproduktiv ist. Eine Bibliothek kann im Laufe weniger Monate vom Early-Adopter-Spielzeug über den Stand der Technik bis hin zur Obsoleszenz werden. In der Lage zu sein, ein Tool zu identifizieren, das noch mindestens ein weiteres Jahr relevant bleiben wird, wird selbst zu einer Kunst.
Als React.js vor zwei Jahren veröffentlicht wurde, lernte ich gerade Angular und tat React schnell als eine obskure, noch eine weitere Templating-Bibliothek ab. In diesen zwei Jahren hat Angular unter den JavaScript-Entwicklern wirklich Fuß gefasst und wurde fast zum Synonym für moderne JS-Entwicklung. Ich fing sogar an, Angular in sehr konservativen Unternehmensumgebungen zu sehen, und ich hielt seine strahlende Zukunft für selbstverständlich.
Doch plötzlich geschah etwas Seltsames. Es scheint, dass Angular ein Opfer des Osborne-Effekts oder „Todes durch Vorankündigung“ geworden ist. Das Team kündigte an, dass erstens Angular 2 völlig anders sein wird, ohne einen klaren Migrationspfad von Angular 1, und zweitens, dass Angular 2 für ein weiteres Jahr oder so nicht verfügbar sein wird. Was sagt das jemandem, der ein neues Webprojekt starten möchte? Möchten Sie Ihr neues Projekt in einem Framework schreiben, das durch eine neue Versionsfreigabe obsolet wird?
Diese Angst unter den Entwicklern spielte React in die Hände, das sich in der Community etablieren wollte. Aber React hat sich immer als das „V“ in „MVC“ vermarktet. Dies sorgte bei Webentwicklern, die es gewohnt sind, mit kompletten Frameworks zu arbeiten, für einigen Frust. Wie ergänze ich die fehlenden Teile? Soll ich meine eigene schreiben? Soll ich einfach eine vorhandene Bibliothek verwenden? Wenn ja, welcher?
Tatsächlich hatte Facebook (Ersteller von React.js) noch ein Ass im Ärmel: Den Flux-Workflow, der versprach, die fehlenden „M“- und „C“-Funktionen zu füllen. Um die Sache noch interessanter zu machen, erklärte Facebook, dass Flux ein „Muster“ und kein Framework ist, und ihre Implementierung von Flux ist nur ein Beispiel für das Muster. Getreu ihrem Wort war ihre Implementierung sehr einfach und beinhaltete das Schreiben einer Menge ausführlicher, sich wiederholender Textbausteine, um die Dinge in Gang zu bringen.
Die Open-Source-Community kam zu Hilfe, und ein Jahr später haben wir Dutzende von Flux-Bibliotheken und sogar einige Meta-Projekte, die darauf abzielen, sie zu vergleichen. Das ist eine gute Sache; Anstatt ein fertiges Unternehmens-Framework zu veröffentlichen, hat Facebook es geschafft, das Interesse in der Community zu wecken und die Menschen zu ermutigen, ihre eigenen Lösungen zu entwickeln.
Dieser Ansatz hat einen interessanten Nebeneffekt: Wenn Sie viele verschiedene Bibliotheken kombinieren müssen, um Ihr vollständiges Framework zu erhalten, entkommen Sie effektiv der Herstellerabhängigkeit, und die Innovationen, die während Ihrer eigenen Framework-Konstruktion entstehen, können problemlos wiederverwendet werden anderswo.
Deshalb sind die neuen Sachen rund um React so interessant; das meiste davon kann problemlos in anderen JavaScript-Umgebungen wiederverwendet werden. Auch wenn Sie nicht vorhaben, React zu verwenden, ist ein Blick auf das Ökosystem inspirierend. Vielleicht möchten Sie Ihr Build-System mit dem leistungsstarken und dennoch vergleichsweise einfach zu konfigurierenden Modul-Bundler Webpack vereinfachen oder heute mit dem Babel-Compiler ECMAScript 6 und sogar ECMAScript 7 schreiben.
In diesem Artikel werde ich einige der interessanten Funktionen und Bibliotheken durchgehen, die verfügbar sind. Erkunden wir also das React-Ökosystem!
Build-System
Das Build-System ist wohl das erste, worauf Sie achten sollten, wenn Sie eine neue Webanwendung erstellen. Das Build-System ist nicht nur ein Werkzeug zum Ausführen von Skripten, sondern formt in der JavaScript-Welt normalerweise die allgemeine Struktur Ihrer Anwendung. Die wichtigsten Aufgaben, die ein Build-System abdecken muss, sind folgende:
- Verwaltung externer und interner Abhängigkeiten
- Ausführen von Compilern und Präprozessoren
- Optimierung von Anlagen für die Produktion
- Ausführen des Entwicklungs-Webservers, File Watcher und Browser Reloader
In den letzten Jahren wurde der Yeoman-Workflow mit Bower und Grunt als die heilige Dreifaltigkeit der modernen Frontend-Entwicklung präsentiert, die die Probleme der Boilerplate-Generierung, der Paketverwaltung bzw. der Ausführung gemeinsamer Aufgaben löst, wobei die fortschrittlicheren Leute kürzlich von Grunt zu Gulp wechselten.
In der React-Umgebung können Sie diese getrost vergessen. Nicht, dass Sie sie nicht verwenden könnten, aber die Chancen stehen gut, dass Sie einfach mit Webpack und dem guten alten NPM davonkommen. Wie ist das möglich? Webpack ist ein Modulbündelr, der die in der Node.js-Welt übliche CommonJS-Modulsyntax auch im Browser implementiert. Es macht die Dinge tatsächlich einfacher, da Sie keinen weiteren Paketmanager für das Frontend lernen müssen; Sie verwenden einfach NPM und teilen Abhängigkeiten zwischen Server und Frontend. Sie müssen sich auch nicht mit dem Problem des Ladens der JS-Dateien in der richtigen Reihenfolge befassen, da dies aus Abhängigkeitsimporten abgeleitet wird, die in jeder Datei angegeben sind, und der gesamte Schwarm korrekt zu einem ladbaren Skript verkettet wird.
Um die Sache noch attraktiver zu machen, kann Webpack im Gegensatz zu seinem älteren Cousin Browserify auch andere Asset-Typen verarbeiten. Mit Loadern können Sie beispielsweise jede Asset-Datei in eine JavaScript-Funktion umwandeln, die die referenzierte Datei entweder einbettet oder lädt. Vergessen Sie also die manuelle Vorverarbeitung und Referenzierung von Assets aus HTML. require
Sie einfach Ihre CSS/SASS/LESS-Dateien von JavaScript an, und Webpack erledigt den Rest mit einer einfachen Konfigurationsdatei. Webpack enthält auch einen Entwicklungs-Webserver und einen Dateibeobachter. Außerdem können Sie den Schlüssel "scripts"
in package.json
, um Shell-Einzeiler zu definieren:
{ "name": "react-example-filmdb", "version": "0.0.1", "description": "Isomorphic React + Flux film database example", "main": "server/index.js", "scripts": { "build": "./node_modules/.bin/webpack --progress --stats --config ./webpack/prod.config.js", "dev": "node --harmony ./webpack/dev-server.js", "prod": "NODE_ENV=production node server/index.js", "test": "./node_modules/.bin/karma start --single-run", "postinstall": "npm run build" } ... }
Und das ist ungefähr alles, was Sie brauchen, um Gulp und Bower zu ersetzen. Natürlich können Sie Yeoman weiterhin zum Generieren von Anwendungsbausteinen verwenden. Lassen Sie sich nicht entmutigen, wenn es keinen Yeoman-Generator für das gewünschte Material gibt (die modernsten Bibliotheken haben oft keinen). Sie können immer noch einfach einige Boilerplates von GitHub klonen und loshacken.
ECMAScript von morgen, heute
Das Tempo der Entwicklung der JavaScript-Sprache hat in den letzten Jahren erheblich zugenommen, und nach einer Zeit, in der Macken entfernt und die Sprache stabilisiert wurden, sehen wir jetzt leistungsstarke neue Funktionen, die hinzukommen noch nicht offiziell gemacht wurde, findet es bereits breite Akzeptanz. Die Arbeit an ECMAScript 7 (ES7) ist im Gange, aber viele seiner Funktionen werden bereits von den moderneren Bibliotheken übernommen.
Wie ist das möglich? Vielleicht denken Sie, dass Sie diese glänzenden neuen JavaScript-Funktionen nicht nutzen können, bis sie von Internet Explorer unterstützt werden, aber denken Sie noch einmal darüber nach. ES-Transpiler sind bereits so allgegenwärtig geworden, dass wir sogar auf eine angemessene Browserunterstützung verzichten können. Der derzeit beste verfügbare ES-Transpiler ist Babel: Er nimmt Ihren neuesten ES6+-Code und wandelt ihn in Vanilla ES5 um, sodass Sie jedes neue ES-Feature verwenden können, sobald es erfunden ist (und in Babel implementiert ist, was normalerweise recht häufig vorkommt schnell).
Die neuesten JavaScript-Funktionen sind in allen Front-End-Frameworks nützlich, und React wurde kürzlich aktualisiert, um gut mit den ES6- und ES7-Spezifikationen zusammenzuarbeiten. Diese neuen Funktionen sollten viele Kopfschmerzen bei der Entwicklung mit React beseitigen. Werfen wir einen Blick auf einige der nützlichsten Ergänzungen und wie sie einem React-Projekt zugute kommen können. Später werden wir sehen, wie man einige nützliche Tools und Bibliotheken mit React verwendet, während man sich diese verbesserte Syntax zunutze macht.
ES6-Klassen
Die objektorientierte Programmierung ist ein mächtiges und weit verbreitetes Paradigma, aber die Herangehensweise von JavaScript ist etwas exotisch. Die meisten Frontend-Frameworks, sei es Backbone, Ember, Angular oder React, haben daher ihre eigenen proprietären Methoden zum Definieren von Klassen und Erstellen von Objekten übernommen. Aber mit ES6 haben wir jetzt traditionelle Klassen in JavaScript, und es macht einfach Sinn, sie zu verwenden, anstatt unsere eigene Implementierung zu schreiben. Also statt:
React.createClass({ displayName: 'HelloMessage', render() { return <div>Hello {this.props.name}</div>; } })
wir können schreiben:
class HelloMessage extends React.Component { render() { return <div>Hello {this.props.name}</div>; } }
Betrachten Sie für ein ausführlicheres Beispiel diesen Code mit der alten Syntax:
React.createClass({ displayName: 'Counter', getDefaultProps: function(){ return {initialCount: 0}; }, getInitialState: function() { return {count: this.props.initialCount} }, propTypes: {initialCount: React.PropTypes.number}, tick() { this.setState({count: this.state.count + 1}); }, render() { return ( <div onClick={this.tick}> Clicks: {this.state.count} </div> ); } });
Und vergleichen Sie mit der ES6-Version:
class Counter extends React.Component { static propTypes = {initialCount: React.PropTypes.number}; static defaultProps = {initialCount: 0}; constructor(props) { super(props); this.state = {count: props.initialCount}; } state = {count: this.props.initialCount}; tick() { this.setState({count: this.state.count + 1}); } render() { return ( <div onClick={this.tick.bind(this)}> Clicks: {this.state.count} </div> ); } }
Hier werden die React Lifecycle Methoden getDefaultProps
und getInitialState
nicht mehr benötigt. getDefaultProps
wird zur statischen Klassenvariablen defaultProps
, und der Anfangszustand wird nur im Konstruktor definiert. Der einzige Nachteil ist, dass Methoden nicht mehr automatisch gebunden sind, sodass Sie bind
verwenden müssen, wenn Sie Handler von JSX aufrufen.
Dekorateure
Decorators sind eine nützliche Funktion von ES7. Sie ermöglichen es Ihnen, das Verhalten einer Funktion oder Klasse zu erweitern, indem Sie sie in eine andere Funktion einschließen. Nehmen wir beispielsweise an, dass Sie für einige Ihrer Komponenten denselben Änderungshandler verwenden möchten, sich aber nicht auf das Antimuster für die Vererbung festlegen möchten. Sie können stattdessen einen Klassendekorateur verwenden. Lassen Sie uns den Decorator wie folgt definieren:
addChangeHandler: function(target) { target.prototype.changeHandler = function(key, attr, event) { var state = {}; state[key] = this.state[key] || {}; state[key][attr] = event.currentTarget.value; this.setState(state); }; return target; }
Wichtig dabei ist, dass die Funktion addChangeHandler
die Funktion changeHandler
zum Prototyp der Zielklasse hinzufügt.
Um den Decorator anzuwenden, könnten wir schreiben:
MyClass = changeHandler(MyClass)
oder eleganter mit ES7-Syntax:
@addChangeHandler class MyClass { ... }
Was den Inhalt der changeHandler
Funktion selbst betrifft, kann das Arbeiten mit Eingaben in React mühsam sein, da React keine bidirektionale Datenbindung hat. Die Funktion changeHandler
versucht, es einfacher zu machen. Der erste Parameter gibt einen key
für das Zustandsobjekt an, das als Datenobjekt für die Eingabe dient. Der zweite Parameter ist das Attribut, in dem der Wert aus dem Eingabefeld gespeichert wird. Diese beiden Parameter werden von JSX mit dem Schlüsselwort bind
festgelegt.
@addChangeHandler class LoginInput extends React.Component { constructor(props) { super(props); this.state = { login: {} }; } render() { return ( <input type='text' value={this.state.login.username} onChange={this.changeHandler.bind(this, 'login', 'username')} /> <input type='password' value={this.state.login.password} onChange={this.changeHandler.bind(this, 'login', 'password')} /> ) } }
Wenn der Benutzer das Feld username ändert, wird sein Wert in this.state.login.username
, ohne dass weitere benutzerdefinierte Handler definiert werden müssen.
Pfeilfunktionen
Der dynamische this
-Kontext von JavaScript war für Entwickler ein ständiges Problem, da der this
-Kontext einer verschachtelten Funktion etwas unintuitiv auf global zurückgesetzt wird, sogar innerhalb einer Klasse. Um dies zu beheben, ist es normalerweise erforderlich, this
in einer äußeren Bereichsvariablen (normalerweise _this
) zu speichern und in inneren Funktionen zu verwenden:
class DirectorsStore { onFetch(directors) { var _this = this; this.directorsHash = {}; directors.forEach(function(x){ _this.directorsHash[x._id] = x; }) } }
Mit der neuen ES6-Syntax kann die function(x){
umgeschrieben werden als (x) => {
. Diese „Pfeil“-Methodendefinition bindet this
nicht nur korrekt an den äußeren Gültigkeitsbereich, sondern ist auch deutlich kürzer, was beim Schreiben von viel asynchronem Code definitiv zählt.
onFetch(directors) { this.directorsHash = {}; directors.forEach((x) => { this.directorsHash[x._id] = x; }) }
Destrukturierende Aufgaben
Destrukturierende Zuweisungen, eingeführt in ES6, ermöglichen es Ihnen, ein zusammengesetztes Objekt auf der linken Seite einer Zuweisung zu haben:
var o = {p: 42, q: true}; var {p, q} = o; console.log(p); // 42 console.log(q); // true
Das ist nett, aber wie hilft es uns eigentlich in React? Betrachten Sie das folgende Beispiel:
function makeRequest(url, method, params) { var config = { url: url, method: method, params: params }; ... }
Mit der Destrukturierung können Sie einige Tastenanschläge sparen. Dem Schlüsselliteral {url, method, params}
werden automatisch Werte aus dem Geltungsbereich mit denselben Namen wie den Schlüsseln zugewiesen. Dieses Idiom wird ziemlich häufig verwendet, und das Eliminieren von Wiederholungen macht den Code weniger fehleranfällig.
function makeRequest(url, method, params) { var config = {url, method, params}; ... }
Die Destrukturierung kann Ihnen auch dabei helfen, nur eine Teilmenge eines Moduls zu laden:
const {clone, assign} = require('lodash'); function output(data, optional) { var payload = clone(data); assign(payload, optional); }
Argumente: Default, Rest und Spread
Funktionsargumente sind in ES6 leistungsfähiger. Schließlich können Sie das Standardargument festlegen:
function http(endpoint, method='GET') { console.log(method) ... } http('/api') // GET
Sind Sie es leid, sich mit dem unhandlichen arguments
herumzuärgern? Mit der neuen Spezifikation können Sie die restlichen Argumente als Array erhalten:
function networkAction(context, method, ...rest) { // rest is an array return method.apply(context, rest); }
Und wenn Sie apply()
nicht aufrufen möchten , können Sie einfach ein Array in Funktionsargumente aufteilen:
myArguments = ['foo', 'bar', 123]; myFunction(...myArguments);
Generatoren und asynchrone Funktionen
ES6 führte JavaScript-Generatoren ein. Ein Generator ist im Grunde eine JavaScript-Funktion, deren Ausführung angehalten und später fortgesetzt werden kann, wobei sie sich an ihren Zustand erinnert. Jedes Mal, wenn das yield
-Schlüsselwort angetroffen wird, wird die Ausführung angehalten und das yield
Argument wird an das aufrufende Objekt zurückgegeben:
function* sequence(from, to) { console.log('Ready!'); while(from <= to) { yield from++; } }
Hier ist ein Beispiel für diesen Generator in Aktion:
> var cursor = sequence(1,3) Ready! > cursor.next() { value: 1, done: false } > cursor.next() { value: 2, done: false } > cursor.next() { value: 3, done: false } > cursor.next() { value: undefined, done: true }
Wenn wir die Generatorfunktion aufrufen, wird sie bis zum ersten yield
ausgeführt und stoppt dann. Nachdem wir next()
aufgerufen haben, gibt es den ersten Wert zurück und setzt die Ausführung fort. Jeder yield
gibt einen anderen Wert zurück, aber nach dem dritten Aufruf wird die Generatorfunktion beendet, und jeder nachfolgende Aufruf von next()
gibt { value: undefined, done: true }
zurück.
Der Zweck von Generatoren besteht natürlich nicht darin, komplizierte Zahlenfolgen zu erzeugen. Der aufregende Teil ist ihre Fähigkeit, die Ausführung von Funktionen zu stoppen und fortzusetzen, was verwendet werden kann, um den asynchronen Programmfluss zu steuern und diese lästigen Callback-Funktionen endlich loszuwerden.
Um diese Idee zu demonstrieren, benötigen wir zunächst eine asynchrone Funktion. Normalerweise hätten wir einige E/A-Operationen, aber der Einfachheit halber verwenden wir einfach setTimeout
und geben ein Promise zurück. (Beachten Sie, dass ES6 auch native Promises für JavaScript eingeführt hat.)
function asyncDouble(x) { var deferred = Promise.defer(); setTimeout(function(){ deferred.resolve(x*2); }, 1000); return deferred.promise; }
Als nächstes brauchen wir eine Verbraucherfunktion:
function consumer(generator){ var cursor = generator(); var value; function loop() { var data = cursor.next(value); if (data.done) { return; } else { data.value.then(x => { value = x; loop(); }) } } loop(); }
Diese Funktion nimmt einen beliebigen Generator als Argument und ruft darauf next()
auf, solange es Werte zum yield
gibt. In diesem Fall handelt es sich bei den ausgegebenen Werten um Zusagen, und daher ist es notwendig, auf die Auflösung der Zusagen zu warten und die Rekursion mit loop()
zu verwenden, um eine Schleife über verschachtelte Funktionen hinweg zu erreichen.
Der Rückgabewert wird im then()
-Handler aufgelöst und an value
übergeben, der im äußeren Gültigkeitsbereich definiert ist und an den next(value)
-Aufruf übergeben wird. Dieser Aufruf macht den Wert zu einem Ergebnis des entsprechenden yield-Ausdrucks. Das bedeutet, dass wir jetzt überhaupt asynchron ohne Rückrufe schreiben können:
function* myGenerator(){ const data1 = yield asyncDouble(1); console.log(`Double 1 = ${data1}`); const data2 = yield asyncDouble(2); console.log(`Double 2 = ${data2}`); const data3 = yield asyncDouble(3); console.log(`Double 3 = ${data3}`); } consumer(myGenerator);
Der Generator myGenerator
wird bei jedem yield
angehalten und wartet darauf, dass der Verbraucher das aufgelöste Versprechen liefert. Und tatsächlich sehen wir die berechneten Zahlen in Intervallen von einer Sekunde in der Konsole erscheinen.
Double 1 = 2 Double 2 = 4 Double 3 = 6
Dies veranschaulicht das Grundkonzept, ich empfehle jedoch nicht, diesen Code in der Produktion zu verwenden. Wählen Sie stattdessen eine gut getestete Bibliothek wie co. Auf diese Weise können Sie auf einfache Weise asynchronen Code mit Erträgen schreiben, einschließlich Fehlerbehandlung:
co(function *(){ var a = yield Promise.resolve(1); console.log(a); var b = yield Promise.resolve(2); console.log(b); var c = yield Promise.resolve(3); console.log(c); }).catch(function(err){ console.error(err.stack); });
Dieses Beispiel zeigt also, wie Sie mit ES6-Generatoren asynchronen Code ohne Rückrufe schreiben. ES7 geht diesen Ansatz noch einen Schritt weiter, indem es die Schlüsselwörter async
und await
einführt und die Notwendigkeit einer Generatorbibliothek insgesamt beseitigt. Mit dieser Funktion würde das vorherige Beispiel wie folgt aussehen:
async function (){ try { var a = await Promise.resolve(1); console.log(a); var b = await Promise.resolve(2); console.log(b); var c = await Promise.resolve(3); console.log(c); } catch (err) { console.error(err.stack); } };
Meiner Meinung nach erleichtert dies die Arbeit mit asynchronem Code in JavaScript. Nicht nur in React, sondern auch überall sonst.
Generatoren sind nicht nur prägnanter und unkomplizierter, sondern ermöglichen uns auch die Verwendung von Techniken, die mit Rückrufen nur sehr schwer zu implementieren wären. Ein prominentes Beispiel für die Güte eines Generators ist die Koa-Middleware-Bibliothek für Node.js. Es zielt darauf ab, Express zu ersetzen, und zu diesem Zweck kommt es mit einem Killer-Feature: Die Middleware-Kette fließt nicht nur stromabwärts (mit Client-Anfrage), sondern auch stromaufwärts , wodurch weitere Änderungen an der Antwort des Servers möglich sind. Betrachten Sie das folgende Beispiel für einen Koa-Server:

// Response time logger middleware app.use(function *(next){ // Downstream var start = new Date; yield next; // Upstream this.body += ' World'; var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); // Response handler app.use(function *(){ this.body = 'Hello'; }); app.listen(3000);
Die Antwort-Middleware yield
wird nachgelagert in den Response-Handler geleitet, der den Antworttext festlegt, und im Upstream-Fluss (nach dem yield
-Ausdruck) ist eine weitere Änderung von this.body
zulässig, ebenso wie andere Funktionen wie Zeitprotokollierung, die möglich sind weil Upstream und Downstream den gleichen Funktionskontext teilen. Dies ist viel mächtiger als Express, bei dem ein Versuch, dasselbe zu erreichen, so enden würde:
var start; // Downstream middleware app.use(function(req, res, next) { start = new Date; next(); // Already returned, cannot continue here }); // Response app.use(function (req, res, next){ res.send('Hello World') next(); }); // Upstream middleware app.use(function(req, res, next) { // res already sent, cannot modify var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); next(); }); app.listen(3000);
Sie können wahrscheinlich schon erkennen, was hier falsch ist; Die Verwendung einer „globalen“ start
führt zu einer Race-Bedingung, die bei gleichzeitigen Anforderungen Unsinn zurückgibt. Die Lösung ist eine nicht offensichtliche Problemumgehung, und Sie können die Änderung der Antwort im Upstream-Fluss vergessen.
Wenn Sie koa verwenden, erhalten Sie außerdem den asynchronen Generator-Workflow kostenlos:
app.use(function *(){ try { const part1 = yield fs.readFile(this.request.query.file1, 'utf8'); const part2 = yield fs.readFile(this.request.query.file2, 'utf8'); this.body = part1 + part2; } catch (err) { this.status = 404 this.body = err; } }); app.listen(3000);
Sie können sich die Versprechungen und Rückrufe vorstellen, die mit der Neuerstellung dieses kleinen Beispiels in Express verbunden sind.
Wie bezieht sich all dieses Node.js-Gerede auf React? Nun, Node ist die erste Wahl, wenn es um ein geeignetes Backend für React geht. Da Node auch in JavaScript geschrieben ist, unterstützt es die gemeinsame Nutzung von Code zwischen Backend und Frontend, sodass wir isomorphe React-Webanwendungen erstellen können. Aber dazu später mehr.
Flussbibliothek
React ist großartig darin, zusammensetzbare Ansichtskomponenten zu erstellen, aber wir brauchen eine Möglichkeit, Daten und Status in der gesamten Anwendung zu verwalten. Es wurde fast allgemein vereinbart, dass React am besten durch die Flux-Anwendungsarchitektur ergänzt wird. Wenn Sie ganz neu bei Flux sind, empfehle ich eine schnelle Auffrischung.
Was nicht so allgemein vereinbart wurde, ist, welche der vielen Flux-Implementierungen zu wählen ist. Facebook Flux wäre die offensichtliche Wahl, aber für die meisten Leute ist es zu ausführlich. Alternative Implementierungen konzentrieren sich hauptsächlich darauf, die Menge der erforderlichen Boilerplate mit einem Konvention-über-Konfiguration-Ansatz zu reduzieren, und auch mit einigen Komfortfunktionen für Komponenten höherer Ordnung, serverseitiges Rendering und so weiter. Einige der Top-Anwärter mit verschiedenen Popularitätskennzahlen sind hier zu sehen. Ich habe mir Alt, Reflux, Flummox, Fluxxor und Marty.js angesehen.
Meine Art, die richtige Bibliothek auszuwählen, ist in keiner Weise objektiv, aber sie könnte trotzdem helfen. Fluxxor war eine dieser ersten Bibliotheken, die ich mir angesehen habe, aber jetzt sieht es ein bisschen veraltet aus. Marty.js ist interessant und hat viele Funktionen, beinhaltet aber immer noch viele Boilerplates, und einige der Funktionen scheinen überflüssig. Reflux sieht gut aus und hat etwas Traktion, fühlt sich aber für Anfänger etwas hart an und es fehlt auch die richtige Dokumentation. Flummox und Alt sind sich sehr ähnlich, aber Alt scheint noch weniger Boilerplate, sehr aktive Entwicklung, aktuelle Dokumentation und eine hilfreiche Slack-Community zu haben. Deshalb habe ich mich für Alt entschieden.
Alt Flux
Mit Alt wird der Flux-Workflow viel einfacher, ohne an Leistungsfähigkeit einzubüßen. Die Flux-Dokumentation von Facebook sagt viel über den Dispatcher aus, aber wir können das ignorieren, da der Dispatcher in Alt durch Konvention implizit mit Aktionen verbunden ist und normalerweise keinen benutzerdefinierten Code benötigt. Damit bleiben uns nur Speicher , Aktionen und Komponenten . Diese drei Ebenen können so verwendet werden, dass sie sich gut in das MVC -Denkmodell einfügen: Stores sind Models , Actions sind Controllers und Components sind Views . Der Hauptunterschied ist der unidirektionale Datenfluss, der für das Flux-Muster zentral ist, was bedeutet, dass Controller (Aktionen) Ansichten (Komponenten) nicht direkt ändern können, sondern nur Modelländerungen (Speichern) auslösen können, an die Ansichten passiv gebunden sind. Dies war bereits eine Best Practice für einige aufgeklärte Angular-Entwickler.
Der Arbeitsablauf ist wie folgt:
- Komponenten initiieren Aktionen.
- Stores hören auf Aktionen und aktualisieren Daten.
- Komponenten sind an Speicher gebunden und werden neu gerendert, wenn Daten aktualisiert werden.
Aktionen
Bei der Verwendung der Alt Flux-Bibliothek gibt es im Allgemeinen zwei Arten von Aktionen: automatisch und manuell. Automatische Aktionen werden mit der Funktion generateActions
erstellt und gehen direkt an den Dispatcher. Manuelle Methoden werden als Methoden Ihrer Aktionsklassen definiert und können mit einer zusätzlichen Nutzlast an den Dispatcher gehen. Der häufigste Anwendungsfall für automatische Aktionen besteht darin, Geschäfte über ein Ereignis in der Anwendung zu benachrichtigen. Manuelle Aktionen sind unter anderem die bevorzugte Art, mit Serverinteraktionen umzugehen.
Die REST-API-Aufrufe gehören also zu Aktionen. Der komplette Arbeitsablauf ist wie folgt:
- Komponente löst eine Aktion aus.
- Der Ersteller der Aktion führt eine asynchrone Serveranfrage aus, und das Ergebnis geht als Nutzlast an den Dispatcher.
- Der Speicher hört auf die Aktion, der entsprechende Aktionshandler erhält das Ergebnis als Argument und der Speicher aktualisiert seinen Status entsprechend.
Für AJAX-Anfragen können wir die axios-Bibliothek verwenden, die unter anderem nahtlos mit JSON-Daten und -Headern umgeht. Anstelle von Promises oder Callbacks können wir das ES7- async
/ await
-Muster verwenden. Wenn der POST
-Antwortstatus nicht 2XX ist, wird ein Fehler ausgegeben und wir senden entweder zurückgesendete Daten oder einen empfangenen Fehler.
Sehen wir uns eine Anmeldeseite für ein einfaches Beispiel des Alt-Workflows an. Die Abmeldeaktion muss nichts Besonderes tun, sondern nur den Shop benachrichtigen, damit wir sie automatisch generieren können. Die Anmeldeaktion ist manuell und erwartet Anmeldedaten als Parameter für den Ersteller der Aktion. Nachdem wir eine Antwort vom Server erhalten haben, senden wir entweder Erfolgsdaten oder, wenn ein Fehler ausgegeben wird, den empfangenen Fehler.
class LoginActions { constructor() { // Automatic action this.generateActions('logout'); } // Manual action async login(data) { try { const response = await axios.post('/auth/login', data); this.dispatch({ok: true, user: response.data}); } catch (err) { console.error(err); this.dispatch({ok: false, error: err.data}); } } } module.exports = (alt.createActions(LoginActions));
Shops
Der Flux-Speicher dient zwei Zwecken: Er hat Aktionshandler und trägt den Zustand. Fahren wir mit unserem Anmeldeseitenbeispiel fort, um zu sehen, wie das funktioniert.
Lassen Sie uns LoginStore
mit zwei Zustandsattributen erstellen: user
für den aktuell angemeldeten Benutzer und error
für den aktuellen Login-bezogenen Fehler. Im Sinne der Reduzierung von Boilerplate ermöglicht uns Alt, mit einer einzigen Funktion bindActions
an alle Aktionen einer Klasse zu binden.
class LoginStore { constructor() { this.bindActions(LoginActions); this.user = null; this.error = null; } ...
Handlernamen werden on
Konvention definiert und dem entsprechenden Aktionsnamen vorangestellt. Die login
wird also von onLogin
usw. behandelt. Beachten Sie, dass der erste Buchstabe des Aktionsnamens im CamelCase-Stil groß geschrieben wird. In unserem LoginStore
haben wir die folgenden Handler, die von den entsprechenden Aktionen aufgerufen werden:
... onLogin(data) { if (data.ok) { this.user = data.user; this.error = null; router.transitionTo('home'); } else { this.user = null; this.error = data.error } } onLogout() { this.user = null; this.error = null; } }
Komponenten
Die übliche Art, Stores an Komponenten zu binden, ist die Verwendung einer Art React-Mixin. Aber da Mixins aus der Mode kommen, muss es einen anderen Weg geben. Einer der neuen Ansätze ist die Verwendung von Komponenten höherer Ordnung. Wir nehmen unsere Komponente und fügen sie in eine Wrapper-Komponente ein, die sich um das Abhören von Stores und das Aufrufen von Re-Rendering kümmert. Unsere Komponente erhält den Status des Stores in props
. Dieser Ansatz ist auch hilfreich, um unseren Code in intelligente und dumme Komponenten zu organisieren, die in letzter Zeit in Mode gekommen sind. Für Alt wird der Komponenten-Wrapper von AltContainer
implementiert:
export default class Login extends React.Component { render() { return ( <AltContainer stores={{LoginStore: LoginStore}}> <LoginPage/> </AltContainer> )} }
Unsere LoginPage
Komponente verwendet auch den zuvor eingeführten changeHandler
Dekorator. Daten aus LoginStore
werden verwendet, um Fehler bei einer nicht erfolgreichen Anmeldung anzuzeigen, und das erneute Rendern wird von AltContainer
übernommen. Durch Klicken auf die login
wird die Anmeldeaktion ausgeführt und der Alt-Flux-Workflow abgeschlossen:
@changeHandler export default class LoginPage extends React.Component { constructor(props) { super(props); this.state = { loginForm: {} }; } login() { LoginActions.login(this.state.loginForm) } render() { return ( <Alert>{{this.props.LoginStore.error}}</Alert> <Input label='Username' type='text' value={this.state.login.username} onChange={this.changeHandler.bind(this, 'loginForm', 'username')} /> <Input label='Password' type='password' value={this.state.login.password} onChange={this.changeHandler.bind(this, 'loginForm', 'password')} /> <Button onClick={this.login.bind(this)}>Login</Button> )} }
Isomorphes Rendern
Isomorphe Webanwendungen sind heutzutage ein heißes Thema, weil sie einige der größten Aufgaben traditioneller Single-Page-Anwendungen lösen. In diesen Anwendungen wird Markup dynamisch von JavaScript im Browser erstellt. Das Ergebnis ist, dass der Inhalt für Clients mit deaktiviertem JavaScript nicht verfügbar ist, insbesondere für Suchmaschinen-Webcrawler. Das bedeutet, dass Ihre Webseite nicht indexiert ist und nicht in den Suchergebnissen erscheint. Es gibt Möglichkeiten, dies zu umgehen, aber sie sind alles andere als optimal. Der isomorphe Ansatz versucht, dieses Problem zu beheben, indem die angeforderte URL einer Einzelseitenanwendung auf dem Server vorgerendert wird. Mit Node.js haben Sie JavaScript auf dem Server, sodass React auch serverseitig ausgeführt werden kann. Das sollte nicht zu schwer sein, oder?
Ein Hindernis ist, dass einige Flux-Bibliotheken, insbesondere solche, die Singletons verwenden, Schwierigkeiten mit dem serverseitigen Rendern haben. Wenn Sie einzelne Flux-Speicher und mehrere gleichzeitige Anfragen an Ihren Server haben, werden die Daten durcheinander gebracht. Einige Bibliotheken lösen dies durch die Verwendung von Flux-Instanzen, aber dies bringt andere Nachteile mit sich, insbesondere die Notwendigkeit, diese Instanzen in Ihrem Code herumzureichen. Alt bietet auch Flux-Instanzen an, hat aber auch das Problem des serverseitigen Renderns mit Singletons gelöst; Es leert Speicher nach jeder Anfrage, sodass jede gleichzeitige Anfrage mit einer sauberen Weste beginnt.
Der Kern der serverseitigen Rendering-Funktionalität wird von React.renderToString
bereitgestellt. Die gesamte Frontend-Anwendung von React wird ebenfalls auf dem Server ausgeführt. Auf diese Weise müssen wir nicht darauf warten, dass das clientseitige JavaScript das Markup erstellt; Es ist auf dem Server für die aufgerufene URL vorgefertigt und wird als HTML an den Browser gesendet. Wenn das Client-JavaScript ausgeführt wird, macht es dort weiter, wo der Server aufgehört hat. Um dies zu unterstützen, können wir die Iso-Bibliothek verwenden, die mit Alt verwendet werden soll.
Zuerst initialisieren wir Flux auf dem Server mit alt.bootstrap
. Es ist möglich, die Flux-Speicher vorab mit Daten zum Rendern zu füllen. Es ist auch notwendig zu entscheiden, welche Komponente für welche URL gerendert werden soll, was die Funktionalität des clientseitigen Router
ist. Wir verwenden die Singleton-Version von alt
, also müssen wir nach jedem Rendern die Speicher mit alt.flush()
, damit sie für eine weitere Anfrage sauber sind. Mit dem iso
-Add-On wird der Status von Flux in das HTML-Markup serialisiert, sodass der Client weiß, wo er abholen muss:
// We use react-router to run the URL that is provided in routes.jsx var getHandler = function(routes, url) { var deferred = Promise.defer(); Router.run(routes, url, function (Handler) { deferred.resolve(Handler); }); return deferred.promise; }; app.use(function *(next) { yield next; // We seed our stores with data alt.bootstrap(JSON.stringify(this.locals.data || {})); var iso = new Iso(); const handler = yield getHandler(reactRoutes, this.request.url); const node = React.renderToString(React.createElement(handler)); iso.add(node, alt.flush()); this.render('layout', {html: iso.render()}); });
Auf der Clientseite erfassen wir den Serverstatus und booten alt
mit den Daten. Dann führen wir Router
und React.render
auf dem Zielcontainer aus, wodurch das vom Server generierte Markup nach Bedarf aktualisiert wird.
Iso.bootstrap(function (state, _, container) { // Bootstrap the state from the server alt.bootstrap(state) Router.run(routes, Router.HistoryLocation, function (Handler, req) { let node = React.createElement(Handler) React.render(node, container) }) })
Schön!
Nützliche Front-End-Bibliotheken
Ein Leitfaden für das React-Ökosystem wäre nicht vollständig, ohne einige Frontend-Bibliotheken zu erwähnen, die besonders gut mit React funktionieren. Diese Bibliotheken bewältigen die häufigsten Aufgaben, die in fast jeder Webanwendung zu finden sind: CSS-Layouts und -Container, formatierte Formulare und Schaltflächen, Validierungen, Datumsauswahl und so weiter. Es macht keinen Sinn, das Rad neu zu erfinden, wenn diese Probleme bereits gelöst sind.
Reaktions-Bootstrap
Twitter's Bootstrap framework has become commonplace since it is of immense help to every web developer who does not want to spend a ton of time working in CSS. In the prototyping phase especially, Bootstrap is indispensable. To leverage the power of bootstrap in a React application, it's good to use it with React-Bootstrap, which comes with nice React syntax and re-implements Bootstrap's jQuery plugins using native React components. The resulting code is terse and easy to understand:
<Navbar brand='React-Bootstrap'> <Nav> <NavItem eventKey={1} href='#'>Link</NavItem> <NavItem eventKey={2} href='#'>Link</NavItem> <DropdownButton eventKey={3} title='Dropdown'> <MenuItem eventKey='1'>Action</MenuItem> <MenuItem eventKey='2'>Another action</MenuItem> <MenuItem eventKey='3'>Something else here</MenuItem> <MenuItem divider /> <MenuItem eventKey='4'>Separated link</MenuItem> </DropdownButton> </Nav> </Navbar>
Personally, I cannot escape the feeling that this is what HTML should always have been like.
If you want to use Bootstrap source with Webpack, consider using less-loader or bootstrap-sass-loader, depending on the preprocessor you prefer. It will allow you to easily customize which Bootstrap components to include, and also allow the usage of LESS or SASS global variables in your CSS code.
React Router
React Router has become the de-facto standard for routing in React. It allows nested routing, support for redirections, plays nicely with isomorphic rendering, and has a simple JSX-based syntax:
<Router history={new BrowserHistory}> <Route path="/" component={App}> <Route path="about" name="about" component={About}/> <Route path="users" name="users" component={Users} indexComponent={RecentUsers}> <Route path="/user/:userId" name="user" component={User}/> </Route> <Route path="*" component={NoMatch}/> </Route> </Router>
React Router also provides a Link
component that you can use for navigation in your application, specifying only the route name:
<nav> <Link to="about">About</Link> <Link to="users">Users</Link> </nav>
There is even a library for integration with React-Bootstrap, so if you are using Bootstrap's components and don't feel like setting the active
class on them manually all the time, you can use react-router-bootstrap and write code like this:
<Nav> <NavItemLink to="about">About</NavItemLink> <NavItemLink to="users">Users</NavItemLink> </Nav>
No additional setup is necessary. Active links will take care of themselves.
Formsy-React
Working with forms can be tedious, so let's get some help from the formsy-react library, which will help us manage validations and data models. The Formsy-React library, strangely enough, does not include the actual form inputs because users are encouraged to write their own (which is great). But if you are content with the common ones, just use formsy-react-components. Bootstrap classes are included:
import Formsy from 'formsy-react'; import {Input} from 'formsy-react-components'; export default class FormsyForm extends React.Component { enableButton() { this.setState({canSubmit: true}); } disableButton() { this.setState({canSubmit: true}); } submit(model) { FormActions.saveEmail(model.email); } render() { return ( <Formsy.Form onValidSubmit={this.submit} onValid={this.enableButton} onInvalid={this.disableButton}> <Input name="email" validations="isEmail" validationError="This is not a valid email" required/> <button type="submit" disabled={!this.state.canSubmit}>Submit</button> </Formsy.Form> )} }
Calendar and Typeahead
Calendar and typeahead are icing on the cake of every UI toolkit. Sadly, these two components were removed from Bootstrap 3, probably because they are too specialized for a general purpose CSS framework. Luckily, I've been able to find worthy replacements in react-pikaday and react-select. I've tested more than 10 libraries, and these two came out as the best. They are dead easy to use, as well:
import Pikaday from 'react-pikaday'; import Select from 'react-select'; export default class CalendarAndTypeahead extends React.Component { constructor(props){ super(props); this.options = [ { value: 'one', label: 'One' }, { value: 'two', label: 'Two' } ]; } dateChange(date) { this.setState({date: date}); }, selectChange(selected) { this.setState({selected: selected}); }, render() { return ( <Pikaday value={this.state.date} onChange={this.dateChange} /> <Select name="form-field-name" value={this.state.selected} options={this.options} onChange={selectChange} /> )} }
Conclusion - React.JS
In this article I've presented libraries and techniques that I consider some of the most productive in current web development. Some of them are React-specific, but due to React's open nature, many of them are usable in other environments as well. Technological progress is sometimes hindered by fear of the newest stuff, so I hope this article will help to dissipate doubts concerning React, Flux and the newest features in ECMAScript.
If you are interested, you can take a look at my example application built with these technologies. The source code is available on GitHub.
Danke fürs Lesen!