React und JSX in Vanilla JS emulieren

Veröffentlicht: 2022-03-11

Nur wenige Menschen mögen Frameworks nicht, aber selbst wenn Sie einer von ihnen sind, sollten Sie die Funktionen, die das Leben ein wenig einfacher machen, zur Kenntnis nehmen und übernehmen.

Ich war in der Vergangenheit gegen die Verwendung von Frameworks. In letzter Zeit habe ich jedoch die Erfahrung gemacht, in einigen meiner Projekte mit React und Angular zu arbeiten. Die ersten paar Male, als ich meinen Code-Editor öffnete und anfing, Code in Angular zu schreiben, fühlte es sich komisch und unnatürlich an; insbesondere nach mehr als zehn Jahren Codierung ohne die Verwendung von Frameworks. Nach einer Weile beschloss ich, mich dem Erlernen dieser Technologien zu verpflichten. Sehr schnell wurde ein großer Unterschied deutlich: Es war so einfach, das DOM zu manipulieren, die Reihenfolge der Knoten bei Bedarf so einfach anzupassen, und es brauchte nicht Seiten um Seiten Code, um eine Benutzeroberfläche zu erstellen.

Obwohl ich immer noch die Freiheit bevorzuge, nicht an ein Framework oder eine Architektur gebunden zu sein, konnte ich die Tatsache nicht ignorieren, dass das Erstellen von DOM-Elementen in einem solchen viel bequemer ist. Also fing ich an, nach Möglichkeiten zu suchen, die Erfahrung in Vanilla JS nachzuahmen. Mein Ziel ist es, einige dieser Ideen aus React zu extrahieren und zu demonstrieren, wie die gleichen Prinzipien in einfachem JavaScript (oft als Vanilla JS bezeichnet) implementiert werden können, um das Leben von Entwicklern ein wenig einfacher zu machen. Um dies zu erreichen, erstellen wir eine einfache App zum Durchsuchen von GitHub-Projekten.

Einfache GitHub-Such-App

Die App, die wir bauen.

Unabhängig davon, wie wir ein Frontend mit JavaScript erstellen, werden wir auf das DOM zugreifen und es manipulieren. Für unsere Anwendung müssen wir eine Darstellung jedes Repositorys erstellen (Miniaturansicht, Name und Beschreibung) und es als Listenelement zum DOM hinzufügen. Wir werden die GitHub-Such-API verwenden, um unsere Ergebnisse abzurufen. Und da wir über JavaScript sprechen, lassen Sie uns die JavaScript-Repositories durchsuchen. Wenn wir die API abfragen, erhalten wir die folgende JSON-Antwort:

 { "total_count": 398819, "incomplete_results": false, "items": [ { "id": 28457823, "name": "freeCodeCamp", "full_name": "freeCodeCamp/freeCodeCamp", "owner": { "login": "freeCodeCamp", "id": 9892522, "avatar_url": "https://avatars0.githubusercontent.com/u/9892522?v=4", "gravatar_id": "", "url": "https://api.github.com/users/freeCodeCamp", "site_admin": false }, "private": false, "html_url": "https://github.com/freeCodeCamp/freeCodeCamp", "description": "The https://freeCodeCamp.org open source codebase "+ "and curriculum. Learn to code and help nonprofits.", // more omitted information }, //... ] }

Der Ansatz von React

React macht es sehr einfach, HTML-Elemente in die Seite zu schreiben, und ist eine der Funktionen, die ich schon immer haben wollte, wenn ich Komponenten in reinem JavaScript schreibe. React verwendet JSX, das regulärem HTML sehr ähnlich ist.

Das ist jedoch nicht das, was der Browser liest.

Unter der Haube verwandelt React JSX in eine Reihe von Aufrufen einer React.createElement Funktion. Werfen wir einen Blick auf ein Beispiel für JSX, das ein Element aus der GitHub-API verwendet, und sehen, was es übersetzt.

 <div className="repository"> <div>{item.name}</div> <p>{item.description}</p> <img src={item.owner.avatar_url} /> </div>;
 ; React.createElement( "div", { className: "repository" }, React.createElement( "div", null, item.name ), React.createElement( "p", null, item.description ), React.createElement( "img", { src: item.owner.avatar_url } ) );

JSX ist sehr einfach. Sie schreiben normalen HTML-Code und fügen Daten aus dem Objekt ein, indem Sie geschweifte Klammern hinzufügen. JavaScript-Code innerhalb der Klammern wird ausgeführt und der Wert wird in das resultierende DOM eingefügt. Einer der Vorteile von JSX ist, dass React ein virtuelles DOM (eine virtuelle Darstellung der Seite) erstellt, um Änderungen und Aktualisierungen zu verfolgen. Anstatt den gesamten HTML-Code neu zu schreiben, ändert React das DOM der Seite, sobald die Informationen aktualisiert werden. Dies ist eines der Hauptprobleme, zu dessen Lösung React entwickelt wurde.

jQuery-Ansatz

Entwickler haben früher häufig jQuery verwendet. Ich möchte es hier erwähnen, weil es immer noch beliebt ist und auch, weil es der Lösung in reinem JavaScript ziemlich nahe kommt. jQuery ruft einen Verweis auf einen DOM-Knoten (oder eine Sammlung von DOM-Knoten) ab, indem es das DOM abfragt. Es umschließt diese Referenz auch mit verschiedenen Funktionalitäten zum Ändern ihres Inhalts.

Während jQuery seine eigenen DOM-Konstruktionswerkzeuge hat, sehe ich am häufigsten in freier Wildbahn nur HTML-Verkettung. Beispielsweise können wir HTML-Code in die ausgewählten Knoten einfügen, indem wir die Funktion html() aufrufen. Wenn wir den Inhalt eines div -Knotens mit der Klasse demo-container ändern möchten, können wir dies laut jQuery-Dokumentation folgendermaßen tun:

 $( "div.demo-container" ).html( "<p>All new content.<em>You bet!</em></p>" );

Dieser Ansatz macht es einfach, DOM-Elemente zu erstellen; Wenn wir jedoch Knoten aktualisieren müssen, müssen wir die benötigten Knoten abfragen oder (häufiger) darauf zurückgreifen, das gesamte Snippet neu zu erstellen, wenn eine Aktualisierung erforderlich ist.

DOM-API-Ansatz

JavaScript in Browsern verfügt über eine integrierte DOM-API, die uns direkten Zugriff auf das Erstellen, Ändern und Löschen der Knoten auf einer Seite ermöglicht. Dies spiegelt sich im Ansatz von React wider, und durch die Verwendung der DOM-API kommen wir den Vorteilen dieses Ansatzes einen Schritt näher. Wir ändern nur die Elemente der Seite, die tatsächlich geändert werden müssen. React verfolgt jedoch auch ein separates virtuelles DOM. Durch den Vergleich der Unterschiede zwischen dem virtuellen und dem tatsächlichen DOM kann React dann erkennen, welche Teile geändert werden müssen.

Diese zusätzlichen Schritte sind manchmal nützlich, aber nicht immer, und die direkte Bearbeitung des DOM kann effizienter sein. Wir können neue DOM-Knoten mit der Funktion _document.createElement_ erstellen, die eine Referenz auf den erstellten Knoten zurückgibt. Durch das Verfolgen dieser Referenzen können wir auf einfache Weise nur die Knoten ändern, die den zu aktualisierenden Teil enthalten.

Unter Verwendung der gleichen Struktur und Datenquelle wie im JSX-Beispiel können wir das DOM folgendermaßen aufbauen:

 var item = document.createElement('div'); item.className = 'repository'; var nameNode = document.createElement('div'); nameNode.innerHTML = item.name item.appendChild(nameNode); var description = document.createElement('p'); description.innerHTML = item.description; item.appendChild(description ); var image = new Image(); Image.src = item.owner.avatar_url; item.appendChild(image); document.body.appendChild(item);

Wenn Sie nur an die Effizienz der Codeausführung denken, ist dieser Ansatz sehr gut. Effizienz bemisst sich aber nicht nur an Ausführungsgeschwindigkeit, sondern auch an Wartungsfreundlichkeit, Skalierbarkeit und Plastizität. Das Problem bei diesem Ansatz ist, dass er sehr ausführlich und manchmal verworren ist. Wir müssen eine Reihe von Funktionsaufrufen schreiben, selbst wenn wir nur eine Grundstruktur konstruieren. Der zweite große Nachteil ist die schiere Anzahl der erstellten und verfolgten Variablen. Angenommen, eine Komponente, an der Sie arbeiten, enthält 30 eigene DOM-Elemente. Sie müssen 30 verschiedene DOM-Elemente und Variablen erstellen und verwenden. Sie können einige von ihnen wiederverwenden und auf Kosten der Wartbarkeit und Plastizität etwas jonglieren, aber es kann sehr schnell sehr chaotisch werden.

Ein weiterer wesentlicher Nachteil liegt in der Anzahl der Codezeilen, die Sie schreiben müssen. Mit der Zeit wird es immer schwieriger, Elemente von einem Elternteil zu einem anderen zu verschieben. Das ist eine Sache, die ich an React wirklich schätze. Ich kann die JSX-Syntax anzeigen und in wenigen Sekunden erfahren, welcher Knoten wo enthalten ist, und bei Bedarf ändern. Und auch wenn es zunächst so aussieht, als wäre es keine große Sache, gibt es bei den meisten Projekten ständige Änderungen, die Sie dazu bringen, nach einem besseren Weg zu suchen.

Vorgeschlagene Lösung

Das direkte Arbeiten mit dem DOM funktioniert und erledigt die Arbeit, aber es macht auch das Erstellen der Seite sehr ausführlich, insbesondere wenn wir HTML-Attribute hinzufügen und Knoten verschachteln müssen. Die Idee wäre also, einige der Vorteile der Arbeit mit Technologien wie JSX zu nutzen und unser Leben einfacher zu machen. Die Vorteile, die wir zu replizieren versuchen, sind die folgenden:

  1. Schreiben Sie Code in HTML-Syntax, damit die Erstellung von DOM-Elementen einfach zu lesen und zu ändern ist.
  2. Da wir kein Äquivalent von virtuellem DOM wie im Fall von React verwenden, müssen wir eine einfache Möglichkeit haben, die Knoten, an denen wir interessiert sind, anzuzeigen und zu verfolgen.

Hier ist eine einfache Funktion, die dies mit einem HTML-Snippet bewerkstelligen würde.

 Browser.DOM = function (html, scope) { // Creates empty node and injects html string using .innerHTML // in case the variable isn't a string we assume is already a node var node; if (html.constructor === String) { var node = document.createElement('div'); node.innerHTML = html; } else { node = html; } // Creates of uses and object to which we will create variables // that will point to the created nodes var _scope = scope || {}; // Recursive function that will read every node and when a node // contains the var attribute add a reference in the scope object function toScope(node, scope) { var children = node.children; for (var iChild = 0; iChild < children.length; iChild++) { if (children[iChild].getAttribute('var')) { var names = children[iChild].getAttribute('var').split('.'); var obj = scope; while (names.length > 0) { var _property = names.shift(); if (names.length == 0) { obj[_property] = children[iChild]; } else { if (!obj.hasOwnProperty(_property)){ obj[_property] = {}; } obj = obj[_property]; } } } toScope(children[iChild], scope); } } toScope(node, _scope); if (html.constructor != String) { return html; } // If the node in the highest hierarchy is one return it if (node.childNodes.length == 1) { // if a scope to add node variables is not set // attach the object we created into the highest hierarchy node // by adding the nodes property. if (!scope) { node.childNodes[0].nodes = _scope; } return node.childNodes[0]; } // if the node in highest hierarchy is more than one return a fragment var fragment = document.createDocumentFragment(); var children = node.childNodes; // add notes into DocumentFragment while (children.length > 0) { if (fragment.append){ fragment.append(children[0]); }else{ fragment.appendChild(children[0]); } } fragment.nodes = _scope; return fragment; }

Die Idee ist einfach, aber mächtig; Wir senden der Funktion den HTML-Code, den wir erstellen möchten, als Zeichenfolge. In der HTML-Zeichenfolge fügen wir den Knoten, für die Referenzen erstellt werden sollen, ein var-Attribut hinzu. Der zweite Parameter ist ein Objekt, in dem diese Referenzen gespeichert werden. Wenn nicht angegeben, erstellen wir eine „Knoten“-Eigenschaft für den zurückgegebenen Knoten oder das Dokumentfragment (falls der höchste Hierarchieknoten mehr als einer ist). Alles wird in weniger als 60 Codezeilen erledigt.

Die Funktion arbeitet in drei Schritten:

  1. Erstellen Sie einen neuen leeren Knoten und verwenden Sie innerHTML in diesem neuen Knoten, um die gesamte DOM-Struktur zu erstellen.
  2. Iterieren Sie über die Knoten, und fügen Sie, wenn das var-Attribut vorhanden ist, eine Eigenschaft im Bereichsobjekt hinzu, die auf den Knoten mit diesem Attribut zeigt.
  3. Gibt den obersten Knoten in der Hierarchie oder ein Dokumentfragment zurück, falls es mehr als eines gibt.

Wie sieht nun unser Code zum Rendern des Beispiels aus?

 var UI = {}; var template = ''; template += '<div class="repository">' template += ' <div var="name"></div>'; template += ' <p var="text"></p>' template += ' <img var="image"/>' template += '</div>'; var item = Browser.DOM(template, UI); UI.name.innerHTML = data.name; UI.text.innerHTML = data.description; UI.image.src = data.owner.avatar_url;

Zuerst definieren wir das Objekt (UI), in dem wir die Referenzen zu den erstellten Knoten speichern. Wir erstellen dann die HTML-Vorlage, die wir verwenden werden, als Zeichenfolge und markieren die Zielknoten mit einem „var“-Attribut. Danach rufen wir die Funktion Browser.DOM mit der Vorlage und dem leeren Objekt auf, das die Referenzen speichern wird. Schließlich verwenden wir die gespeicherten Referenzen, um die Daten innerhalb der Knoten zu platzieren.

Dieser Ansatz trennt auch den Aufbau der DOM-Struktur und das Einfügen der Daten in separate Schritte, was dazu beiträgt, den Code organisiert und gut strukturiert zu halten. Dies ermöglicht es uns, die DOM-Struktur separat zu erstellen und die Daten auszufüllen (oder zu aktualisieren), wenn sie verfügbar sind.

Fazit

Während einige von uns die Idee nicht mögen, auf Frameworks umzusteigen und die Kontrolle abzugeben, ist es wichtig, dass wir die Vorteile erkennen, die diese Frameworks mit sich bringen. Es gibt einen Grund, warum sie so beliebt sind.

Und obwohl ein Framework möglicherweise nicht immer Ihrem Stil oder Ihren Bedürfnissen entspricht, können einige Funktionalitäten und Techniken übernommen, emuliert oder manchmal sogar von einem Framework entkoppelt werden. Einige Dinge werden bei der Übersetzung immer verloren gehen, aber viel kann gewonnen und zu einem winzigen Bruchteil der Kosten verwendet werden, die ein Framework mit sich bringt.