Was Web Frameworks lösen und wie man ohne sie auskommt (Teil 1)

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ In diesem Artikel geht Noam Rosenthal tief in einige technische Features ein, die in Frameworks üblich sind, und erklärt, wie einige der verschiedenen Frameworks sie implementieren und was sie kosten.

Ich interessiere mich seit kurzem sehr dafür, Frameworks mit Vanilla JavaScript zu vergleichen. Es begann nach einiger Frustration, die ich bei der Verwendung von React in einigen meiner freiberuflichen Projekte hatte, und mit meiner jüngsten, engeren Bekanntschaft mit Webstandards als Spezifikationseditor.

Mich hat interessiert, welche Gemeinsamkeiten und Unterschiede die Frameworks haben, was die Webplattform als schlankere Alternative zu bieten hat und ob sie ausreicht. Mein Ziel ist es nicht, Frameworks zu verprügeln, sondern die Kosten und Vorteile zu verstehen, festzustellen, ob es eine Alternative gibt, und zu sehen, ob wir daraus lernen können, selbst wenn wir uns für die Verwendung eines Frameworks entscheiden.

In diesem ersten Teil werde ich tief in einige technische Merkmale eintauchen, die Frameworks gemeinsam sind, und wie einige der verschiedenen Frameworks sie implementieren. Ich werde auch die Kosten für die Verwendung dieser Frameworks betrachten.

Die Frameworks

Ich habe vier Frameworks ausgewählt, die ich mir ansehen möchte: React, das heute das dominierende ist, und drei neuere Konkurrenten, die behaupten, Dinge anders zu machen als React.

  • Reagieren
    „React macht es schmerzlos, interaktive UIs zu erstellen. Deklarative Ansichten machen Ihren Code vorhersehbarer und einfacher zu debuggen.“
  • SolidJS
    „Solid folgt der gleichen Philosophie wie React… Es hat jedoch eine völlig andere Implementierung, die auf die Verwendung eines virtuellen DOM verzichtet.“
  • Schlank
    „Svelte ist ein radikal neuer Ansatz zum Erstellen von Benutzeroberflächen … ein Kompilierungsschritt, der beim Erstellen Ihrer App stattfindet. Anstatt Techniken wie das virtuelle DOM-Diffing zu verwenden, schreibt Svelte Code, der das DOM chirurgisch aktualisiert, wenn sich der Status Ihrer App ändert.“
  • Zündete
    „Lit baut auf den Web Components-Standards auf und fügt nur … Reaktivität, deklarative Vorlagen und eine Handvoll durchdachter Funktionen hinzu.“

Um zusammenzufassen, was die Frameworks über ihre Unterscheidungsmerkmale aussagen:

  • React erleichtert das Erstellen von Benutzeroberflächen mit deklarativen Ansichten.
  • SolidJS folgt der Philosophie von React, verwendet jedoch eine andere Technik.
  • Svelte verwendet einen Ansatz zur Kompilierzeit für Benutzeroberflächen.
  • Lit verwendet vorhandene Standards mit einigen zusätzlichen leichten Funktionen.

Welche Frameworks lösen

Die Frameworks selbst erwähnen die Wörter declarative, reactivity und virtual DOM. Lassen Sie uns untersuchen, was diese bedeuten.

Deklarative Programmierung

Die deklarative Programmierung ist ein Paradigma, bei dem Logik definiert wird, ohne den Kontrollfluss anzugeben. Wir beschreiben, was das Ergebnis sein muss, anstatt welche Schritte uns dorthin führen würden.

In den frühen Tagen deklarativer Frameworks, um 2010, waren DOM-APIs viel schlichter und ausführlicher, und das Schreiben von Webanwendungen mit imperativem JavaScript erforderte eine Menge Boilerplate-Code. Zu diesem Zeitpunkt setzte sich das Konzept des „Model-View-Viewmodel“ (MVVM) durch, mit den damals bahnbrechenden Knockout- und AngularJS-Frameworks, die eine deklarative JavaScript-Ebene bereitstellten, die diese Komplexität innerhalb der Bibliothek bewältigte.

MVVM ist heute kein weit verbreiteter Begriff und eine Art Variation des älteren Begriffs „Datenbindung“.

Datenbindung

Die Datenbindung ist eine deklarative Methode, um auszudrücken, wie Daten zwischen einem Modell und einer Benutzeroberfläche synchronisiert werden.

Alle beliebten UI-Frameworks bieten eine Form der Datenbindung, und ihre Tutorials beginnen mit einem Datenbindungsbeispiel.

Hier ist die Datenbindung in JSX (SolidJS und React):

 function HelloWorld() { const name = "Solid or React"; return ( <div>Hello {name}!</div> ) }

Datenbindung in Lit:

 class HelloWorld extends LitElement { @property() name = 'lit'; render() { return html`<p>Hello ${this.name}!</p>`; } }

Datenbindung in Svelte:

 <script> let name = 'world'; </script> <h1>Hello {name}!</h1>

Reaktivität

Reaktivität ist eine deklarative Art, die Ausbreitung von Veränderungen auszudrücken.

Wenn wir eine Möglichkeit haben, die Datenbindung deklarativ auszudrücken, brauchen wir eine effiziente Methode für das Framework, um Änderungen zu verbreiten.

Die React-Engine vergleicht das Ergebnis des Renderns mit dem vorherigen Ergebnis und wendet den Unterschied auf das DOM selbst an. Diese Art der Änderungsweitergabe wird als virtuelles DOM bezeichnet.

In SolidJS geschieht dies expliziter mit seinem Speicher und seinen integrierten Elementen. Beispielsweise würde das Show -Element anstelle des virtuellen DOM nachverfolgen, was sich intern geändert hat.

In Svelte wird der „reaktive“ Code generiert. Svelte weiß, welche Ereignisse eine Änderung verursachen können, und generiert einfachen Code, der die Grenze zwischen dem Ereignis und der DOM-Änderung zieht.

In Lit wird die Reaktivität mithilfe von Elementeigenschaften erreicht, die sich im Wesentlichen auf die eingebaute Reaktivität von benutzerdefinierten HTML-Elementen stützt.

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

Logik

Wenn ein Framework eine deklarative Schnittstelle für die Datenbindung bereitstellt, muss es mit seiner Implementierung von Reaktivität auch eine Möglichkeit bieten, einen Teil der Logik auszudrücken, die traditionell imperativ geschrieben wird. Die grundlegenden Bausteine ​​der Logik sind „wenn“ und „für“, und alle wichtigen Frameworks bieten einen Ausdruck dieser Bausteine.

Bedingungen

Abgesehen von der Bindung von Grunddaten wie Zahlen und Strings liefert jedes Framework ein „bedingtes“ Primitiv. In React sieht das so aus:

 const [hasError, setHasError] = useState(false); return hasError ? <label>Message</label> : null; … setHasError(true);

SolidJS bietet eine eingebaute bedingte Komponente, Show :

 <Show when={state.error}> <label>Message</label> </Show>

Svelte bietet die #if Direktive:

 {#if state.error} <label>Message</label> {/if}

In Lit würden Sie eine explizite ternäre Operation in der render verwenden:

 render() { return this.error ? html`<label>Message</label>`: null; }

Listen

Das andere übliche Framework-Grundelement ist die Listenbehandlung. Listen sind ein wichtiger Bestandteil von Benutzeroberflächen – Kontaktlisten, Benachrichtigungen usw. – und um effizient zu arbeiten, müssen sie reaktiv sein und nicht die gesamte Liste aktualisieren, wenn sich ein Datenelement ändert.

In React sieht die Listenbehandlung wie folgt aus:

 contacts.map((contact, index) => <li key={index}> {contact.name} </li>)

React verwendet das spezielle key , um zwischen Listenelementen zu unterscheiden, und stellt sicher, dass nicht bei jedem Rendern die gesamte Liste ersetzt wird.

In SolidJS werden die eingebauten Elemente for und index verwendet:

 <For each={state.contacts}> {contact => <DIV>{contact.name}</DIV> } </For>

Intern verwendet SolidJS seinen eigenen Speicher in Verbindung mit for und index , um zu entscheiden, welche Elemente aktualisiert werden, wenn sich Elemente ändern. Es ist expliziter als React und ermöglicht es uns, die Komplexität des virtuellen DOM zu vermeiden.

Svelte verwendet die each -Direktive, die basierend auf ihren Updatern transpiliert wird:

 {#each contacts as contact} <div>{contact.name}</div> {/each}

Lit stellt eine repeat bereit, die ähnlich wie die key Listenzuordnung von React funktioniert:

 repeat(contacts, contact => contact.id, (contact, index) => html`<div>${contact.name}</div>`

Komponentenmodell

Eine Sache, die außerhalb des Rahmens dieses Artikels liegt, ist das Komponentenmodell in den verschiedenen Frameworks und wie damit umgegangen werden kann, indem benutzerdefinierte HTML-Elemente verwendet werden.

Hinweis : Dies ist ein großes Thema, und ich hoffe, es in einem zukünftigen Artikel zu behandeln, da dieser zu lang werden würde. :)

Die Kosten

Frameworks bieten deklarative Datenbindung, Kontrollfluss-Primitive (Bedingungen und Listen) und einen reaktiven Mechanismus zur Weitergabe von Änderungen.

Sie bieten auch andere wichtige Dinge, wie z. B. eine Möglichkeit, Komponenten wiederzuverwenden, aber das ist ein Thema für einen separaten Artikel.

Sind Frameworks sinnvoll? Jawohl. Sie geben uns all diese praktischen Funktionen. Aber ist das die richtige Frage? Die Verwendung eines Frameworks ist mit Kosten verbunden. Mal sehen, was diese Kosten sind.

Bündelgröße

Wenn ich mir die Bundle-Größe ansehe, schaue ich mir gerne die minimierte Nicht-Gzip-Größe an. Das ist die Größe, die für die CPU-Kosten der JavaScript-Ausführung am relevantesten ist.

  • ReactDOM ist etwa 120 KB groß.
  • SolidJS ist etwa 18 KB groß.
  • Lit ist etwa 16 KB groß.
  • Svelte ist etwa 2 KB groß, aber die Größe des generierten Codes variiert.

Es scheint, dass die heutigen Frameworks einen besseren Job machen als React, um die Paketgröße klein zu halten. Das virtuelle DOM erfordert viel JavaScript.

Baut

Irgendwie haben wir uns daran gewöhnt, unsere Web-Apps zu „bauen“. Es ist unmöglich, ein Frontend-Projekt zu starten, ohne Node.js und einen Bundler wie Webpack einzurichten, sich mit einigen kürzlichen Konfigurationsänderungen im Babel-TypeScript-Starterpaket zu befassen und all das Zeug.

Je ausdrucksstärker und je kleiner die Bundle-Größe des Frameworks ist, desto größer ist die Belastung durch Build-Tools und die Transpilationszeit.

Svelte behauptet, dass das virtuelle DOM reiner Overhead ist. Ich stimme zu, aber vielleicht sind „Bauen“ (wie bei Svelte und SolidJS) und benutzerdefinierte clientseitige Template-Engines (wie bei Lit) auch reiner Overhead anderer Art?

Debuggen

Beim Bauen und Transpilieren fallen andere Kosten an.

Der Code, den wir sehen, wenn wir die Web-App verwenden oder debuggen, unterscheidet sich völlig von dem, was wir geschrieben haben. Wir verlassen uns jetzt auf spezielle Debugging-Tools unterschiedlicher Qualität, um das, was auf der Website passiert, zurückzuentwickeln und mit Fehlern in unserem eigenen Code zu verbinden.

In React gehört der Call-Stack nie „dein“ – React übernimmt die Planung für dich. Das funktioniert super, wenn es keine Bugs gibt. Aber versuchen Sie, die Ursache für das erneute Rendern in Endlosschleife zu identifizieren, und Sie werden eine Welt voller Schmerzen erleben.

In Svelte ist die Bündelgröße der Bibliothek selbst klein, aber Sie werden eine ganze Reihe von kryptisch generiertem Code versenden und debuggen, der Sveltes Implementierung von Reaktivität ist, die an die Anforderungen Ihrer App angepasst ist.

Bei Lit geht es weniger ums Bauen, sondern um es effektiv zu debuggen, muss man seine Template-Engine verstehen. Dies könnte der Hauptgrund sein, warum ich Frameworks gegenüber skeptisch eingestellt bin.

Wenn Sie nach benutzerdefinierten deklarativen Lösungen suchen, enden Sie mit einem schmerzhafteren imperativen Debugging. Die Beispiele in diesem Dokument verwenden Typescript für die API-Spezifikation, aber der Code selbst erfordert keine Transpilation.

Upgrades

In diesem Dokument habe ich mir vier Frameworks angesehen, aber es gibt mehr Frameworks, als ich zählen kann (AngularJS, Ember.js und Vue.js, um nur einige zu nennen). Können Sie sich darauf verlassen, dass das Framework, seine Entwickler, sein Mindshare und sein Ökosystem für Sie arbeiten, während es sich weiterentwickelt?

Eine Sache, die frustrierender ist, als eigene Fehler zu beheben, ist, Workarounds für Framework-Fehler finden zu müssen. Und noch frustrierender als Framework-Fehler sind Fehler, die auftreten, wenn Sie ein Framework auf eine neue Version aktualisieren, ohne Ihren Code zu ändern.

Dieses Problem existiert zwar auch in Browsern, aber wenn es auftritt, passiert es jedem, und in den meisten Fällen steht ein Fix oder eine veröffentlichte Problemumgehung unmittelbar bevor. Außerdem basieren die meisten Muster in diesem Dokument auf ausgereiften Webplattform-APIs; es ist nicht immer notwendig, mit der blutenden Kante zu gehen.

Zusammenfassung

Wir sind etwas tiefer in das Verständnis der Kernprobleme eingetaucht, die Frameworks zu lösen versuchen, und wie sie sie lösen, wobei wir uns auf Datenbindung, Reaktivität, Bedingungen und Listen konzentriert haben. Wir haben uns auch die Kosten angeschaut.

In Teil 2 werden wir sehen, wie diese Probleme ohne die Verwendung eines Frameworks angegangen werden können und was wir daraus lernen können. Bleib dran!

Besonderer Dank gilt den folgenden Personen für technische Überprüfungen: Yehonatan Daniv, Tom Bigelajzen, Benjamin Greenbaum, Nick Ribal und Louis Lazaris.