Kapitel 1. Der Einstiegskram
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Beginnen wir mit einem Haftungsausschluss: React wurde dafür gemacht, von allen genutzt zu werden. Du könntest sogar durch dein Leben gehen, ohne dieses Buch gelesen zu haben, und React ohne Probleme weiter benutzen! Dieses Buch taucht viel tiefer in React ein, für diejenigen unter uns, die neugierig auf die zugrundeliegenden Mechanismen, fortgeschrittenen Muster und bewährten Methoden sind . Es eignet sich besser, um zu wissen, wie React funktioniert, als um zu lernen, wie man React benutzt. Es gibt viele andere Bücher, die mit der Absicht geschrieben wurden, den Leuten beizubringen, wie man React als Endbenutzer benutzt. Im Gegensatz dazu hilft dir dieses Buch, React auf der Ebene eines Bibliotheks- oder Framework-Autors zu verstehen und nicht auf der eines Endnutzers. Deshalb wollen wir gemeinsam in die Tiefe gehen und ganz oben anfangen: mit den übergeordneten Einsteigerthemen. Wir beginnen mit den Grundlagen von React und tauchen dann immer tiefer in die Details ein, wie React funktioniert.
In diesem Kapitel werden wir darüber sprechen, warum es React gibt, wie es funktioniert und welche Probleme es löst. Wir gehen auf die ursprüngliche Inspiration und das Design ein und verfolgen die Entwicklung von den bescheidenen Anfängen bei Facebook bis hin zu der heute weit verbreiteten Lösung. Dieses Kapitel ist eine Art Meta-Kapitel (kein Wortspiel beabsichtigt), denn es ist wichtig, den Kontext von React zu verstehen, bevor wir in die Details eintauchen.
Warum ist React eine Sache?
Die Antwort lautet in einem Wort: Aktualisierungen. In den Anfängen des Internets hatten wir viele statische Seiten. Wir füllten Formulare aus, drückten auf "Submit" und luden eine völlig neue Seite. Das war eine Zeit lang in Ordnung, aber mit der Zeit entwickelten sich die Möglichkeiten des Internets erheblich. Mit den wachsenden Möglichkeiten wuchs auch unser Wunsch nach einem besseren Nutzererlebnis im Internet. Wir wollten, dass sich Dinge sofort aktualisieren, ohne dass wir darauf warten müssen, dass eine neue Seite gerendert und geladen wird. Wir wollten, dass sich das Web und seine Seiten schneller und "unmittelbarer" anfühlen. Das Problem war, dass diese sofortigen Aktualisierungen aus verschiedenen Gründen in großem Maßstab schwer zu realisieren waren:
- Leistung
-
Die Aktualisierung von Webseiten führte oft zu Engpässen bei der Leistung, weil wir Arbeiten durchführen mussten, die dazu führten, dass die Browser das Layout einer Seite neu berechneten (Reflow genannt) und die Seite neu anzeigten.
- Verlässlichkeit
-
Es war schwierig, den Status zu verfolgen und sicherzustellen, dass der Status in einem umfangreichen Webangebot konsistent ist, weil wir den Status an mehreren Stellen verfolgen und sicherstellen mussten, dass der Status an all diesen Stellen konsistent ist. Das war besonders schwierig, wenn mehrere Leute an derselben Codebasis arbeiteten.
- Sicherheit
-
Wir mussten sicher sein, dass wir alle HTML- und JavaScript-Daten, die wir in die Seite einfügen, bereinigen, um Exploits wie Cross-Site Scripting (XSS) und Cross-Site Request Forgery (CSRF) zu verhindern.
Um vollständig zu verstehen und zu würdigen, wie React diese Probleme für uns löst, müssen wir den Kontext verstehen, in dem React entstanden ist, und die Welt ohne oder vor React. Das wollen wir jetzt tun.
Die Welt vor React
Das waren einige der großen Probleme für diejenigen von uns, die vor React Web-Apps entwickelten. Wir mussten herausfinden, wie unsere Apps schnell und unmittelbar funktionieren, aber auch für Millionen von Nutzern skaliert werden können und zuverlässig und sicher funktionieren. Nehmen wir zum Beispiel einen Button-Klick: Wenn ein Nutzer auf einen Button klickt, wollen wir die Benutzeroberfläche aktualisieren, um zu zeigen, dass der Button angeklickt wurde. Wir müssen mindestens vier verschiedene Zustände berücksichtigen, in denen sich die Benutzeroberfläche befinden kann:
- Vorab-Klick
-
Die Schaltfläche ist in ihrem Standardzustand und wurde nicht angeklickt.
- Angeklickt, aber ausstehend
-
Die Schaltfläche wurde angeklickt, aber die Aktion, die die Schaltfläche ausführen soll, ist noch nicht abgeschlossen.
- Geklickt und erfolgreich
-
Die Schaltfläche wurde angeklickt und die Aktion, die die Schaltfläche ausführen soll, wurde abgeschlossen. Von hier aus können wir die Schaltfläche in den Zustand vor dem Anklicken zurückversetzen oder die Farbe der Schaltfläche ändern (grün), um den Erfolg anzuzeigen.
- Geklickt und fehlgeschlagen
-
Die Schaltfläche wurde angeklickt, aber die Aktion, die die Schaltfläche ausführen sollte, ist fehlgeschlagen. Wir können die Schaltfläche in den Zustand vor dem Anklicken zurückversetzen oder die Farbe der Schaltfläche ändern (rot), um den Fehler anzuzeigen.
Sobald wir diese Zustände haben, müssen wir herausfinden, wie wir die Benutzeroberfläche aktualisieren können, um sie widerzuspiegeln. Oft erfordert die Aktualisierung der Benutzeroberflächedie folgenden Schritte:
-
Finde die Schaltfläche in der Host-Umgebung (oft der Browser) mit einer Art Element-Locator-API, wie
document.querySelector
oderdocument.getElementById
. -
Füge der Schaltfläche Ereignis-Listener hinzu, um auf Klick-Ereignisse zu warten.
-
Führe alle Statusaktualisierungen als Reaktion auf Ereignisse durch.
-
Wenn die Schaltfläche die Seite verlässt, entferne die Ereignis-Listener und bereinige alle Zustände.
Das ist ein einfaches Beispiel, aber ein gutes, um damit anzufangen. Nehmen wir an, wir haben einen Button mit der Aufschrift "Gefällt mir", und wenn ein Nutzer darauf klickt, wollen wir den Button auf "Gefällt mir" aktualisieren. Wie machen wir das? Zunächst einmal haben wir ein HTML-Element:
<
button
>
Like</
button
>
Wir brauchen eine Möglichkeit, diese Schaltfläche mit JavaScript zu referenzieren, also geben wir ihr ein id
-Attribut:
<
button
id
=
"likeButton"
>
Like</
button
>
Super! Jetzt, wo es eine id
gibt, kann JavaScript mit ihr arbeiten, um sie interaktiv zu machen. Mitdocument.getElementById
erhalten wir einen Verweis auf die Schaltfläche und fügen der Schaltfläche einen Ereignis-Listener hinzu, der auf Klick-Ereignisse wartet:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
// do something
});
Jetzt, da wir einen Ereignis-Listener haben, können wir etwas tun, wenn die Schaltfläche angeklickt wird. Nehmen wir an, wir wollen die Schaltfläche so aktualisieren, dass sie mit "Gefällt mir" beschriftet ist, wenn sie angeklickt wird. Das können wir tun, indem wir den Textinhalt der Schaltfläche aktualisieren:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
likeButton
.
textContent
=
"Liked"
;
});
Toll! Jetzt haben wir eine Schaltfläche, auf der "Gefällt mir" steht, und wenn sie angeklickt wird, heißt es "Geliebt". Das Problem dabei ist, dass wir Dinge nicht "liken" können. Das müssen wir ändern und die Schaltfläche so aktualisieren, dass sie wieder "Gefällt mir" sagt, wenn sie im "Gefällt mir"-Zustand angeklickt wird. Wir müssen der Schaltfläche einen Status hinzufügen, damit wir wissen, ob sie angeklickt wurde oder nicht. Das können wir tun, indem wir ein data-liked
Attribut zur Schaltfläche hinzufügen:
<
button
id
=
"likeButton"
data-liked
=
"false"
>
Like</
button
>
Jetzt, da wir dieses Attribut haben, können wir damit verfolgen, ob die Schaltfläche angeklickt wurde oder nicht. Wir können den Textinhalt der Schaltfläche anhand des Wertes dieses Attributs aktualisieren:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
const
liked
=
likeButton
.
getAttribute
(
"data-liked"
)
===
"true"
;
likeButton
.
setAttribute
(
"data-liked"
,
!
liked
);
likeButton
.
textContent
=
liked
?
"Like"
:
"Liked"
;
});
Warte, aber wir ändern nur die textContent
der Schaltfläche! Wir speichern den "Gefällt mir"-Status nicht wirklich in einer Datenbank. Normalerweise müssten wir dazu über das Netzwerk kommunizieren, etwa so:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
var
liked
=
likeButton
.
getAttribute
(
"data-liked"
)
===
"true"
;
// communicate over the network
var
xhr
=
new
XMLHttpRequest
();
xhr
.
open
(
"POST"
,
"/like"
,
true
);
xhr
.
setRequestHeader
(
"Content-Type"
,
"application/json;charset=UTF-8"
);
xhr
.
onload
=
function
()
{
if
(
xhr
.
status
>=
200
&&
xhr
.
status
<
400
)
{
// Success!
likeButton
.
setAttribute
(
"data-liked"
,
!
liked
);
likeButton
.
textContent
=
liked
?
"Like"
:
"Liked"
;
}
else
{
// We reached our target server, but it returned an error
console
.
error
(
"Server returned an error:"
,
xhr
.
statusText
);
}
};
xhr
.
onerror
=
function
()
{
// There was a connection error of some sort
console
.
error
(
"Network error."
);
};
xhr
.
send
(
JSON
.
stringify
({
liked
:
!
liked
}));
});
Natürlich verwenden wir XMLHttpRequest
und var
, um der Zeit gerecht zu werden. React wurde 2013 als Open-Source-Software veröffentlicht, und die allgemeinerefetch
API wurde 2015 eingeführt. Zwischen XMLHttpRequest
undfetch
gab es jQuery, das die Komplexität mit Primitiven wie $.ajax()
, $.post()
, etc. abstrahierte.
Wenn wir das heute schreiben würden, würde es eher so aussehen:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
const
liked
=
likeButton
.
getAttribute
(
"data-liked"
)
===
"true"
;
// communicate over the network
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
!
liked
}),
}).
then
(()
=>
{
likeButton
.
setAttribute
(
"data-liked"
,
!
liked
);
likeButton
.
textContent
=
liked
?
"Like"
:
"Liked"
;
});
});
Ohne zu sehr abzuschweifen, geht es jetzt darum, dass wir über das Netzwerk kommunizieren, aber was ist, wenn die Netzwerkanfrage fehlschlägt? Dann müssen wir den Textinhalt der Schaltfläche aktualisieren, um den Fehler anzuzeigen. Das können wir tun, indem wir der Schaltfläche ein data-failed
Attribut hinzufügen:
<
button
id
=
"likeButton"
data-liked
=
"false"
data-failed
=
"false"
>
Like</
button
>
Jetzt können wir den Textinhalt der Schaltfläche anhand des Wertes dieses Attributs aktualisieren:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
const
liked
=
likeButton
.
getAttribute
(
"data-liked"
)
===
"true"
;
// communicate over the network
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
!
liked
}),
})
.
then
(()
=>
{
likeButton
.
setAttribute
(
"data-liked"
,
!
liked
);
likeButton
.
textContent
=
liked
?
"Like"
:
"Liked"
;
})
.
catch
(()
=>
{
likeButton
.
setAttribute
(
"data-failed"
,
true
);
likeButton
.
textContent
=
"Failed"
;
});
});
Es gibt noch einen weiteren Fall: den Prozess, in dem du eine Sache gerade "magst". Das ist der "Schwebezustand". Um dies im Code abzubilden, müssen wir ein weiteres Attribut für den Status "in der Schwebe" auf der Schaltfläche festlegen, indem wirdata-pending
hinzufügen:
<
button
id
=
"likeButton"
data-pending
=
"false"
data-liked
=
"false"
data-failed
=
"false"
>
Like</
button
>
Jetzt können wir die Schaltfläche deaktivieren, wenn eine Netzwerkanfrage läuft, damit mehrere Klicks nicht zu einer Warteschlange von Netzwerkanfragen und damit zu merkwürdigen Race Conditions und einer Überlastung des Servers führen. Wir können das so machen:
const
likeButton
=
document
.
getElementById
(
"likeButton"
);
likeButton
.
addEventListener
(
"click"
,
()
=>
{
const
liked
=
likeButton
.
getAttribute
(
"data-liked"
)
===
"true"
;
const
isPending
=
likeButton
.
getAttribute
(
"data-pending"
)
===
"true"
;
likeButton
.
setAttribute
(
"data-pending"
,
"true"
);
likeButton
.
setAttribute
(
"disabled"
,
"disabled"
);
// communicate over the network
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
!
liked
}),
})
.
then
(()
=>
{
likeButton
.
setAttribute
(
"data-liked"
,
!
liked
);
likeButton
.
textContent
=
liked
?
"Like"
:
"Liked"
;
likeButton
.
setAttribute
(
"disabled"
,
null
);
})
.
catch
(()
=>
{
likeButton
.
setAttribute
(
"data-failed"
,
"true"
);
likeButton
.
textContent
=
"Failed"
;
})
.
finally
(()
=>
{
likeButton
.
setAttribute
(
"data-pending"
,
"false"
);
});
});
Wir können auch leistungsstarke Techniken wie Entprellung und Drosselung einsetzen, um zu verhindern, dass Nutzer/innen überflüssige oder sich wiederholende Aktionen durchführen.
Hinweis
Ganz nebenbei erwähnen wir Entprellung und Drosselung. Zur Verdeutlichung: Debouncing verzögert die Ausführung einer Funktion, bis eine bestimmte Zeit seit dem letzten Ereignisauslöser verstrichen ist (z. B. wartet es darauf, dass die Benutzer aufhören zu tippen, um die Eingaben zu verarbeiten), und Throttling begrenzt die Ausführung einer Funktion auf maximal ein Mal pro Zeitintervall, um sicherzustellen, dass sie nicht zu häufig ausgeführt wird (z. B. verarbeitet es Scroll-Ereignisse in bestimmten Abständen). Beide Techniken optimieren die Leistung, indem sie die Ausführungsrate von Funktionen steuern.
OK, jetzt ist unser Button ziemlich robust und kann mehrere Zustände verarbeiten - aber es bleiben noch einige Fragen offen:
-
Ist
data-pending
wirklich notwendig? Können wir nicht einfach prüfen, ob die Schaltfläche deaktiviert ist? Wahrscheinlich nicht, denn eine deaktivierte Schaltfläche könnte auch aus anderen Gründen deaktiviert sein, z. B. weil der Benutzer nicht angemeldet ist oder keine Berechtigung hat, die Schaltfläche anzuklicken. -
Wäre es sinnvoller, ein
data-state
Attribut zu haben, bei demdata-state
eines vonpending
,liked
oderunliked
sein kann, anstatt so viele andere Datenattribute? Wahrscheinlich, aber dann müssten wir für jeden Fall einen großen Switch/Case- oder ähnlichen Codeblock hinzufügen. Letztendlich ist der Umfang des Codes bei beiden Ansätzen nicht vergleichbar: In beiden Fällen bleibt die Komplexität und Ausführlichkeit erhalten. -
Wie können wir diesen Knopf isoliert testen? Können wir das?
-
Warum lassen wir die Schaltfläche zunächst in HTML schreiben und arbeiten dann später in JavaScript damit? Wäre es nicht besser, wenn wir die Schaltfläche einfach in JavaScript mit
document.createElement('button')
und danndocument.appendChild(likeButton)
erstellen könnten? Das würde das Testen erleichtern und den Code in sich geschlossener machen, aber dann müssten wir den Überblick über seine Eltern behalten, wenn seine Eltern nichtdocument
sind. Es könnte sogar sein, dass wir alle Eltern auf der Seite im Auge behalten müssen.
React hilft uns, einige dieser Probleme zu lösen, aber nicht alle: Die Frage, wie man den Zustand in einzelne Flags (isPending
, hasFailed
, etc.) oder eine einzelne Zustandsvariable (wiestate
) aufteilt, ist eine Frage, die React nicht für uns beantwortet. Diese Frage müssen wir für uns selbst beantworten. React hilft uns jedoch dabei, das Problem der Skalierung zu lösen: Wir können viele Schaltflächen erstellen, die interaktiv sein müssen, und die Benutzeroberfläche als Reaktion auf Ereignisse auf eine minimale und effiziente Weise aktualisieren, und zwar auf eine testbare, reproduzierbare, deklarative, performante, vorhersehbare und zuverlässige Weise.
Darüber hinaus hilft uns React dabei, den Zustand viel vorhersehbarer zu machen , indem es den Zustand der Benutzeroberfläche vollständig verwaltet und auf der Grundlage dieses Zustands rendert. Dies steht in krassem Gegensatz dazu, dass der Zustand vom Browser verwaltet und bearbeitet wird, dessen Zustand aufgrund einer Reihe von Faktoren wie anderen clientseitigen Skripten, die auf der Seite laufen, Browsererweiterungen, Gerätebeschränkungen und vielen anderen Variablen sehr unzuverlässig sein kann.
Unser Beispiel mit dem Like-Button ist ein sehr einfaches Beispiel, aber es ist ein gutes, um damit anzufangen. Bisher haben wir gesehen, wie wir JavaScript nutzen können, um einen Button interaktiv zu machen, aber das ist ein sehr manueller Prozess, wenn wir es gut machen wollen: Wir müssen den Button im Browser finden, einen Event-Listener hinzufügen, den Textinhalt des Buttons aktualisieren und unzählige Kanten berücksichtigen. Das ist eine Menge Arbeit und nicht sehr skalierbar. Was wäre, wenn wir viele Schaltflächen auf der Seite hätten? Was ist, wenn wir viele Schaltflächen haben, die interaktiv sein müssen? Was wäre, wenn wir viele Schaltflächen hätten, die interaktiv sein müssen, und wenn wir die Benutzeroberfläche als Reaktion auf Ereignisse aktualisieren müssten? Würden wir die Ereignisdelegation (oder Event Bubbling) verwenden und einen Ereignis-Listener an die höhere document
anhängen? Oder sollten wir jeder Schaltfläche einen Ereignis-Listener zuweisen?
Wie im Vorwort erwähnt, geht dieses Buch davon aus, dass wir diese Aussage hinreichend verstanden haben: Browser rendern Webseiten. Webseiten sind HTML-Dokumente, die mit CSS gestylt und mit JavaScript interaktiv gemacht werden. Das hat jahrzehntelang gut funktioniert und tut es immer noch, aber die Entwicklung moderner Webanwendungen, die mit diesen Technologien eine beträchtliche Anzahl von Nutzern (z. B. Millionen) bedienen sollen, erfordert ein hohes Maß an Abstraktion, um dies sicher und zuverlässig mit möglichst wenig Fehlermöglichkeiten zu tun. Anhand des Beispiels des Like-Buttons, das wir untersucht haben, ist leider klar, dass wir dabei etwas Hilfe brauchen.
Betrachten wir ein anderes Beispiel, das ein bisschen komplexer ist als unser Like-Button. Wir beginnen mit einem einfachen Beispiel: einer Liste von Artikeln. Nehmen wir an, wir haben eine Liste von Artikeln und wollen einen neuen Artikel zur Liste hinzufügen. Wir könnten das mit einem HTML-Formular machen, das ungefähr so aussieht:
<
ul
id
=
"list-parent"
></
ul
>
<
form
id
=
"add-item-form"
action
=
"/api/add-item"
method
=
"POST"
>
<
input
type
=
"text"
id
=
"new-list-item-label"
/>
<
button
type
=
"submit"
>
Add Item</
button
>
</
form
>
JavaScript gibt uns Zugang zu den Document Object Model (DOM) APIs. Für die Unwissenden: Das DOM ist ein speicherinterne Modell der Dokumentstruktur einer Webseite: Es ist ein Baum von Objekten, der die Elemente auf deiner Seite darstellt und dir Möglichkeiten gibt, mit ihnen über JavaScript zu interagieren. Das Problem ist, dass die DOMs auf den Geräten der Nutzer/innen wie ein fremder Planet sind: Wir können nicht wissen, welche Browser sie benutzen, unter welchen Netzwerkbedingungen und mit welchen Betriebssystemen sie arbeiten. Die Folge? Wir müssen einen Code schreiben, der all diesen Faktoren standhält.
Wie wir bereits besprochen haben, ist der Zustand einer Anwendung ohne einen Mechanismus zum Abgleich des Zustands nur schwer vorhersehbar, um den Überblick zu behalten. Um mit unserem Listenbeispiel fortzufahren, betrachten wir einen JavaScript-Code, der ein neues Element zur Liste hinzufügt:
(
function
myApp
()
{
var
listItems
=
[
"I love"
,
"React"
,
"and"
,
"TypeScript"
];
var
parentList
=
document
.
getElementById
(
"list-parent"
);
var
addForm
=
document
.
getElementById
(
"add-item-form"
);
var
newListItemLabel
=
document
.
getElementById
(
"new-list-item-label"
);
addForm
.
onsubmit
=
function
(
event
)
{
event
.
preventDefault
();
listItems
.
push
(
newListItemLabel
.
value
);
renderListItems
();
};
function
renderListItems
()
{
for
(
i
=
0
;
i
<
listItems
.
length
;
i
++
)
{
var
el
=
document
.
createElement
(
"li"
);
el
.
textContent
=
listItems
[
i
];
parentList
.
appendChild
(
el
);
}
}
renderListItems
();
})();
Dieses Codeschnipsel ist so geschrieben, dass es den frühen Webanwendungen so ähnlich wie möglich ist. Warum geht das mit der Zeit schief? Vor allem, weil die Entwicklung von Anwendungen, die auf diese Weise skalieren sollen, im Laufe der Zeit einige Probleme mit sich bringt, die sie zum Problem machen:
- Fehleranfällig
-
addForm
Dasonsubmit
-Attribut könnte von anderen clientseitigen JavaScript-Programmen auf der Seite leicht umgeschrieben werden. Wir könnten stattdessenaddEventListener
verwenden, aber das wirft mehr Fragen auf:-
Wo und wann würden wir mit
removeEventListener
aufräumen? -
Würden wir im Laufe der Zeit eine Menge Event-Hörer anhäufen, wenn wir nicht aufpassen?
-
Welche Strafen werden wir dafür bezahlen?
-
Wie passt die Delegation von Veranstaltungen dazu?
-
- Unvorhersehbar
-
Unsere Quellen der Wahrheit sind gemischt: Wir halten Listenelemente in einem JavaScript-Array, verlassen uns aber auf bestehende Elemente im DOM (wie ein Element mit
id="list-parent"
), um unsere App fertigzustellen. Aufgrund dieser gegenseitigen Abhängigkeiten zwischen JavaScript und HTML müssen wir noch ein paar weitere Dinge beachten:-
Was ist, wenn es fälschlicherweise mehrere Elemente mit demselben
id
gibt? -
Was ist, wenn es das Element gar nicht gibt?
-
Was ist, wenn es sich nicht um eine
ul
handelt? Können wir Listenelemente (li
Elemente) an andere Elternteile anhängen? -
Was wäre, wenn wir stattdessen Klassennamen verwenden?
Unsere Wahrheitsquellen sind gemischt zwischen JavaScript und HTML, was die Wahrheit unzuverlässig macht. Wir würden mehr davon profitieren, wenn wir eine einzige Quelle der Wahrheit hätten. Außerdem werden ständig Elemente durch JavaScript auf der Client-Seite zum DOM hinzugefügt oder daraus entfernt. Wenn wir uns auf das Vorhandensein dieser bestimmten Elemente verlassen, hat unsere App keine Garantie, dass sie zuverlässig funktioniert, da die Benutzeroberfläche ständig aktualisiert wird. Unsere App ist in diesem Fall voll von "Seiteneffekten", bei denen ihr Erfolg oder Misserfolg von einem Userland-Anliegen abhängt. React hat hier Abhilfe geschaffen, indem es für ein von der funktionalen Programmierung inspiriertes Modell plädiert, bei dem Seiteneffekte absichtlich markiert und isoliert werden.
-
- Ineffizient
-
renderListItems
rendert die Elemente auf dem Bildschirm der Reihe nach. Jede Mutation des DOM kann rechenintensiv sein, vor allem wenn es um Layoutverschiebungen und Reflows geht. Da wir uns auf einem fremden Planeten mit unbekannter Rechenleistung befinden, kann das bei großen Listen ziemlich unsicher für die Leistung sein. Erinnere dich daran, dass unsere groß angelegte Webanwendung von Millionen von Menschen auf der ganzen Welt genutzt werden soll, auch von Menschen mit leistungsschwachen Geräten aus Gemeinden, die keinen Zugang zu den neuesten und besten Apple M3 Max Prozessoren haben. Anstatt das DOM für jedes einzelne Listenelement nacheinander zu aktualisieren, wäre es in diesem Szenario vielleicht idealer, diese Vorgänge irgendwie zu bündeln und sie alle gleichzeitig auf das DOM anzuwenden. Aber vielleicht lohnt sich das für uns als Ingenieure nicht, denn vielleicht werden die Browser irgendwann ihre Arbeitsweise mit schnellen Aktualisierungen des DOM aktualisieren und die Vorgänge automatisch für uns bündeln.
Dies sind einige der Probleme, die Webentwickler seit Jahren plagen, bevor React und andere Abstraktionen aufkamen. Code so zu verpacken, dass er wartbar, wiederverwendbar und in großem Umfang vorhersehbar ist, war ein Problem, für das es in der Branche kaum einen einheitlichen Konsens gab. Die Schwierigkeit, zuverlässige und skalierbare Benutzeroberflächen zu erstellen, wurde damals von vielen Webunternehmen geteilt. Zu diesem Zeitpunkt entstanden im Web mehrere JavaScript-basierte Lösungen, die dieses Problem lösen sollten: Backbone, KnockoutJS, AngularJS und jQuery. Schauen wir uns diese Lösungen der Reihe nach an und sehen wir uns an, wie sie dieses Problem gelöst haben. Das wird uns helfen zu verstehen, wie sich React von diesen Lösungen unterscheidet und ihnen vielleicht sogar überlegen ist.
jQuery
Lasst uns erkunden, wie wir einige dieser Probleme früher im Web mit Tools gelöst haben, die vor React entwickelt wurden, und so lernen, warum React so wichtig ist. Wir beginnen mit jQuery und greifen dabei unser Like-Button-Beispiel von vorhin wieder auf.
Wir haben also einen Like-Button im Browser, den wir interaktiv machen wollen:
<
button
id
=
"likeButton"
>
Like</
button
>
Mit jQuery würden wir das "Gefällt mir"-Verhalten hinzufügen, wie wir es zuvor getan haben, etwa so:
$
(
"#likeButton"
).
on
(
"click"
,
function
()
{
this
.
prop
(
"disabled"
,
true
);
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
this
.
text
()
===
"Like"
}),
})
.
then
(()
=>
{
this
.
text
(
this
.
text
()
===
"Like"
?
"Liked"
:
"Like"
);
})
.
catch
(()
=>
{
this
.
text
(
"Failed"
);
})
.
finally
(()
=>
{
this
.
prop
(
"disabled"
,
false
);
});
});
In diesem Beispiel sehen wir, dass wir Daten an die Benutzeroberfläche binden und diese Datenbindung nutzen, um die Benutzeroberfläche an Ort und Stelle zu aktualisieren. jQuery ist ein ziemlich aktives Werkzeug, um die Benutzeroberfläche selbst zu manipulieren.
jQuery läuft auf eine sehr "nebenwirkungsreiche" Art und Weise und interagiert ständig mit Zuständen außerhalb seiner eigenen Kontrolle. Wir nennen das "seitenwirksam", weil es direkte und globale Änderungen an der Struktur der Seite von jeder Stelle des Codes aus zulässt, auch von anderen importierten Modulen oder sogar von einer entfernten Skriptausführung! Das kann zu unvorhersehbarem Verhalten und komplexen Interaktionen führen, die schwer nachzuvollziehen sind, da Änderungen an einem Teil der Seite andere Teile auf unvorhersehbare Weise beeinflussen können. Diese verstreute und unstrukturierte Manipulation macht den Code schwer zu warten und zu debuggen.
Moderne Frameworks lösen diese Probleme, indem sie strukturierte, vorhersehbare Wege zur Aktualisierung der Benutzeroberfläche ohne direkte Manipulation des DOM anbieten. Dieses Muster war damals weit verbreitet und ist schwer zu erklären und zu testen, weil sich die Welt um den Code herum, also der Zustand der Anwendung neben dem Code, ständig ändert. Irgendwann müssen wir innehalten und uns fragen: "Wie sieht der Zustand der Anwendung im Browser gerade aus?" - eine Frage, die immer schwieriger zu beantworten war, je komplexer unsere Anwendungen wurden.
Außerdem ist dieser Button mit jQuery schwer zu testen, weil er nur ein Event-Handler ist. Wenn wir einen Test schreiben würden, würde er wie folgt aussehen:
test
(
"LikeButton"
,
()
=>
{
const
$button
=
$
(
"#likeButton"
);
expect
(
$button
.
text
()).
toBe
(
"Like"
);
$button
.
trigger
(
"click"
);
expect
(
$button
.
text
()).
toBe
(
"Liked"
);
});
Das einzige Problem ist, dass $('#likeButton')
in der Testumgebung null
zurückgibt, weil es sich nicht um einen echten Browser handelt. Um diesen Code zu testen, müssten wir die Browserumgebung nachbilden, was eine Menge Arbeit ist. Das ist ein häufiges Problem mit jQuery: Es ist schwer zu testen, weil es schwierig ist, das Verhalten, das es hinzufügt, zu isolieren. jQuery hängt auch stark von der Browserumgebung ab. Außerdem teilt sich jQuery das Eigentum an der Benutzeroberfläche mit dem Browser , was es schwierig macht, Schlussfolgerungen zu ziehen und zu testen: Dem Browser gehört die Oberfläche, und jQuery ist nur ein Gast. Diese Abweichung vom Paradigma des "einseitigen Datenflusses" war seinerzeit ein häufiges Problem bei Bibliotheken.
Schließlich verlor jQuery an Beliebtheit, als sich das Web weiterentwickelte und der Bedarf an robusteren und skalierbareren Lösungen deutlich wurde. Obwohl jQuery immer noch in vielen Produktionsanwendungen eingesetzt wird, ist es nicht mehr die beste Lösung für die Entwicklung moderner Webanwendungen. Hier sind einige der Gründe, warum jQuery in Ungnade gefallen ist:
- Gewicht und Ladezeiten
-
Einer der Hauptkritikpunkte an jQuery ist seine Größe. Die Integration der gesamten jQuery-Bibliothek in Web-Projekte bringt zusätzliches Gewicht mit sich, was vor allem für Websites, die schnelle Ladezeiten anstreben, anstrengend sein kann. Im Zeitalter des mobilen Surfens, in dem viele Nutzerinnen und Nutzer über langsamere oder begrenzte Datenverbindungen verfügen, zählt jedes Kilobyte. Die Einbindung der gesamten jQuery-Bibliothek kann sich daher negativ auf die Leistung und das Erlebnis der mobilen Nutzerinnen und Nutzer auswirken.
Vor React war es üblich, Konfiguratoren für Bibliotheken wie jQuery und Mootools anzubieten, aus denen sich die Nutzer die gewünschten Funktionen herauspicken konnten. Das hat zwar dazu beigetragen, dass weniger Code ausgeliefert wurde, aber die Entscheidungen, die die Entwickler treffen mussten, und der gesamteEntwicklungsworkflow wurden dadurch komplexer.
- Redundanz mit modernen Browsern
-
Als jQuery aufkam, hat es viele Unstimmigkeiten zwischen den Browsern beseitigt und Entwicklern eine einheitliche Methode an die Hand gegeben, um diese Unterschiede bei der Auswahl und Änderung von Elementen im Browser zu berücksichtigen. Mit der Entwicklung des Internets haben sich auch die Browser weiterentwickelt. Viele Funktionen, die jQuery zu einem Muss gemacht haben, wie z. B. die konsistente DOM-Manipulation oder netzwerkorientierte Funktionen zum Abrufen von Daten, werden jetzt von modernen Browsern nativ und konsistent unterstützt. Die Verwendung von jQuery für diese Aufgaben in der modernen Webentwicklung kann als überflüssig angesehen werden, da sie eine unnötige Ebene der Komplexität hinzufügt.
document.querySelector
ersetzt zum Beispiel ganz einfach die in jQuery integrierte$
selector API. - Überlegungen zur Leistung
-
Während jQuery viele Aufgaben vereinfacht, geht das oft auf Kosten der Leistung. Native JavaScript-Methoden auf Laufzeitebene verbessern sich mit jeder Iteration des Browsers und können daher irgendwann schneller ausgeführt werden als ihre jQuery-Entsprechungen. Bei kleinen Projekten kann dieser Unterschied vernachlässigbar sein. Bei größeren und komplexeren Webanwendungen kann sich diese Komplexität jedoch anhäufen und zu spürbaren Rucklern oder schlechterer Reaktionsfähigkeit führen.
Aus diesen Gründen hat jQuery zwar eine zentrale Rolle bei der Entwicklung des Webs gespielt und viele Herausforderungen für Entwickler vereinfacht, aber die moderne Weblandschaft bietet native Lösungen, die jQuery oft weniger relevant machen. Als Entwickler müssen wir die Vorzüge von jQuery gegen seine potenziellen Nachteile abwägen, vor allem im Zusammenhang mit aktuellen Webprojekten.
jQuery war trotz seiner Nachteile eine absolute Revolution in der Art und Weise, wie wir damals mit dem DOM interagierten. So sehr, dass andere Bibliotheken aufkamen, die jQuery nutzten, aber mehr Vorhersehbarkeit und Wiederverwendbarkeit in den Mix brachten. Eine dieser Bibliotheken war Backbone, ein Versuch, die gleichen Probleme zu lösen, die React heute löst, aber viel früher. Lasst uns eintauchen.
Backbone
Backbone wurde in den frühen 2010er Jahren entwickelt und war eine der ersten Lösungen für die Probleme, die wir in der Welt vor React erforscht haben: Zustandsdissonanzen zwischen dem Browser und JavaScript, Wiederverwendung von Code, Testbarkeit und mehr. Es war eine elegante und einfache Lösung: eine Bibliothek, mit der man "Modelle" und "Ansichten" erstellen konnte. Backbone hatte seine eigene Interpretation des traditionellen MVC-Musters (Model-View-Controller) (siehe Abbildung 1-1). Wir wollen dieses Muster ein wenig verstehen, um React besser zu verstehen und die Grundlage für eineDiskussion auf höherer Ebenezu schaffen.
Das MVC-Muster
Das MVC-Muster ist eine Design-Philosophie, die Software-Anwendungen in drei miteinander verbundene Komponenten unterteilt, um die internen Darstellungen von Informationen von der Art und Weise zu trennen, wie diese Informationen dem Benutzer präsentiert oder von ihm angenommen werden. Hier ist eine Aufschlüsselung:
- Modell
-
Das Model ist für die Daten und die Geschäftsregeln der Anwendung verantwortlich. Das Model weiß nichts von der View und dem Controller und stellt sicher, dass die Geschäftslogik von der Benutzeroberfläche isoliert ist.
- Siehe
-
Die View stellt die Benutzeroberfläche der Anwendung dar. Sie zeigt dem Benutzer Daten aus dem Modell an und sendet Benutzerbefehle an den Controller. Die View ist passiv, d. h. sie wartet darauf, dass das Model Daten zur Anzeige bereitstellt, und holt oder speichert sie nicht direkt. Die View kümmert sich auch nicht selbst um die Benutzerinteraktion, sondern delegiert diese Aufgabe an die nächste Komponente: den Controller.
- Controller
-
Der Controller fungiert als Schnittstelle zwischen dem Modell und der Ansicht. Er nimmt die Benutzereingaben von der Ansicht entgegen, verarbeitet sie (mit möglichen Aktualisierungen des Modells) und gibt die Ausgabedarstellung an die Ansicht zurück. Der Controller entkoppelt das Modell von der Ansicht und macht die Systemarchitektur flexibler.
Der Hauptvorteil des MVC-Musters ist die Trennung der Belange, d.h. die Geschäftslogik, die Benutzeroberfläche und die Benutzereingaben werden in verschiedene Abschnitte der Codebasis aufgeteilt. Das macht die Anwendung nicht nur modularer, sondern auch einfacher zu warten, zu skalieren und zu testen. Das MVC-Muster ist in Webanwendungen weit verbreitet. Viele Frameworks wie Django, Ruby on Rails und ASP.NET MVC bieten integrierte Unterstützung dafür.
Das MVC-Muster ist seit vielen Jahren ein fester Bestandteil des Softwaredesigns, insbesondere in der Webentwicklung. Mit der Weiterentwicklung von Webanwendungen und den gestiegenen Erwartungen der Nutzer an interaktive und dynamische Schnittstellen sind jedoch einige Grenzen des traditionellen MVC-Musters deutlich geworden. Hier erfährst du, wo MVC versagen kann und wie React diese Herausforderungen meistert:
- Komplexe Interaktivität und Zustandsverwaltung
-
Traditionelle MVC-Architekturen haben oft Probleme, wenn es um die Verwaltung komplexer Benutzeroberflächen mit vielen interaktiven Elementen geht. Wenn eine Anwendung wächst, kann die Verwaltung von Zustandsänderungen und deren Auswirkungen auf verschiedene Teile der Benutzeroberfläche umständlich werden, da sich die Controller häufen und manchmal mit anderen Controllern in Konflikt geraten, wobei einige Controller Ansichten steuern, die sie nicht repräsentieren, oder die Trennung zwischen MVC-Komponenten im Produktcode nicht genau festgelegt ist.
React mit seiner komponentenbasierten Architektur und dem virtuellen DOM macht es einfacher, über Zustandsänderungen und ihre Auswirkungen auf die Benutzeroberfläche nachzudenken, indem es UI-Komponenten wie eine Funktion betrachtet: Sie erhalten Eingaben (Requisiten) und geben auf der Grundlage dieser Eingaben (Elemente) Ausgaben zurück. Dieses mentale Modell hat das MVC-Muster radikal vereinfacht, da Funktionen in JavaScript allgegenwärtig sind und im Vergleich zu einem externen mentalen Modell, das nicht in der Programmiersprache verankert ist, wie MVC, viel einfacher zu verstehen sind.
- Bidirektionale Datenbindung
-
Einige MVC-Frameworks verwenden eine Zwei-Wege-Datenbindung, die bei unvorsichtiger Handhabung zu unbeabsichtigten Nebeneffekten führen kann, bei denen in manchen Fällen entweder die Ansicht nicht mehr mit dem Modell synchronisiert ist oder umgekehrt. Außerdem ist die Frage nach dem Dateneigentum bei der Zwei-Wege-Datenbindung oft nicht eindeutig zu beantworten, und die Trennung der einzelnen Bereiche ist unklar. Das ist besonders interessant, weil MVC zwar ein bewährtes Modell für Teams ist, die genau wissen, wiesie ihre Anliegen für ihre Anwendungsfälletrennen können, diese Trennungsregeln aber nur seltendurchgesetzt werden - vor allem, wenn sie mit einer hohen Ausbringungsgeschwindigkeit und einem schnellenWachstum des Startups konfrontiert sind.
React nutzt ein Muster, das im Gegensatz zur bidirektionalen Datenbindung steht und "unidirektionaler Datenfluss" genannt wird (mehr dazu später), um einen unidirektionalen Datenfluss durch Systeme wie Forget zu priorisieren und sogar zu erzwingen (worauf wir im weiteren Verlauf des Buches noch eingehen werden). Diese Ansätze machen UI-Aktualisierungen vorhersehbarer, ermöglichen uns eine klarere Trennung von Belangen und sind letztendlich förderlich für schnell wachsende Software-Teams.
- Feste Kopplung
-
In einigen MVC-Implementierungen sind Model, View und Controller eng miteinander gekoppelt, so dass es schwierig ist, eines der Elemente zu ändern oder zu refaktorisieren, ohne die anderen zu beeinflussen. React fördert mit seinem komponentenbasierten Modell einen modulareren und entkoppelten Ansatz, der die Zusammenführung von Abhängigkeiten in der Nähe ihrer UI-Darstellung ermöglicht und unterstützt.
Da dies ein React-Buch ist, müssen wir nicht zu sehr in die Details dieses Musters einsteigen, aber für unsere Zwecke hier waren Modelle konzeptionell Datenquellen und Views konzeptionell Benutzeroberflächen, die diese Daten konsumierten und darstellten. Backbone exportierte komfortable APIs, um mit diesen Modellen und Ansichten zu arbeiten, und bot eine Möglichkeit, die Modelle und Ansichten miteinander zu verbinden. Diese Lösung war für ihre Zeit sehr leistungsfähig und flexibel. Außerdem war sie skalierbar und ermöglichte es den Entwicklern, ihren Code in Isolation zu testen.
Hier ist zum Beispiel unser früheres Button-Beispiel, diesmal mitBackbone:
const
LikeButton
=
Backbone
.
View
.
extend
({
tagName
:
"button"
,
attributes
:
{
type
:
"button"
,
},
events
:
{
click
:
"onClick"
,
},
initialize
()
{
this
.
model
.
on
(
"change"
,
this
.
render
,
this
);
},
render
()
{
this
.
$el
.
text
(
this
.
model
.
get
(
"liked"
)
?
"Liked"
:
"Like"
);
return
this
;
},
onClick
()
{
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
!
this
.
model
.
get
(
"liked"
)
}),
})
.
then
(()
=>
{
this
.
model
.
set
(
"liked"
,
!
this
.
model
.
get
(
"liked"
));
})
.
catch
(()
=>
{
this
.
model
.
set
(
"failed"
,
true
);
})
.
finally
(()
=>
{
this
.
model
.
set
(
"pending"
,
false
);
});
},
});
const
likeButton
=
new
LikeButton
({
model
:
new
Backbone
.
Model
({
liked
:
false
,
}),
});
document
.
body
.
appendChild
(
likeButton
.
render
().
el
);
Hast du bemerkt, dass LikeButton
Backbone.View
erweitert und eine Methoderender
hat, die this
zurückgibt? Wir werden uns später eine ähnliche Methoderender
in React ansehen, aber wir wollen nicht zu weit vorpreschen. Es ist auch erwähnenswert, dass Backbone keine eigene Implementierung für render
enthält. Stattdessen musstest du das DOM entweder manuell mit jQuery verändern oder ein Templating-System wie Handlebars verwenden.
Backbone bietet eine verkettbare API, die es Entwicklern ermöglicht, Logik als Eigenschaften von Objekten zu platzieren. Wenn wir dies mit unserem vorherigen Beispiel vergleichen, sehen wir, dass Backbone die Erstellung einer Schaltfläche, die interaktiv ist und die Benutzeroberfläche als Reaktion auf Ereignisse aktualisiert, viel bequemer gemacht hat.
Backbone hat es außerdem einfacher gemacht, diese Schaltfläche isoliert zu testen, da wir eine LikeButton
Instanz erstellen und dann die render
Methode aufrufen können, um sie zu testen.
Wir testen diese Komponente folgendermaßen:
test
(
"LikeButton initial state"
,
()
=>
{
const
likeButton
=
new
LikeButton
({
model
:
new
Backbone
.
Model
({
liked
:
false
,
// Initial state set to not liked
}),
});
likeButton
.
render
();
// Ensure render is called to reflect the initial state
// Check the text content to be "Like" reflecting the initial state
expect
(
likeButton
.
el
.
textContent
).
toBe
(
"Like"
);
});
Wir können sogar das Verhalten der Schaltfläche testen, nachdem sich ihr Zustand geändert hat, wie im Falle eines Klick-Ereignisses, so:
test
(
"LikeButton"
,
async
()
=>
{
// Mark the function as async to handle promise
const
likeButton
=
new
LikeButton
({
model
:
new
Backbone
.
Model
({
liked
:
false
,
}),
});
expect
(
likeButton
.
render
().
el
.
textContent
).
toBe
(
"Like"
);
// Mock fetch to prevent actual HTTP request
global
.
fetch
=
jest
.
fn
(()
=>
Promise
.
resolve
({
json
:
()
=>
Promise
.
resolve
({
liked
:
true
}),
})
);
// Await the onClick method to ensure async operations are complete
await
likeButton
.
onClick
();
expect
(
likeButton
.
render
().
el
.
textContent
).
toBe
(
"Liked"
);
// Optionally, restore fetch to its original implementation if needed
global
.
fetch
.
mockRestore
();
});
Aus diesem Grund war Backbone zu dieser Zeit eine sehr beliebte Lösung. Die Alternative war, eine Menge Code zu schreiben, der schwer zu testen und schwer zu erklären war, ohne dass es eine Garantie dafür gab, dass der Code wie erwartet zuverlässig funktionieren würde. Daher war Backbone eine sehr willkommene Lösung. Obwohl Backbone in seinen Anfängen wegen seiner Einfachheit und Flexibilität sehr beliebt war, ist es nicht ohne Kritikpunkte. Hier sind einige der negativen Aspekte von Backbone.js:
- Ausführlicher Code und Kauderwelsch
-
Einer der häufigsten Kritikpunkte an Backbone.js ist die Menge an Boilerplate Code, den Entwickler schreiben müssen. Bei einfachen Anwendungen mag das keine große Sache sein, aber wenn die Anwendung wächst, wächst auch der Boilerplate-Code, was zu überflüssigem und schwer zu wartendem Code führen kann.
- Fehlende bidirektionale Datenbindung
-
Im Gegensatz zu einigen seiner Zeitgenossen bietet Backbone.js keine integrierte Zwei-Wege-Datenbindung. Das heißt, wenn sich die Daten ändern, wird das DOM nicht automatisch aktualisiert und umgekehrt. Entwickler müssen oft eigenen Code schreiben oder Plug-ins verwenden, um diese Funktion zu erreichen.
- Ereignisgesteuerte Architektur
-
Aktualisierungen von Modelldaten können zahlreiche Ereignisse in der gesamten Anwendung auslösen. Diese Kaskade von Ereignissen kann unüberschaubar werden und dazu führen, dass nicht klar ist, wie sich die Änderung eines einzelnen Datensatzes auf den Rest der Anwendung auswirkt, was die Fehlersuche und Wartung erschwert. Um diese Probleme in den Griff zu bekommen, musstendie Entwicklerinnen und Entwickler oft eine sorgfältige Ereignisverwaltung anwenden, um zu verhindern, dass sich Aktualisierungen auf die gesamte Anwendung auswirken.
- Mangelnde Kompositionsfähigkeit
-
Backbone.js verfügt nicht über integrierte Funktionen zur einfachen Verschachtelung von Ansichten, was die Erstellung komplexer Benutzeroberflächen erschweren kann. React hingegen ermöglicht die nahtlose Verschachtelung von Komponenten mit Hilfe des Children-Prop, was den Aufbau komplizierter Benutzeroberflächenhierarchien wesentlich einfacher macht. Marionette.js, eine Erweiterung von Backbone, hat versucht, einige dieser Kompositionsprobleme zu lösen, bietet aber keine so integrierte Lösung wie das Komponentenmodell von React.
Auch wenn Backbone.js eine Reihe von Herausforderungen mit sich bringt, darf man nicht vergessen, dass kein Tool oder Framework perfekt ist. Die beste Wahl hängt oft von den spezifischen Bedürfnissen des Projekts und den Vorlieben des Entwicklungsteams ab. Leider hat die Popularität von Backbone.js in den letzten Jahren abgenommen, insbesondere mit dem Aufkommen von React. Manche würden sagen, dass React es getötet hat, aber wir behalten uns das Urteil vor.
KnockoutJS
Vergleichen wir diesen Ansatz mit einer anderen damals beliebten Lösung: KnockoutJS. KnockoutJS, das in den frühen 2010er Jahren entwickelt wurde, war eine Bibliothek, die eine Möglichkeit bot, "Observables" und "Bindings" zu erstellen und dabei das Dependency Trackingzu nutzen, wenn sich der Zustand ändert.
KnockoutJS war eine der ersten, wenn nicht sogar die erste reaktive JavaScript-Bibliotheken, wobei Reaktivität als Aktualisierung von Werten als Reaktion auf Zustandsänderungen in einer beobachtbaren Weise definiert ist. Moderne Varianten dieser Art von Reaktivität werden manchmal als "Signale" bezeichnet und sind in Bibliotheken wie Vue.js, SolidJS, Svelte, Qwik, modernem Angular usw. zu finden. In Kapitel 10 gehen wir näher auf diese ein.
Observables waren konzeptionell Datenquellen und Bindungen waren konzeptionell Benutzeroberflächen, die diese Daten konsumierten und darstellten: Observables waren wie Modelle und Bindungen waren wie Ansichten.
Als Weiterentwicklung des MVC-Musters, das wir zuvor besprochen haben, arbeitet KnockoutJS jedoch eher nach einem Model-View-ViewModel- oder MVVM-Muster (siehe Abbildung 1-2). Lasst uns dieses Muster im Detail verstehen.
MVVM-Muster
Das MVVM-Muster ist ein architektonisches Entwurfsmuster, das vor allem bei Anwendungen mit reichhaltigen Benutzeroberflächen beliebt ist, z. B. bei solchen, die mit Plattformen wie WPF und Xamarin erstellt werden. MVVM ist eine Weiterentwicklung des traditionellen Model-View-Controller (MVC)-Musters, das auf moderne UI-Entwicklungsplattformen zugeschnitten ist, bei denen die Datenbindungein wichtiges Merkmal ist. Hier ist eine Aufschlüsselung der MVVM-Komponenten:
- Modell
- Siehe
-
-
Zeigt dem Benutzer Informationen an und nimmt Benutzereingaben entgegen.
-
Bei MVVM ist die View passiv und enthält keine Anwendungslogik. Stattdessen bindet sie sich deklarativ an das ViewModel und spiegelt Änderungen automatisch durch Datenbindungsmechanismen wider.
- ViewModel
-
-
Stellt Daten und Befehle zur Verfügung, an die sich die Ansicht binden kann. Die Daten liegen hier oft in einem Format vor, das für die Anzeige geeignet ist.
-
Verarbeitet Benutzereingaben, oft durch Befehlsmuster.
-
Enthält die Präsentationslogik und wandelt die Daten aus dem Modell in ein Format um, das in der Ansicht leicht angezeigt werden kann.
-
Das Besondere ist, dass das ViewModel nicht weiß, welche View es benutzt, was eine entkoppelte Architektur ermöglicht.
Der Hauptvorteil des MVVM-Musters ist die Trennung der Belangeähnlich wie bei MVC, was dazu führt:
- Prüfbarkeit
-
Die Entkopplung von ViewModel und View macht es einfacher, Unit-Tests für die Präsentationslogik zu schreiben, ohne die Benutzeroberfläche einzubeziehen.
- Wiederverwendbarkeit
-
Das ViewModel kann in verschiedenen Ansichten oder Plattformen wiederverwendet werden.
- Instandhaltbarkeit
-
Mit einer klaren Trennung ist es einfacher, den Code zu verwalten, zu erweitern und zu refaktorisieren.
- Datenbindung
-
Das Muster eignet sich besonders gut für Plattformen, die Datenbindung unterstützen, da es die Menge an Boilerplate-Code reduziert, die zur Aktualisierung der Benutzeroberfläche erforderlich ist.
Da wir sowohl MVC als auch MVVM-Patterns besprochen haben, wollen wir sie kurz gegenüberstellen, damit wir die Unterschiede zwischen ihnen verstehen (siehe Tabelle 1-1).
Kriterien | MVC | MVVM |
---|---|---|
Primärer Zweck |
Vor allem für Webanwendungen, um die Benutzeroberfläche von der Logik zu trennen. |
Maßgeschneidert für Rich-UI-Anwendungen, insbesondere mit Zwei-Wege-Datenbindung, wie z. B. Desktop- oder SPAs. |
Komponenten |
Modell: Daten und Geschäftslogik. Ansicht: Benutzeroberfläche. Controller: verwaltet Benutzereingaben, aktualisiert die Ansicht. |
Modell: Daten und Geschäftslogik. View: Elemente der Benutzeroberfläche. ViewModel: Brücke zwischen Model und View. |
Datenfluss |
Die Benutzereingaben werden vom Controller verwaltet, der das Modell und dann die Ansicht aktualisiert. |
Die View bindet sich direkt an das ViewModel. Änderungen in der View werden automatisch im ViewModel reflektiert und umgekehrt. |
Entkopplung |
Die View ist oft eng mit dem Controller gekoppelt. |
Hohe Entkopplung, da das ViewModel nicht weiß, welche spezifische View es verwendet. |
Benutzerinteraktion |
Wird vom Controller verwaltet. |
Wird durch Datenbindungen und Befehle im ViewModel gehandhabt. |
Eignung der Plattform |
Erfahrung in der Entwicklung von Webanwendungen (z.B. Ruby on Rails, Django, ASP.NET MVC). |
Geeignet für Plattformen, die robuste Datenbindung unterstützen (z. B. WPF, Xamarin). |
Anhand dieses kurzen Vergleichs können wir erkennen, dass der eigentliche Unterschied zwischen MVC- und MVVM-Mustern in der Kopplung und der Bindung liegt: Ohne Controller zwischen Model und View ist der Datenbesitz klarer und näher am Nutzer. React verbessert MVVM mit seinem unidirektionalen Datenfluss ( ), auf den wir gleich noch eingehen werden, noch weiter, indem es das Dateneigentum noch enger eingrenzt, so dass die Zustände den Komponenten gehören, die sie benötigen. Kommen wir nun zurück zu KnockoutJS und seiner Beziehung zu React.
KnockoutJS exportierte APIs, um mit diesen Observables und Bindungen zu arbeiten. Schauen wir uns an, wie wir den Like-Button in KnockoutJS implementieren würden. Das wird uns helfen, besser zu verstehen, warum React. Hier ist die KnockoutJS-Version unseres Buttons:
function
createViewModel
({
liked
})
{
const
isPending
=
ko
.
observable
(
false
);
const
hasFailed
=
ko
.
observable
(
false
);
const
onClick
=
()
=>
{
isPending
(
true
);
fetch
(
"/like"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
({
liked
:
!
liked
()
}),
})
.
then
(()
=>
{
liked
(
!
liked
());
})
.
catch
(()
=>
{
hasFailed
(
true
);
})
.
finally
(()
=>
{
isPending
(
false
);
});
};
return
{
isPending
,
hasFailed
,
onClick
,
liked
,
};
}
ko
.
applyBindings
(
createViewModel
({
liked
:
ko
.
observable
(
false
)
}));
In KnockoutJS ist ein "View Model" ein JavaScript-Objekt, das Schlüssel und Werte enthält, die wir mit demdata-bind
-Attribut an verschiedene Elemente auf unserer Seite binden. In KnockoutJS gibt es keine "Komponenten" oder "Templates", sondern nur ein View Model und eine Möglichkeit, es an ein Element im Browser zu binden.
Unsere Funktion createViewModel
entspricht der Erstellung eines View-Modells mit Knockout. Anschließend verwenden wir ko.applyBindings
, um das View Model mit der Host-Umgebung (dem Browser) zu verbinden. Die Funktion ko.applyBindings
nimmt ein View Model und findet alle Elemente im Browser, die ein data-bind
Attribut haben, das Knockout verwendet, um sie mit dem View Model zu verbinden.
Eine Schaltfläche in unserem Browser würde an die Eigenschaften dieses View-Modells wie folgt gebunden werden :
<
button
data-bind
=
"click:
onClick
,
text:
liked
?
'
Liked
'
:
isPending
?
[...]
></
button
>
Beachte, dass dieser Code der Einfachheit halber gekürzt wurde.
Wir binden das HTML-Element an das "View Model", das wir mit unserer FunktioncreateViewModel
erstellt haben, und die Seite wird interaktiv. Wie du dir vorstellen kannst, ist es eine Menge Arbeit, explizit Änderungen in Observables zu abonnieren und dann die Benutzeroberfläche als Reaktion auf diese Änderungen zu aktualisieren. KnockoutJS war zu seiner Zeit eine großartige Bibliothek, aber sie erforderte auch eine Menge Kesselsteincode, um die Dinge zu erledigen.
Außerdem wurden die View-Modelle oft sehr groß und komplex, was zu einer zunehmenden Unsicherheit bei Refactors und Code-Optimierungen führte. Schließlich endeten wir mit weitschweifigen, monolithischen View-Modellen, die schwer zu testen und zu verstehen waren. Dennoch war KnockoutJS eine sehr beliebte Lösung und eine großartige Bibliothek für seine Zeit. Es war auch relativ einfach, isoliert zu testen, was ein großes Plus war.
Für die Nachwelt, hier ist, wie wir diese Schaltfläche in KnockoutJS testen würden:
test
(
"LikeButton"
,
()
=>
{
const
viewModel
=
createViewModel
({
liked
:
ko
.
observable
(
false
)
});
expect
(
viewModel
.
liked
()).
toBe
(
false
);
viewModel
.
onClick
();
expect
(
viewModel
.
liked
()).
toBe
(
true
);
});
AngularJS
AngularJS wurde von Google im Jahr 2010 entwickelt. Es war ein bahnbrechendes JavaScript-Framework, das die Webentwicklungslandschaft maßgeblich beeinflusst hat. Es stand in scharfem Kontrast zu den Bibliotheken und Frameworks, die wir gerade besprochen haben, da es mehrere innovative Funktionen enthielt, deren Auswirkungen in den nachfolgenden Bibliotheken zu sehen sind, einschließlich React. Durch einen detaillierten Vergleich von AngularJS mit diesen anderen Bibliotheken und einen Blick auf seine zentralen Funktionen wollen wir versuchen, den Weg zu verstehen, den es für React geebnet hat.
Bidirektionale Datenbindung
Zwei-Wege-Datenbindung war ein Markenzeichen von AngularJS, das die Interaktion zwischen der Benutzeroberfläche und den zugrunde liegenden Daten stark vereinfacht hat. Wenn sich das Modell (die zugrundeliegenden Daten) ändert, wird die Ansicht (die Benutzeroberfläche) automatisch aktualisiert, um die Änderung widerzuspiegeln, und umgekehrt. Dies stand im krassen Gegensatz zu Bibliotheken wie jQuery, bei denen die Entwickler/innen das DOM manuell manipulieren mussten, um Änderungen in den Daten widerzuspiegeln und Benutzereingaben zur Aktualisierung der Daten zu erfassen.
Betrachten wir eine einfache AngularJS-Anwendung, bei der die Zwei-Wege-Datenbindung eine entscheidende Rolle spielt:
<!DOCTYPE html>
<
html
>
<
head
>
<
script
src
=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"
>
</
script
>
</
head
>
<
body
ng-app
=
""
>
<
p
>
Name:<
input
type
=
"text"
ng-model
=
"name"
/></
p
>
<
p
ng-if
=
"name"
>
Hello, {{name}}!</
p
>
</
body
>
</
html
>
In dieser Anwendung bindet die Direktive ng-model
den Wert des Eingabefeldes an die Variable name
. Wenn du in das Eingabefeld tippst, wird das Modell name
aktualisiert und die Ansicht - in diesem Fall die Begrüßung"Hello, {{name}}!"
- wird in Echtzeit aktualisiert.
Modulare Architektur
Mit AngularJS wurde eine modulare Architektur eingeführt, die es Entwicklern ermöglicht, die Komponenten ihrer Anwendung logisch zu trennen. Jedes Modul konnte eine Funktionalität kapseln und unabhängig entwickelt, getestet und gewartet werden. Manche bezeichnen dies als Vorläufer des Komponentenmodells von React, aber das ist umstritten.
Hier ist ein kurzes Beispiel:
var
app
=
angular
.
module
(
"myApp"
,
[
"ngRoute"
,
"appRoutes"
,
"userCtrl"
,
"userService"
,
]);
var
userCtrl
=
angular
.
module
(
"userCtrl"
,
[]);
userCtrl
.
controller
(
"UserController"
,
function
(
$scope
)
{
$scope
.
message
=
"Hello from UserController"
;
});
var
userService
=
angular
.
module
(
"userService"
,
[]);
userService
.
factory
(
"User"
,
function
(
$http
)
{
//...
});
Im obigen Beispiel hängt das Modul myApp
von mehreren anderen Modulen ab: ngRoute
, appRoutes
, userCtrl
und userService
. Jedes abhängige Modul konnte in einer eigenen JavaScript-Datei stehen und separat vom Hauptmodul myApp
entwickelt werden. Dieses Konzept unterscheidet sich deutlich von jQuery und Backbone.js, die kein Konzept für ein "Modul" in diesem Sinne haben.
Wir injizieren diese Abhängigkeiten (appRoutes
, userCtrl
, etc.) in unsere Root app
mit einem Muster namens Dependency Injection, das in Angular populär geworden ist. Natürlich war dieses Muster weit verbreitet, bevor JavaScript-Module standardisiert wurden. Seitdem haben sich die Anweisungen import
undexport
schnell durchgesetzt. Um diese Abhängigkeiten mit denen von React-Komponenten zu vergleichen, wollen wir ein wenig mehr über Dependency Injection sprechen.
Dependency Injection
Dependency Injection (DI) ist ein Entwurfsmuster, bei dem ein Objekt seine Abhängigkeiten erhält, anstatt sie zu erstellen. AngularJS hat dieses Entwurfsmuster in seinen Kern integriert, was in anderen JavaScript-Bibliotheken zu dieser Zeit nicht üblich war. Dies hatte tiefgreifende Auswirkungen auf die Art und Weise, wie Module und Komponenten erstellt und verwaltet werden, und förderte einen höheren Grad an Modularität und Wiederverwendbarkeit.
Hier ist ein Beispiel dafür, wie DI in AngularJS funktioniert:
var
app
=
angular
.
module
(
"myApp"
,
[]);
app
.
controller
(
"myController"
,
function
(
$scope
,
myService
)
{
$scope
.
greeting
=
myService
.
sayHello
();
});
app
.
factory
(
"myService"
,
function
()
{
return
{
sayHello
:
function
()
{
return
"Hello, World!"
;
},
};
});
In diesem Beispiel ist myService
ein Dienst, der über DI in denmyController
Controller injiziert wird. Der Controller muss nicht wissen, wie er den Dienst erstellen kann. Er deklariert den Dienst einfach als Abhängigkeit, und AngularJS kümmert sich um die Erstellung und das Injizieren. Das vereinfacht die Verwaltung von Abhängigkeiten und verbessert die Testbarkeit und Wiederverwendbarkeit von Komponenten.
Vergleich mit Backbone.js und Knockout.js
Backbone.js und Knockout.js waren zwei beliebte Bibliotheken, die zu der Zeit verwendet wurden, als AngularJS eingeführt wurde. Beide Bibliotheken hatten ihre Stärken, aber ihnen fehlten einige Funktionen, die in AngularJS eingebaut wurden.
Backbone.js zum Beispiel gab Entwicklern mehr Kontrolle über ihren Code und war weniger eigenwillig als AngularJS. Diese Flexibilität war sowohl eine Stärke als auch eine Schwäche: Sie ermöglichte mehr Anpassungen, erforderte aber auch mehr Boilerplate-Code. AngularJS mit seiner bidirektionalen Datenbindung und DI ermöglichte mehr Struktur. Es gab mehr Meinungen, die zu einer höheren Entwicklungsgeschwindigkeit führten: etwas, das wir bei modernen Frameworks wie Next.js, Remix usw. sehen. Das ist ein Grund, warum AngularJS seiner Zeit weit voraus war.
Backbone hatte auch keine Lösung für die direkte Änderung der Ansicht (des DOM) und überließ dies oft den Entwicklern. AngularJS kümmerte sich mit seiner Zwei-Wege-Datenbindung um DOM-Mutationen, was ein großes Plus war.
Knockout.js konzentrierte sich in erster Linie auf die Datenbindung und verfügte nicht über einige der anderen leistungsstarken Tools, die AngularJS bot, wie DI und eine modulare Architektur. AngularJS war ein vollwertiges Framework und bot eine umfassendere Lösung für die Erstellung von Single-Page-Anwendungen (SPAs). AngularJS wurde zwar eingestellt, aber seine neuere Variante namens Angular bietet dieselben, wenn auch verbesserten, umfassenden Vorteile, die es zu einer idealen Wahl für umfangreiche Anwendungen machen.
AngularJS-Kompromisse
AngularJS (1.x) stellte bei seiner Einführung einen großen Sprung in der Webentwicklung dar. Da sich die Landschaft der Webentwicklung jedoch schnell weiterentwickelte, wurden bestimmte Aspekte von AngularJS als Einschränkungen oder Schwächen angesehen, die zu seinem relativen Niedergang beitrugen. Einige dieser Aspekte sind:
- Leistung
-
AngularJS hatte Probleme mit der Leistung, insbesondere bei großen Anwendungen mit komplexen Datenverbindungen. Der Digest-Zyklus in AngularJS, eine zentrale Funktion zur Erkennung von Änderungen, konnte in großen Anwendungen zu langsamen Aktualisierungen und verzögerten Benutzeroberflächen führen. Die Zwei-Wege-Datenbindung, die zwar innovativ und in vielen Situationen nützlich ist, trug ebenfalls zu den Leistungsproblemen bei.
- Komplexität
-
AngularJS führte eine Reihe neuer Konzepte ein, darunter Direktiven, Controller, Services, Dependency Injection, Factories und mehr. Diese Funktionen machten AngularJS zwar leistungsfähig, aber auch komplex und schwer zu erlernen, besonders für Anfänger. Eine häufige Debatte war zum Beispiel: "Soll dies eine Factory oder ein Service sein?", was viele Entwicklerteams vor ein Rätsel stellte.
- Probleme bei der Migration zu Angular 2+
-
Als Angular 2 angekündigt wurde, war es nicht abwärtskompatibel mit AngularJS 1.x. und erforderte, dass der Code in Dart und/oder TypeScript geschrieben wurde. Das bedeutete, dass Entwickler/innen große Teile ihres Codes neu schreiben mussten, um auf Angular 2 umzusteigen, was als große Hürde angesehen wurde. Die Einführung von Angular 2+ spaltete die Angular-Gemeinschaft und sorgte für Verwirrung, was den Weg für React ebnete.
- Komplexe Syntax in Templates
-
Die Möglichkeit von AngularJS, komplexe JavaScript-Ausdrücke in Template-Attributen wie
on-click="$ctrl.some.deeply.nested.field = 123"
zu verwenden, war problematisch, da dies zu einer Vermischung von Präsentations- und Geschäftslogik im Markup führte. Dieser Ansatz führte zu Problemen bei der Wartbarkeit, da die Entschlüsselung und Verwaltung des verflochtenen Codes mühsam wurde.Außerdem war die Fehlersuche auf schwieriger, weil Templating-Schichten nicht von vornherein für komplexe Logik ausgelegt waren und Fehler, die durch diese Inline-Ausdrücke entstanden, schwer zu finden und zu beheben waren. Außerdem verstießen solche Praktiken gegen das Prinzip der Trennung von Belangen, einer grundlegenden Designphilosophie, die eine getrennte Behandlung verschiedener Aspekte einer Anwendung vorsieht, um die Codequalität und die Wartbarkeit zu verbessern.
Theoretisch sollte ein Template eine Controller-Methode aufrufen, um eine Aktualisierung durchzuführen, aber nichts hat das eingeschränkt.
- Fehlende Typensicherheit
-
Templates in AngularJS funktionierten nicht mit statischen Typprüfungen wie TypeScript, was es schwierig machte, Fehler frühzeitig im Entwicklungsprozess zu erkennen. Das war ein großer Nachteil, vor allem für große Anwendungen, bei denen die Typsicherheit für die Wartbarkeit und Skalierbarkeit entscheidend ist.
- Verwirrendes
$scope
Modell -
Das
$scope
Objekt in AngularJS wurde aufgrund seiner Rolle beim Binden von Daten und seines Verhaltens in verschiedenen Kontexten oft als Quelle der Verwirrung empfunden, da es als Klebstoff zwischen der View und dem Controller diente, sein Verhalten aber nicht immer intuitiv oder vorhersehbar war.Dies führte dazu, dass es vor allem für Neulinge schwierig war zu verstehen, wie die Daten zwischen dem Modell und dem View synchronisiert wurden. Außerdem konnte
$scope
Eigenschaften von übergeordneten Bereichen in verschachtelten Controllern erben, was es schwierig machte, nachzuvollziehen, wo eine bestimmte$scope
Eigenschaft ursprünglich definiert oder geändert wurde.Diese Vererbung konnte zu unerwarteten Nebeneffekten in der Anwendung führen, insbesondere wenn es um verschachtelte Bereiche ging, in denen sich Eltern- und Kindbereiche ungewollt gegenseitig beeinflussen konnten. Das Konzept der Bereichshierarchie und die prototypische Vererbung, auf der es basierte, standen oft im Widerspruch zu den traditionelleren und vertrauten lexikalischen Scoping-Regeln in JavaScript, wodurch eine weitere Ebene der Lernkomplexität hinzugefügt wurde.
React zum Beispiel legt den Status bei der Komponente ab, die ihn braucht, und vermeidet so dieses Problem vollständig.
- Begrenzte Entwicklungswerkzeuge
-
AngularJS bot keine umfangreichen Entwicklerwerkzeuge für Debugging und Performance-Profiling, vor allem im Vergleich zu den DevTools in React wie Replay.io, das umfangreiche Möglichkeiten für das Debugging von React-Anwendungen auf Zeitreise bietet.
React eingeben
Etwa zu dieser Zeit wurde React bekannt. Eine der Kernideen, die React vorstellte, war die komponentenbasierte Architektur. Obwohl die Umsetzung unterschiedlich ist, ist die zugrundeliegende Idee ähnlich: Es ist optimal, Benutzeroberflächen für das Web und andere Plattformen zu erstellen, indem wiederverwendbare Komponenten zusammengestellt werden.
Während AngularJS Direktiven verwendete, um Ansichten an Modelle zu binden, führte React JSX und ein radikal einfacheres Komponentenmodell ein. Doch ohne die Grundlage, die AngularJS mit der Förderung einer komponentenbasierten Architektur durch Angular-Module gelegt hat, wäre der Übergang zum React-Modell vielleicht nicht so reibungslos verlaufen.
In AngularJS war das bidirektionale Datenbindungsmodell der Industriestandard; es hatte jedoch auch einige Nachteile, wie z. B. potenzielle Leistungsprobleme bei großen Anwendungen. React hat daraus gelernt und ein unidirektionales Datenflussmuster eingeführt, das Entwicklern mehr Kontrolle über ihre Anwendungen gibt und es einfacher macht zu verstehen, wie sich Daten im Laufe der Zeit verändern.
React führte auch das virtuelle DOM ein, über das wir in Kapitel 3 lesen werden: ein Konzept, das die Leistung verbessert, indem es die direkte DOM-Manipulation minimiert. AngularJS hingegen manipulierte das DOM oft direkt, was zu Leistungsproblemen und anderen Problemen mit inkonsistenten Zuständen führen konnte, die wir kürzlich mit jQuery diskutiert haben.
Wir wären nachlässig, wenn wir nicht erwähnen würden, dass AngularJS nicht nur die Webentwicklungslandschaft revolutioniert hat, als es eingeführt wurde, sondern auch den Weg für die Entwicklung zukünftiger Frameworks und Bibliotheken geebnet hat, zu denen auch React gehört.
Sehen wir uns an, wie React in all das hineinpasst und woher React an diesem Punkt der Geschichte kam. Zu dieser Zeit waren UI-Updates noch ein relativ schwieriges und ungelöstes Problem. Heute sind sie noch lange nicht gelöst, aber React hat sie merklich vereinfacht und andere Bibliotheken wie SolidJS, Qwik und andere dazu inspiriert. Meta's Facebook war keine Ausnahme in Bezug auf das Problem der Komplexität und Größe der UI. Deshalb hat Meta eine Reihe interner Lösungen entwickelt, die die damals bereits vorhandenen ergänzen. Eine der ersten davon war BoltJS: ein Tool, das die Facebook-Ingenieure "zusammengeschraubt" haben. Es wurde eine Kombination von Tools zusammengestellt, um Updates der Facebook-Benutzeroberfläche intuitiver zu gestalten.
Ungefähr zu dieser Zeit hatte der Facebook-Ingenieur Jordan Walke eine radikale Idee, die mit dem damaligen Status quo aufräumte und minimale Teile von Webseiten bei Aktualisierungen vollständig durch neue Seiten ersetzte. Wie wir bereits gesehen haben, verwalten JavaScript-Bibliotheken die Beziehungen zwischen Ansichten (Benutzeroberflächen) und Modellen (Datenquellen) mithilfe eines Paradigmas, das als Zwei-Wege-Datenbindung bezeichnet wird. Angesichts der bereits erwähnten Einschränkungen dieses Modells hatte Jordan die Idee, stattdessen ein Paradigma namens einseitiger Datenfluss zu verwenden. Dieses Paradigma war viel einfacher und es war viel einfacher, die Ansichten und Modelle synchron zu halten. Dies war die Geburtsstunde der unidirektionalen Architektur, die später die Grundlage von React bilden sollte.
Das Leistungsversprechen von React
OK, die Geschichtsstunde ist vorbei. Hoffentlich haben wir jetzt genug Kontext, um zu verstehen, warum React eine Sache ist. Angesichts der Tatsache, dass es so einfach war, in unsicheren, unvorhersehbaren und ineffizienten JavaScript-Code zu verfallen, brauchten wir eine Lösung, die uns in eine Grube des Erfolgs lenkt, in der wir zufällig gewinnen. Lasst uns darüber sprechen, wie React das genau macht.
Deklarativer versus imperativer Code
React bietet eine deklarative Abstraktion des DOMs. Wir werden später im Buch noch genauer darauf eingehen, aber im Wesentlichen bietet es uns eine Möglichkeit, Code zu schreiben, der ausdrückt , was wir sehen wollen, und sich dann darum kümmert, wie das geschieht, damit unsere Benutzeroberfläche sicher, vorhersehbar und effizient funktioniert.
Schauen wir uns die Listen-App an, die wir vorhin erstellt haben. In React könnten wir sie wie folgt umschreiben:
function
MyList
()
{
const
[
items
,
setItems
]
=
useState
([
"I love"
]);
return
(
<
div
>
<
ul
>
{
items
.
map
((
i
)
=>
(
<
li
key
=
{
i
/* keep items unique */
}
>
{
i
}
<
/li>
))}
<
/ul>
<
NewItemForm
onAddItem
=
{(
newItem
)
=>
setItems
([...
items
,
newItem
])}
/>
<
/div>
);
}
Beachte, dass wir in der return
buchstäblich etwas schreiben, das wie HTML aussieht: Es sieht so aus, wie wir es sehen wollen. Ich möchte einen Kasten mit einemNewItemForm
und einer Liste sehen. Bumm. Wie kommt es dorthin? Das muss React herausfinden. Fügen wir die Listenelemente in Stapeln ein, um Teile davon auf einmal hinzuzufügen? Oder fügen wir sie der Reihe nach hinzu, eins nach dem anderen? React kümmert sich darum , wie das gemacht wird, während wir nur beschreiben , was wir tun wollen. In weiteren Kapiteln werden wir in React eintauchen und genau untersuchen, wie es das zum Zeitpunkt des Schreibens macht.
Sind wir dann auf Klassennamen angewiesen, um HTML-Elemente zu referenzieren? Haben wirgetElementById
in JavaScript? Nein. React erstellt unter der Haube einzigartige "React-Elemente" für uns, die es nutzt, um Änderungen zu erkennen und inkrementelle Aktualisierungen vorzunehmen, sodass wir keine Klassennamen und andere Bezeichner aus dem Benutzercode lesen müssen, deren Existenz wir nicht garantieren können: Unsere Quelle der Wahrheit ist mit React ausschließlich JavaScript.
Wir exportieren unsere MyList
Komponente nach React, und React bringt sie für uns auf sichere, vorhersehbare und performante Weise auf den Bildschirm - ohne Fragen zu stellen. Die Aufgabe der Komponente ist es, uns eine Beschreibung zu liefern, wie dieser Teil der Benutzeroberfläche aussehen soll. Dazu verwendet sie einvirtuelles DOM (vDOM), das eine leichtgewichtige Beschreibung der geplanten UI-Struktur ist. React vergleicht dann das virtuelle DOM nach einer Aktualisierungmit dem virtuellen DOM vor einer Aktualisierung und wandelt dies in kleine, performante Aktualisierungen des realen DOMs um, damit es mit dem virtuellen DOM übereinstimmt. Auf diese Weise kann React Aktualisierungen am DOM vornehmen.
Das virtuelle DOM
Das virtuelle DOM ist ein Programmierkonzept, das das echte DOM darstellt, aber als JavaScript Objekt. Wenn dir das jetzt zu kompliziert ist, mach dir keine Sorgen: Kapitel 3 widmet sich diesem Thema und geht etwas ausführlicher darauf ein. Für den Moment ist es nur wichtig zu wissen, dass das virtuelle DOM es Entwicklern ermöglicht, die Benutzeroberfläche zu aktualisieren, ohne das eigentliche DOM direkt zu verändern. React nutzt das virtuelle DOM, um Änderungen an einer Komponente zu verfolgen und die Komponente nur bei Bedarf neu zu erstellen. Dieser Ansatz ist schneller und effizienter, als bei jeder Änderung den gesamten DOM-Baum zu aktualisieren.
In React ist das virtuelle DOM eine leichtgewichtige Darstellung des tatsächlichen DOM-Baums. Es ist ein einfaches JavaScript-Objekt, das die Struktur und die Eigenschaften der UI-Elemente beschreibt. React erstellt und aktualisiert das virtuelle DOM, damit es mit dem tatsächlichen DOM-Baum übereinstimmt, und alle Änderungen, die am virtuellen DOM vorgenommen werden, werden über einen Prozess namens Reconciliation auf das tatsächliche DOM übertragen.
Kapitel 4 ist diesem Thema gewidmet, aber für unsere kontextbezogene Diskussion hier wollen wir uns eine kleine Zusammenfassung mit ein paar Beispielen ansehen. Um zu verstehen, wie das virtuelle DOM funktioniert, kehren wir zu unserem Beispiel mit dem Like-Button zurück. Wir werden eine React-Komponente erstellen, die einen Like-Button und die Anzahl der Likes anzeigt. Wenn der Nutzer auf die Schaltfläche klickt, soll sich die Anzahl der Likes um eins erhöhen.
Hier ist der Code für unsere Komponente:
import
React
,
{
useState
}
from
"react"
;
function
LikeButton
()
{
const
[
likes
,
setLikes
]
=
useState
(
0
);
function
handleLike
()
{
setLikes
(
likes
+
1
);
}
return
(
<
div
>
<
button
onClick
=
{
handleLike
}>
Like
</
button
>
<
p
>{
likes
}
Likes
</
p
>
</
div
>
);
}
export
default
LikeButton
;
In diesem Code haben wir den Hook useState
verwendet, um eine Statusvariable likes
zu erstellen, die die Anzahl der Likes enthält. Ein Hook ist eine spezielle Funktion, mit der wir React-Funktionen wie Zustands- und Lebenszyklusmethoden in funktionalen Komponenten nutzen können. Hooks ermöglichen die Wiederverwendung von zustandsbehafteter Logik, ohne die Komponentenhierarchie zu verändern. So lassen sich Hooks leicht extrahieren und zwischen Komponenten oder sogar mit der Community als eigenständige Open-Source-Pakete teilen.
Wir haben auch eine Funktion handleLike
definiert, die den Wert der Likes um eins erhöht, wenn der Button angeklickt wird. Zum Schluss stellen wir den Like-Button und die Anzahl der Likes mit JSX dar.
Schauen wir uns nun genauer an, wie das virtuelle DOM in diesem Beispiel funktioniert.
Wenn die Komponente LikeButton
zum ersten Mal gerendert wird, erstellt React einen virtuellen DOM-Baum, der den tatsächlichen DOM-Baum widerspiegelt. Das virtuelle DOM enthält ein einzelnes div
Element, das ein button
Element und einp
Element enthält:
{
$$typeof
:
Symbol
.
for
(
'react.element'
),
type
:
'div'
,
props
:
{},
children
:
[
{
$$typeof
:
Symbol
.
for
(
'react.element'
),
type
:
'button'
,
props
:
{
onClick
:
handleLike
},
children
:
[
'Like'
]
},
{
$$typeof
:
Symbol
.
for
(
'react.element'
),
type
:
'p'
,
props
:
{},
children
:
[
0
,
' Likes'
]
}
]
}
Die Eigenschaft children
des Elements p
enthält den Wert der Zustandsvariablen Likes
, die anfangs auf Null gesetzt ist.
Wenn der Nutzer auf den Like-Button klickt, wird die Funktion handleLike
aufgerufen, die die Statusvariable likes
aktualisiert. React erstellt dann einen neuen virtuellen DOM-Baum, der den aktualisierten Zustand widerspiegelt:
{
type
:
'div'
,
props
:
{},
children
:
[
{
type
:
'button'
,
props
:
{
onClick
:
handleLike
},
children
:
[
'Like'
]
},
{
type
:
'p'
,
props
:
{},
children
:
[
1
,
' Likes'
]
}
]
}
Beachte, dass der virtuelle DOM-Baum dieselben Elemente wie zuvor enthält, aber die Eigenschaft children
des Elements p
wurde aktualisiert, um den neuen Wert von likes widerzuspiegeln, und zwar von 0
auf 1
. Was auf folgt, ist ein Prozess, der in React Reconciliation genannt wird und bei dem das neue vDOM mit dem alten verglichen wird. Lass uns diesen Prozess kurz besprechen.
Nachdem ein neuer virtueller DOM-Baum berechnet wurde, führt React einen Prozess namens Reconciliation durch, um die Unterschiede zwischen dem neuen und dem alten Baum zu verstehen. Bei der Reconciliation wird der alte virtuelle DOM-Baum mit dem neuen virtuellen DOM-Baum verglichen und festgestellt, welche Teile des tatsächlichen DOMs aktualisiert werden müssen. Wenn du dich dafür interessierst, wie das genau funktioniert, findest du in Kapitel 4 eine ausführliche Beschreibung. Betrachten wir zunächst einmal unseren Like-Button.
In unserem Beispiel vergleicht React den alten virtuellen DOM-Baum mit dem neuen virtuellen DOM-Baum und stellt fest, dass sich das Element p
geändert hat: genauer gesagt, dass sich seine Props oder sein Zustand oder beides geändert haben. Dadurch kann React die Komponente als "schmutzig" oder "sollte aktualisiert werden" markieren. React berechnet dann eine minimale Anzahl von Aktualisierungen, die im tatsächlichen DOM vorgenommen werden müssen, um den Zustand des neuen vDOM mit dem DOM in Einklang zu bringen, und aktualisiert schließlich das tatsächliche DOM, um die Änderungen im virtuellen DOM widerzuspiegeln.
React aktualisiert nur die notwendigen Teile des aktuellen DOM, um die Anzahl der DOM-Manipulationen zu minimieren. Dieser Ansatz ist viel schneller und effizienter, als bei jeder Änderung den gesamten DOM-Baum zu aktualisieren.
Das virtuelle DOM ist eine mächtige und einflussreiche Erfindung für das moderne Web. Neuere Bibliotheken wie Preact und Inferno haben es übernommen, nachdem es sich in React bewährt hatte. In Kapitel 4 werden wir mehr über das virtuelle DOM erfahren, aber jetzt gehen wir erst einmal zum nächsten Abschnitt über.
Das Komponentenmodell
React ermutigt dazu, in Komponenten zu denken: Das heißt, du zerlegst deine Anwendung in kleinere Teile und fügst sie in einen größeren Baum ein, um deine Anwendung zu komponieren. Das Komponentenmodell ist ein Schlüsselkonzept in React und macht React so leistungsstark. Lasst uns darüber sprechen, warum:
-
Es ermutigt dazu, überall das Gleiche wiederzuverwenden, damit du es, wenn es kaputt geht, an einer Stelle reparierst und es überall repariert ist. Das nennt man DRY (Don't Repeat Yourself) Entwicklung und ist ein Schlüsselkonzept in der Softwareentwicklung. Wenn wir zum Beispiel eine
Button
Komponente haben, können wir sie an vielen Stellen in unserer App verwenden, und wenn wir den Stil des Buttons ändern müssen, können wir das an einer Stelle tun und es wird überall geändert. -
React ist leichter in der Lage, den Überblick über die Komponenten zu behalten und Performance-Magie wie Memoisierung, Batching und andere Optimierungen unter der Haube durchzuführen, wenn es in der Lage ist, bestimmte Komponenten immer wieder zu identifizieren und Aktualisierungen an den bestimmten Komponenten über die Zeit zu verfolgen. Das nennt man Keying. Wenn wir z. B. eine Komponente
Button
haben, können wir ihr einenkey
prop geben und React kann die KomponenteButton
im Laufe der Zeit verfolgen und "weiß", wann sie aktualisiert werden muss oder wann die Aktualisierung übersprungen werden sollte, um weiterhin minimale Änderungen an der Benutzeroberfläche vorzunehmen. Die meisten Komponenten haben implizite Schlüssel, aber wir können sie auch explizit angeben, wenn wir das möchten. -
Es hilft uns, Probleme zu trennen und die Logik näher an den Teilen der Benutzeroberfläche zu platzieren, die von der Logik betroffen sind. Wenn wir z.B. eine
RegisterButton
Komponente haben, können wir die Logik für das, was passiert, wenn die Schaltfläche angeklickt wird, in dieselbe Datei wie dieRegisterButton
Komponente packen, anstatt in verschiedene Dateien zu springen, um die Logik für das, was passiert, wenn die Schaltfläche angeklickt wird, zu finden. Die KomponenteRegisterButton
würde eine einfachere KomponenteButton
umhüllen, und die KomponenteRegisterButton
wäre für die Logik zuständig, die beim Anklicken der Schaltfläche ausgeführt wird. Dies wird Komposition genannt.
Das Komponentenmodell von React ist ein grundlegendes Konzept, das die Beliebtheit und den Erfolg des Frameworks begründet. Dieser Entwicklungsansatz hat zahlreiche Vorteile, wie z. B. eine größere Modularität, eine einfachere Fehlersuche und eine effizientere Wiederverwendung von Code.
Unveränderlicher Zustand
Die Design-Philosophie von React betont ein Paradigma, bei dem der Zustand unserer Anwendung als eine Menge unveränderlicher Werte beschrieben wird. Jede Statusaktualisierung wird als neuer, eindeutiger Snapshot und Speicherverweis behandelt. Dieser unveränderliche Ansatz für die Zustandsverwaltung ist ein zentraler Bestandteil des Wertversprechens von React und hat mehrere Vorteile für die Entwicklung robuster, effizienter und vorhersehbarer Benutzeroberflächen.
Durch die Erzwingung der Unveränderlichkeit stellt React sicher, dass die UI-Komponenten zu jedem Zeitpunkt einen bestimmten Zustand wiedergeben. Wenn sich der Zustand ändert, gibst du ein neues Objekt zurück, das den neuen Zustand repräsentiert, anstatt ihn direkt zu verändern. Das macht es einfacher, Änderungen zu verfolgen, zu debuggen und das Verhalten deiner Anwendung zu verstehen. Da dieZustandsübergänge diskret sind und sich nicht gegenseitig beeinflussen, ist die Wahrscheinlichkeit von subtilen Fehlern, die durch einen gemeinsamen veränderbaren Zustand verursacht werden, deutlich geringer.
In den nächsten Kapiteln werden wir untersuchen, wie React Statusaktualisierungen stapelt und sie asynchron verarbeitet, um die Leistung zu optimieren. Da der Zustand unveränderlich behandelt werden muss, können diese "Transaktionen" sicher gebündelt und angewendet werden, ohne dass eine Aktualisierung den Zustand einer anderen beschädigt. Dies führt zu einer berechenbareren Zustandsverwaltung und kann die Leistung der App verbessern, insbesondere bei komplexen Zustandsübergängen.
Die Verwendung unveränderlicher Zustände stärkt die bewährten Methoden in der Softwareentwicklung weiter. Er ermutigt die Entwickler/innen, funktional über ihren Datenfluss nachzudenken, wodurch Seiteneffekte reduziert werden und der Code leichter nachvollziehbar wird. Die Klarheit eines unveränderlichen Datenflusses vereinfacht das mentale Modell für das Verständnis, wie eine Anwendung funktioniert.
Unveränderlichkeit ermöglicht auch leistungsstarke Entwicklerwerkzeuge, wie Zeitreise-Debugging mit Tools wie Replay.io, bei dem Entwickler die Zustandsänderungen einer Anwendung vorwärts und rückwärts durchlaufen können, um die Benutzeroberfläche zu jedem beliebigen Zeitpunkt zu überprüfen. Dies ist nur möglich, wenn jede Zustandsänderung als eindeutiger und unveränderter Snapshot gespeichert wird.
Das Engagement von React für unveränderliche Zustandsaktualisierungen ist eine bewusste Designentscheidung, die zahlreiche Vorteile mit sich bringt. Sie steht im Einklang mit den Prinzipien der modernen funktionalen Programmierung und ermöglicht effiziente UI-Updates, optimiert die Leistung, verringert die Wahrscheinlichkeit von Bugs und verbessert die allgemeine Erfahrung der Entwickler. Dieser Ansatz der Zustandsverwaltung liegt vielen der fortschrittlichen Funktionen von React zugrunde und wird auch in Zukunft ein Eckpfeiler sein, wenn sich React weiterentwickelt.
React freigeben
Der unidirektionale Datenfluss war eine radikale Abkehr von der Art und Weise, wie wir jahrelang Webanwendungen entwickelt hatten, und wurde mit Skepsis aufgenommen. Die Tatsache, dass Facebook ein großes Unternehmen mit vielen Ressourcen, vielen Nutzern und vielen Ingenieuren mit eigenen Meinungen war, machte den Aufstieg zu einem steilen Weg. Nach eingehender Prüfung wurde React zu einem internen Erfolg. Es wurde von Facebook und dann von Instagram übernommen.
Im Jahr 2013 wurde es dann als Open Source veröffentlicht und stieß auf heftige Kritik. React wurde wegen der Verwendung von JSX heftig kritisiert, und Facebook wurde vorgeworfen, "HTML in JavaScript einzubauen" und die Trennung der Belange aufzuheben. Facebook wurde als das Unternehmen bekannt, das "bewährte Methoden neu überdenkt" und das Web kaputt macht. Nachdem Unternehmen wie Netflix, Airbnb unddie New York Times React langsam und stetig übernommen hatten, wurde es schließlich zum De-facto-Standard für die Entwicklung von Benutzeroberflächen im Web.
Es ist jedoch wichtig, den Kontext von React zu verstehen, bevor wir in die Details eintauchen: insbesondere die Klasse der technischen Probleme, für deren Lösung React entwickelt wurde. Wenn du dich mehr für die Geschichte von React interessierst, gibt es eine vollständige Dokumentation über dieGeschichte von React, die auf YouTube unter React.js: The Documentary von Honeypot frei verfügbar ist.
Angesichts der Tatsache, dass Facebook bei diesen Problemen in der ersten Reihe saß, leistete React Pionierarbeit mit einem komponentenbasierten Ansatz für die Erstellung von Benutzeroberflächen, der diese Probleme und noch mehr lösen würde, wobei jede Komponente eine in sich geschlossene Codeeinheit ist, die wiederverwendet und mit anderen Komponenten zusammengesetzt werden kann, um komplexere Benutzeroberflächen zu erstellen.
Ein Jahr nachdem React als Open-Source-Software veröffentlicht wurde, brachte Facebook Flux heraus: ein Muster für die Verwaltung des Datenflusses in React-Anwendungen. Flux war eine Antwort auf die Herausforderungen bei der Verwaltung des Datenflusses in großen Anwendungen und ein wichtiger Bestandteil des React-Ökosystems. Werfen wir einen Blick auf Flux und wie es in das React-Ökosystem passt.
Die Flux-Architektur
Flux ist ein architektonisches Entwurfsmuster für die Entwicklung von clientseitigen Webanwendungen, das von Facebook (jetzt Meta) verbreitet wurde (siehe Abbildung 1-3). Es betont einen unidirektionalen Datenfluss, der den Datenfluss innerhalb der App vorhersehbarer macht.
Hier sind die wichtigsten Konzepte der Flux-Architektur:
- Aktionen
-
Aktionen sind einfache Objekte, die neue Daten und eine identifizierende Typeigenschaft enthalten. Sie repräsentieren die externen und internen Eingaben in das System, wie Benutzerinteraktionen, Serverantworten und Formulareingaben. Aktionen werden über einen zentralen Dispatcher an verschiedene Speicher weitergeleitet:
// Example of an action object
{
type
:
'ADD_TODO'
,
text
:
'Learn Flux Architecture'
}
- Dispatcher
-
Der Dispatcher ist der zentrale Knotenpunkt der Flux-Architektur. Er empfängt Aktionen und sendet sie an die registrierten Läden in der Anwendung. Er verwaltet eine Liste von Rückrufen, und jeder Laden registriert sich und seinen Rückruf beim Dispatcher. Wenn eine Aktion versendet wird, wird sie an alle registrierten Rückrufe gesendet:
// Example of dispatching an action
Dispatcher
.
dispatch
(
action
);
- Läden
-
Stores enthalten den Anwendungsstatus und die Logik. Sie ähneln den Models in der MVC-Architektur, aber sie verwalten den Zustand vieler Objekte. Sie melden sich beim Dispatcher an und bieten Callbacks, um die Aktionen zu verarbeiten. Wenn der Zustand eines Stores aktualisiert wird, sendet er ein Änderungsereignis aus, um die Views zu informieren, dass sich etwas geändert hat:
// Example of a store
class
TodoStore
extends
EventEmitter
{
constructor
()
{
super
();
this
.
todos
=
[];
}
handleActions
(
action
)
{
switch
(
action
.
type
)
{
case
"ADD_TODO"
:
this
.
todos
.
push
(
action
.
text
);
this
.
emit
(
"change"
);
break
;
default
:
// no op
}
}
}
- Ansichten
-
Views sind React-Komponenten. Sie hören auf Änderungsereignisse aus den Stores und aktualisieren sich selbst, wenn sich die Daten, von denen sie abhängen, ändern. Sie können auch neue Aktionen erstellen, um den Systemzustand zu aktualisieren und bilden so einen unidirektionalen Kreislauf des Datenflusses.
Die Flux-Architektur fördert einen unidirektionalen Datenfluss durch ein System, was es einfacher macht, Änderungen im Laufe der Zeit zu verfolgen. Diese Vorhersehbarkeit kann später als Grundlage für Compiler genutzt werden, um den Code weiter zu optimieren, wie es bei React Forget der Fall ist (mehr dazu später).
Vorteile der Flux-Architektur
Die Flux-Architektur bringt eine Reihe von Vorteilen mit sich, die helfen, die Komplexität zu bewältigen und die Wartbarkeit von Webanwendungen zu verbessern. Hier sind einige der bemerkenswerten Vorteile:
- Eine einzige Quelle der Wahrheit
-
Flux legt Wert auf eine einzige Quelle der Wahrheit für den Zustand der Anwendung, der in den Stores gespeichert wird. Diese zentrale Zustandsverwaltung macht das Verhalten der Anwendung vorhersehbarer und leichter verständlich. Es vermeidet die Komplikationen, die mit mehreren, voneinander abhängigen Wahrheitsquellen einhergehen, die zu Fehlern und inkonsistenten Zuständen in der gesamten Anwendung führen können.
- Prüfbarkeit
-
Die klar definierten Strukturen und der vorhersehbare Datenfluss von Flux machen die Anwendung hochgradig testbar. Die Trennung von Belangenzwischen verschiedenen Teilen des Systems (wie Actions, Dispatcher, Stores und Views) ermöglicht es, jeden Teil isoliert zu testen. Außerdem ist es einfacher, Tests zu schreiben, wenn der Datenfluss unidirektional ist und der Status an bestimmten, vorhersehbaren Orten gespeichert wird.
- Trennung der Interessen (SoC)
-
Wie bereits beschrieben, trennt Flux die Aufgaben der verschiedenen Teile des Systems klar voneinander. Diese Trennung macht das System modularer, einfacher zu warten und leichter zu verstehen. Jeder Teil hat eine klar definierte Rolle und der unidirektionale Datenfluss macht deutlich, wie diese Teile miteinander interagieren.
Die Flux-Architektur bietet eine solide Grundlage für den Aufbau robuster, skalierbarer und wartbarer Webanwendungen. Die Betonung eines unidirektionalen Datenflusses, einer einzigen Wahrheitsquelle und der Trennung von Belangen führt zu Anwendungen, die einfacher zu entwickeln, zu testen und zu debuggen sind .
Nachbereitung: Warum ist React so eine Sache?
React ist eine tolle Sache, weil es Entwicklern ermöglicht, Benutzeroberflächen mit größerer Vorhersehbarkeit und Zuverlässigkeit zu erstellen. Wir können deklarativ ausdrücken , was wir auf dem Bildschirm haben möchten, während React sich um das Wie kümmert, indem es das DOM auf effiziente Weise inkrementell aktualisiert. Außerdem ermutigt es uns, in Komponenten zu denken, was uns hilft, unsere Anliegen zu trennen und Code leichter wiederzuverwenden. Es ist bei Meta kampferprobt und für den Einsatz in großem Maßstab konzipiert. Außerdem ist es quelloffen und kostenlos zu verwenden.
React verfügt außerdem über ein umfangreiches und aktives Ökosystem, in dem Entwicklern eine Vielzahl von Tools, Bibliotheken und Ressourcen zur Verfügung stehen. Dieses Ökosystem umfasst Tools zum Testen, Debuggen und Optimieren von React-Anwendungen sowie Bibliotheken für allgemeine Aufgaben wie Datenmanagement, Routing und Zustandsverwaltung. Außerdem ist die React-Community sehr engagiert und hilfsbereit. Es gibt viele Online-Ressourcen, Foren und Communities, die Entwicklern helfen, zu lernen und zu wachsen.
React ist plattformunabhängig, d. h., es kann für die Entwicklung von Webanwendungen für eine Vielzahl von Plattformen verwendet werden, darunter Desktop, Mobile und Virtual Reality. Diese Flexibilität macht React zu einer attraktiven Option für Entwickler/innen, die Anwendungen für mehrere Plattformen entwickeln müssen, da sie eine einzige Codebasis verwenden können, um Anwendungen zu erstellen, die auf mehreren Geräten laufen.
Zusammenfassend lässt sich sagen, dass sich das Leistungsversprechen von React auf die komponentenbasierte Architektur, das deklarative Programmiermodell, das virtuelle DOM, JSX, das umfangreiche Ökosystem, die Plattformunabhängigkeit und die Unterstützung durch Meta stützt. Zusammen machen diese Eigenschaften React zu einer attraktiven Option für Entwickler, die schnelle, skalierbare und wartbare Webanwendungen erstellen müssen. Egal, ob du eine einfache Website oder eine komplexe Unternehmensanwendung erstellst, mit React kannst du deine Ziele effizienter und effektiver erreichen als mit vielen anderen Technologien. Schauen wir uns das mal an.
Kapitel Rückblick
In diesem Kapitel haben wir einen kurzen Überblick über die Geschichte von React gegeben, sein ursprüngliches Leistungsversprechen vorgestellt und erklärt, wie es die Probleme unsicherer, unvorhersehbarer und ineffizienter Aktualisierungen von Benutzeroberflächen im großen Maßstab löst. Wir haben auch über das Komponentenmodell gesprochen und darüber, warum es für Benutzeroberflächen im Web revolutionär ist. Fassen wir noch einmal zusammen, was wir gelernt haben. Idealerweise bist du nach diesem Kapitel besser über die Wurzeln von React und seinen Ursprung sowie über seine wichtigsten Stärken und sein Leistungsversprechen informiert.
Fragen überprüfen
Vergewissere dich, dass du die Themen, die wir behandelt haben, vollständig verstanden hast. Nimm dir einen Moment Zeit, um die folgenden Fragen zu beantworten:
-
Was war die Motivation, React zu entwickeln?
-
Wie verbessert React frühere Muster wie MVC und MVVM?
-
Was ist so besonders an der Flux-Architektur?
-
Was sind die Vorteile von deklarativen Programmierabstraktionen?
-
Welche Rolle spielt das virtuelle DOM bei der effizienten Aktualisierung der Benutzeroberfläche?
Wenn du Schwierigkeiten hast, diese Fragen zu beantworten, ist dieses Kapitel vielleicht eine weitere Lektüre wert. Wenn nicht, lass uns das nächste Kapitel lesen.
Als Nächstes
In Kapitel 2 werden wir etwas tiefer in diese deklarative Abstraktion eintauchen, die es uns ermöglicht, das auszudrücken, was wir auf dem Bildschirm sehen wollen: die Syntax und das Innenleben von JSX - der Sprache, die wie HTML in JavaScript aussieht und die React in seinen Anfängen viel Ärger eingebracht hat, sich aber letztendlich als idealer Weg erwiesen hat, um Benutzeroberflächen im Web zu erstellen, und eine Reihe von zukünftigen Bibliotheken für die Erstellung von Benutzeroberflächen beeinflusst hat.
Get Fluent React now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.