In Teil 1 habe ich verschiedene Techniken im funktionalen Stil durchgesprochen, um HTML basierend auf JavaScript-Daten sauber darzustellen. Wir haben unsere UI in Komponentenfunktionen zerlegt, die jeweils einen Teil des Markups als Funktion von Daten zurückgaben. Diese haben wir dann zu Ansichten zusammengesetzt, die durch einen einzigen Funktionsaufruf aus neuen Daten rekonstruiert werden konnten.
Das ist die Bonusrunde. In diesem Beitrag wird das Ziel sein, der vollwertigen, klassenbasierten React-Komponentensyntax so nahe wie möglich zu kommen, und zwar mit VanillaJS (d.h. nativem JavaScript ohne Bibliotheken/Frameworks). Ich möchte vorausschicken, dass einige der hier vorgestellten Techniken nicht übermäßig praktisch sind, aber ich denke, sie werden dennoch eine unterhaltsame und interessante Erkundung dessen bieten, wie weit JavaScript in den letzten Jahren gekommen ist und was genau React für uns leistet.
Artikelserie
- Reiner funktionaler Stil
- Klassenbasierte Komponenten (Sie sind hier!)
Von Funktionen zu Klassen
Lassen Sie uns das gleiche Beispiel wie im ersten Beitrag weiterverwenden: ein Blog. Unsere funktionale BlogPost-Komponente sah so aus
var blogPostData = {
author: 'Brandon Smith',
title: 'A CSS Trick',
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};
function BlogPost(postData) {
return `<div class="post">
<h1>${postData.title}</h1>
<h3>By ${postData.author}</h3>
<p>${postData.body}</p>
</div>`;
}
document.querySelector('body').innerHTML = BlogPost(blogPostData);
Bei klassenbasierten Komponenten benötigen wir weiterhin dieselbe Rendering-Funktion, binden sie jedoch als Methode einer Klasse ein. Instanzen der Klasse halten ihre eigenen BlogPost-Daten und wissen, wie sie sich selbst rendern.
var blogPostData = {
author: 'Brandon Smith',
title: 'A CSS Trick',
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};
class BlogPost {
constructor(props) {
this.state = {
author: props.author,
title: props.title,
body: props.body
}
}
render() {
return `<div class="post">
<h1>${this.state.title}</h1>
<h3>By ${this.state.author}</h3>
<p>${this.state.body}</p>
</div>`;
}
}
var blogPostComponent = new BlogPost(blogPostData);
document.querySelector('body').innerHTML = blogPostComponent.render();
Zustand ändern
Der Vorteil eines klassenbasierten (objektorientierten) Programmierstils ist, dass er die Kapselung von Zustand ermöglicht. Stellen wir uns vor, unsere Blog-Seite erlaubt es Admin-Benutzern, ihre Blog-Posts direkt auf derselben Seite zu bearbeiten, auf der Leser sie sehen. Instanzen der BlogPost-Komponente könnten ihren eigenen Zustand verwalten, getrennt von der äußeren Seite und/oder anderen Instanzen von BlogPost. Wir können den Zustand über eine Methode ändern
class BlogPost {
constructor(props) {
this.state = {
author: props.author,
title: props.title,
body: props.body
}
}
render() {
return `<div class="post">
<h1>${this.state.title}</h1>
<h3>By ${this.state.author}</h3>
<p>${this.state.body}</p>
</div>`;
}
setBody(newBody) {
this.state.body = newBody;
}
}
In einem realen Szenario müsste diese Zustandsänderung jedoch entweder durch eine Netzwerkanfrage oder ein DOM-Ereignis ausgelöst werden. Lassen Sie uns untersuchen, wie letzteres aussehen würde, da dies der häufigste Fall ist.
Ereignisse behandeln
Normalerweise ist das Abhören von DOM-Ereignissen unkompliziert – verwenden Sie einfach element.addEventListener() –, aber die Tatsache, dass unsere Komponenten nur zu Zeichenfolgen ausgewertet werden und nicht zu tatsächlichen DOM-Elementen, macht es schwieriger. Wir haben kein Element, an das wir binden können, und das einfache Einfügen eines Funktionsaufrufs in onchange reicht nicht aus, da es nicht an unsere Komponentinstanz gebunden wäre. Wir müssen irgendwie von globaler Ebene auf unsere Komponente verweisen, wo der Snippet ausgewertet wird. Hier ist meine Lösung
document.componentRegistry = { };
document.nextId = 0;
class Component {
constructor() {
this._id = ++document.nextId;
document.componentRegistry[this._id] = this;
}
}
class BlogPost extends Component {
constructor(props) {
super();
this.state = {
author: props.author,
title: props.title,
body: props.body
}
}
render() {
return `<div class="post">
<h1>${this.state.title}</h1>
<h3>By ${this.state.author}</h3>
<textarea onchange="document.componentRegistry[${this._id}].setBody(this.value)">
${this.state.body}
</textarea>
</div>`;
}
setBody(newBody) {
this.state.body = newBody;
}
}
Okay, hier passiert einiges.
Referenzierung der Komponentinstanz
Zuerst mussten wir aus der HTML-Zeichenkette eine Referenz auf die aktuelle Instanz der Komponente erhalten. React kann dies einfacher tun, da JSX tatsächlich in eine Reihe von Funktionsaufrufen und nicht in eine HTML-Zeichenkette umgewandelt wird. Dies ermöglicht es dem Code, this direkt zu übergeben, und die Referenz auf das JavaScript-Objekt bleibt erhalten. Wir hingegen müssen eine Zeichenkette von JavaScript serialisieren, um sie in unsere Zeichenkette von HTML einzufügen. Daher muss die Referenz auf unsere Komponentinstanz irgendwie als Zeichenkette dargestellt werden. Um dies zu erreichen, weisen wir jeder Komponentinstanz beim Konstruieren eine eindeutige ID zu. Sie müssen dieses Verhalten nicht in einer übergeordneten Klasse unterbringen, aber es ist ein guter Anwendungsfall für Vererbung. Im Wesentlichen geschieht Folgendes: Wann immer eine BlogPost-Instanz konstruiert wird, erstellt sie eine neue ID, speichert sie als Eigenschaft auf sich selbst und registriert sich unter dieser ID in document.componentRegistry. Nun kann jeder JavaScript-Code überall unser Objekt abrufen, wenn er diese ID hat. Andere Komponenten, die wir möglicherweise schreiben, könnten ebenfalls von der Component-Klasse erben und automatisch eigene eindeutige IDs erhalten.
Aufruf der Methode
Wir können also die Komponentinstanz aus einer beliebigen JavaScript-Zeichenkette abrufen. Als Nächstes müssen wir die Methode darauf aufrufen, wenn unser Ereignis ausgelöst wird (onchange). Isolieren wir den folgenden Ausschnitt und gehen wir durch, was passiert
<textarea onchange="document.componentRegistry[${this._id}].setBody(this.value)">
${this.state.body}
</textarea>
Sie sind wahrscheinlich mit dem Hinzufügen von Event-Listenern vertraut, indem Sie Code in on_______ HTML-Attribute einfügen. Der darin enthaltene Code wird ausgewertet und ausgeführt, wenn das Ereignis ausgelöst wird.
document.componentRegistry[${this._id}] sucht im Komponenten-Registry und holt die Komponentinstanz anhand ihrer ID. Denken Sie daran, dass dies alles innerhalb einer Template-Zeichenkette geschieht, sodass ${this._id} zur ID der aktuellen Komponente ausgewertet wird. Der resultierende HTML wird so aussehen
<textarea onchange="document.componentRegistry[0].setBody(this.value)">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</textarea>
Wir rufen die Methode auf diesem Objekt auf und übergeben this.value (wobei this das Element ist, auf dem das Ereignis stattfindet; in unserem Fall <textarea>) als newBody.
Aktualisierung als Reaktion auf Zustandsänderungen
Der Wert unserer JavaScript-Variable wird geändert, aber wir müssen tatsächlich ein erneutes Rendering durchführen, um seinen Wert auf der gesamten Seite widergespiegelt zu sehen. In unserem letzten Artikel haben wir das Rendering so durchgeführt
function update() {
document.querySelector('body').innerHTML = BlogPost(blogPostData);
}
An dieser Stelle müssen wir einige Anpassungen für klassenbasierte Komponenten vornehmen. Wir wollen unsere Komponentinstanzen nicht jedes Mal wegwerfen und neu aufbauen, wenn wir rendern; wir wollen nur die HTML-Zeichenkette neu erstellen. Der interne Zustand muss erhalten bleiben. Unsere Objekte werden also separat existieren, und wir rufen einfach erneut render() auf
var blogPost = new BlogPost(blogPostData);
function update() {
document.querySelector('body').innerHTML = blogPost.render();
}
Wir müssen dann update() aufrufen, wann immer wir den Zustand ändern. Das ist eine weitere Sache, die React transparent für uns erledigt; seine setState()-Funktion ändert den Zustand und löst auch ein erneutes Rendering für diese Komponente aus. Das müssen wir manuell tun
// ...
setBody(newBody) {
this.state.body = newBody;
update();
}
// ...
Beachten Sie, dass es selbst bei einer komplexen verschachtelten Struktur von Komponenten immer nur eine einzige update()-Funktion gibt und diese immer auf die Root-Komponente angewendet wird.
Kindkomponenten
React (sowie praktisch alle anderen JavaScript-Frameworks) unterscheidet zwischen Elementen und Komponenten, die eine Komponente ausmachen, und denen, die ihre Kinder sind. Kinder können von außen übergeben werden, was uns erlaubt, benutzerdefinierte Komponenten zu schreiben, die Container für beliebige andere Inhalte sind. Das können wir auch tun.
class BlogPost extends Component {
constructor(props, children) {
super();
this.children = children;
this.state = {
author: props.author,
title: props.title,
body: props.body
}
}
render() {
return `<div class="post">
<h1>${this.state.title}</h1>
<h3>By ${this.state.author}</h3>
<textarea onchange="document.componentRegistry[${this._id}].setBody(this.value)">
${this.state.body}
</textarea>
<div>
${this.children.map((child) => child.render()).join('')}
</div>
</div>`;
}
setBody(newBody) {
this.state.body = newBody;
update();
}
}
Dies ermöglicht es uns, Nutzungs-Code wie den folgenden zu schreiben
var adComponents = ...;
var blogPost = new BlogPost(blogPostData, adComponents);
was die Komponenten an der vorgesehenen Stelle im Markup einfügen wird.
Abschließende Gedanken
React scheint einfach, aber es tut viele subtile Dinge, um uns das Leben viel einfacher zu machen. Das Offensichtlichste ist die Leistung; nur die Komponenten neu zu rendern, deren Zustand sich aktualisiert, und die durchgeführten DOM-Operationen drastisch zu minimieren. Aber einige der weniger offensichtlichen Dinge sind auch wichtig.
Eines davon ist, dass React durch granulare DOM-Änderungen statt durch vollständiges Neuerstellen des DOM einige natürliche DOM-Zustände beibehält, die bei unserer Technik verloren gehen. Dinge wie CSS-Übergänge, vom Benutzer vergrößerte Textbereiche, Fokus und Cursorposition in einem Eingabefeld gehen verloren, wenn wir das DOM löschen und neu erstellen. Für unseren Anwendungsfall ist das machbar. Aber in vielen Situationen ist es das vielleicht nicht. Natürlich könnten wir DOM-Änderungen selbst vornehmen, aber dann sind wir wieder am Anfang und verlieren unsere deklarative, funktionale Syntax.
React bietet uns die Vorteile der DOM-Manipulation und ermöglicht es uns gleichzeitig, unseren Code in einem wartungsfreundlicheren, deklarativeren Stil zu schreiben. Wir haben gezeigt, dass Vanilla JavaScript beides kann, aber nicht beides gleichzeitig haben kann.
Artikelserie
- Reiner funktionaler Stil
- Klassenbasierte Komponenten (Sie sind hier!)
Nur zur Information – man muss das `super` oder `constructor`-Idiom nie in vanilla JS verwenden. Man muss nicht einmal `Class` verwenden. Das Modul-Pattern kapselt all diese Konzepte und erlaubt es, eine Masse von Code prägnant zu organisieren.
Außerdem, warum auf '$' beschränken? Ich weiß, es ist vertraut, aber es ist hässlich wie die Sünde und verwirrt die Idee hinter dem, was passiert.
Klassen dienen einem anderen Zweck als Module. Eine Klasse kapselt nicht nur; sie erlaubt mehrere Instanziierungen eines bestimmten Objekttyps. Es stimmt, dass der gleiche Effekt mit Prototypen anstelle der neuen Klassensyntax erzielt werden könnte, aber die Klassensyntax ist dort, wo sie angebracht ist, viel lesbarer und ein besseres Analogon zu den Framework-Entsprechungen dieses Anwendungsfalls, da die meisten von ihnen sie entweder selbst verwenden oder TypeScript verwenden, das eine ähnliche Syntax hat.
Ich glaube, Sie missverstehen die Verwendung von
$. Es ist Teil der ES6 Template-Literal-Syntax: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literalsDu hast Recht wegen des $. Es fühlt sich immer noch wie eine nichtssagende Abkürzung an und ich frage mich, ob das zu Verwechslungen mit jQuery führt.
Module ermöglichen jedoch eine sehr einfache Wiederverwendung ohne die umständliche Prototypen-Schnittstelle. Das Klassen-Idiom ist nicht lesbarer als die Verwendung eines Crockford-Style Revealing Module. Ich behaupte, dass das Modul viel sauberer ist – besonders wegen `super` und `constructor`. Es mag für Leute, die Sprachen verwenden, die für klassenbasierte Strukturen entwickelt wurden, vertrauter sein, aber es ist völlig unnötig und fügt mehr Ballast als Zucker hinzu.
Ich war mir des Crockford-Modul-Patterns nicht bewusst, ich dachte, Sie sprächen von ES6-Modulen. Nachdem ich es nachgeschlagen habe, ja, man könnte diese Technik ziemlich einfach anpassen, um Crockford-Module anstelle von ES6-Klassen zu verwenden. Der Unterschied ist eine Frage der persönlichen Vorliebe; für viele Leute (ich würde sogar sagen, die meisten) ist die Standard-Klassensyntax lesbarer und vertrauter. Aber natürlich erlaubt sie keine privaten Mitglieder und wird nur von relativ neueren Browsern unterstützt, so dass es Kompromisse gibt. Zur Demonstration habe ich Vertrautheit und Ähnlichkeit mit bestehenden Lösungen gegenüber anderen Merkmalen bevorzugt, aber in einem realen Anwendungsfall gibt es Argumente für beide.
Ich habe auf diesen Folgeartikel gewartet. Danke fürs Schreiben, denn er hilft zu erklären, warum man React verwenden würde… es scheint für all die versteckten Dinge zu sein, die einem beim Erstellen einer Web-"App" helfen.
Im Gegensatz dazu scheint Ihr VanillaJS-Beispiel hier wie jedes beliebige herkömmliche Template-System in JS zu sein, nicht mehr und nicht weniger. Fehlt mir etwas?
Meine Erfahrung mit komplett JS-gerenderten "Websites" ist, dass sie einfach furchtbar sind. Wie die alten Super-HTML-Hacks und Tabellenlayouts, oder Flash-Intros, animierte GIF-Spams usw. Das Schlimmste heute ist ein JS-"Lade"-Spinner, der 10 Sekunden lang für eine "Website" angezeigt wird ( wenn es einfach nur das sein sollte). Seufz
Die langsamsten, am wenigsten reaktionsschnellen, grellsten Schrottteile scheinen von aufgeblähten JS-Frameworks angetrieben zu werden. Wenn sie nur das verwenden würden, was dieser Artikel zeigt, würden sie wahrscheinlich sofort laden. Aber es scheint, dass React, Angular usw. normale Websites in "Apps" verwandelt haben, zum Nachteil des Besuchers.
Ich bin bereit, belehrt zu werden, da ich in den letzten 20 Jahren Webentwicklung viel ändern musste, aber wozu dienen React-Style-"Apps" für etwas wie einen Blog (der nur eine Website ist)? Eine direkte Antwort wäre willkommen, denn zu diesem Zeitpunkt scheint React keine sehr gute Lösung für reines Templating zu sein, aber dann wieder auch VanillaJS nicht. Oder um diese Frage anders zu stellen: Ist React das "richtige Werkzeug für den Job", wenn der Job nur eine Website ist?
Danke fürs Lesen :)
Sie haben Recht, dass mein Beispiel im Grunde nur einfaches Templating und nicht viel mehr ist. Der Hauptzweck der Artikel war es, 1) den Leuten zu zeigen, dass dies jetzt auf der Client-Seite möglich ist, ohne auch nur ein reguläres Templating-Framework wie Handlebars.js, und 2) die Vorteile hervorzuheben, die ein modernes Framework hinzufügt.
Es gibt viele Debatten darüber, ob Frameworks wie React für alle Websites oder nur für "Web-Apps" verwendet werden sollten und was das überhaupt bedeutet. Chris Coyiers Beitrag zu dem Thema (https://css-tricks.de/project-need-react/) und Sacha Greifs Gegenargument (https://css-tricks.de/projects-need-react/) waren Teil dessen, was mich zu diesem Artikel veranlasste.
Meine persönliche Haltung ist, dass Frameworks für etwas weitgehend Statisches wie einen Blog nicht lohnen, aber dass eine Technik wie die, die ich hier beschrieben habe, es vielleicht sein könnte (obwohl das teilweise meiner Faulheit bei der Konfiguration vieler Build-Tools geschuldet ist). Was das Client- vs. Server-Templating angeht, so ist das meiner Meinung nach eher eine Frage der persönlichen Vorliebe als alles andere. Etwas Vanilla JavaScript ohne Framework wird die Ladezeit der Seite wahrscheinlich nicht merklich erhöhen, aber ich habe meine eigene Strategie auch noch nicht auf einer realen Website ausprobiert, so dass es möglich ist, dass sie einige Einschränkungen hat, die sich erst beim Hochskalieren bemerkbar machen.
Danke Vanderson und Brandon fürs Klären. Das beschäftigt mich schon eine Weile :)
Brandon, danke für einen exzellenten Artikel! Könnten Sie den Artikel mit Live-Beispielen auf Codepen ergänzen? Bei den letzten 2 Beispielen hat es nicht funktioniert. Danke im Voraus
Hallo Brandon, danke für deinen Post.
Es war ein Licht in meinem Gehirn, ich konnte viel damit lernen.
Ich sehe nur einen Fehler in diesem Ausdruck
${this.children.map((child) => child.render()).join(”)}
Es braucht ein return vor child.render(), denn ohne return wird nichts ausgegeben.
Tatsächlich, bei ES6-Pfeilfunktionen, wenn Sie nur eine Anweisung im Funktionskörper haben, können Sie die geschweiften Klammern und auch das `return`-Schlüsselwort weglassen :)
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions