Chorda. Versuch es deklarativ zu machen

Jedes Mal, wenn ich mich hinsetzen muss, um eine neue Anwendung zu erstellen, bin ich leichtsinnig. In meinem Kopf dreht sich alles um die Frage, welche Bibliothek oder welches Framework diesmal benötigt wird. Das letzte Mal, als ich über die X-Bibliothek schrieb, aber jetzt ist das Y-Framework erwachsen und übergeben worden, und es gibt immer noch ein cooles UI-Kit Z, und eine Menge Arbeit ist von früheren Projekten übrig geblieben.


Irgendwann wurde mir klar, dass das Framework keine Rolle spielt - was ich brauche, kann ich auf jedem von ihnen tun. Hier scheint es, als sollte man glücklich sein, etwas mit dem Maximum an Sternen auf den Github nehmen und sich beruhigen. Trotzdem entsteht immer wieder der unwiderstehliche Wunsch, etwas Eigenes zu tun, sein eigenes Fahrrad. Na dann. Einige allgemeine Gedanken zu diesem Thema und ein Rahmen namens Chorda warten auf Sie unter dem Schnitt.


In der Tat ist das Problem nicht, dass die Entscheidung eines anderen schlecht oder unwirksam ist. Nein. Das Problem ist, dass uns die Entscheidung eines anderen dazu bringt, auf eine Weise zu denken, die für uns möglicherweise unpraktisch ist. Aber warte. Was bedeutet „bequem-unbequem“ und wie kann sich dies überhaupt auf die Entwicklung auswirken? Denken Sie daran, dass es in der Tat so etwas wie DX gibt - eine Reihe etablierter persönlicher und allgemein akzeptierter Praktiken. Von hier aus können wir sagen, dass es für uns bequem ist, wenn unser eigenes DX mit dem DX des Autors der Bibliothek oder des Frameworks übereinstimmt. Und in dem Fall, in dem sie auseinander gehen, entsteht das Unbehagen, die Verärgerung und die Suche nach etwas Neuem.


Ein bisschen Geschichte


Wenn Sie eine Benutzeroberfläche für eine Unternehmensanwendung entwickeln, werden Sie mit einer großen Anzahl von Benutzerformularen konfrontiert. Und eines Tages taucht ein brillanter Gedanke in meinem Kopf auf: Warum erstelle ich jedes Mal ein Webformular, wenn ich einfach die Felder in JSON auflisten und die resultierende Struktur dem Generator zuführen kann? Und obwohl dieser Ansatz in der Welt des blutigen Unternehmertums nicht allzu gut funktioniert (warum das so ist, ist dies eine separate Konversation), ist die Idee, von einem imperativen zu einem deklarativen Stil zu wechseln, im Allgemeinen nicht schlecht. Ein Beweis dafür ist die große Anzahl von Generatoren von Webformularen, -seiten und sogar ganzen Websites, die im Web leicht zu finden sind.


Irgendwann war mir also der Wunsch, meinen Code aufgrund des Übergangs zur Deklarativität zu verbessern, nicht fremd. Sobald wir jedoch nicht nur Standard-HTML-Elemente, sondern auch komplexe und interaktive Widget-Komponenten benötigten, konnten wir mit einem einfachen Generator nicht mehr davonkommen. Die Anforderungen an Code-Wiederverwendbarkeit, Integrierbarkeit, Erweiterbarkeit usw. kamen schnell hinzu. Die Entwicklung einer eigenen Komponentenbibliothek mit einer deklarativen API ließ nicht lange auf sich warten.


Aber hier geschah kein Glück. Die wahrscheinlich beste Situation spiegelt die Meinung meines Kollegen wider, der die erstellte Bibliothek nutzen sollte. Er schaute sich die Beispiele an, die Dokumentation und sagte: "Die Bibliothek ist cool. Schön, dynamisch. Aber wie kann ich jetzt eine Anwendung daraus machen?" Und er hatte recht. Es stellte sich heraus, dass es nicht gleichbedeutend ist, mehrere Komponenten zu kombinieren, damit sie nahtlos funktionieren.


Seitdem ist viel Zeit vergangen. Und als ich wieder einmal von dem Wunsch besucht wurde, Gedanken und Ideen zusammenzubringen, beschloss ich, etwas anderes zu machen und nicht von unten nach oben zu gehen, sondern von oben nach unten.


Anwendungsverwaltung == Statusverwaltung


Ich bin eher daran gewöhnt, die Anwendung als eine Finite-State-Maschine mit einigen Klonzuständen zu betrachten. Und die Arbeit der Anwendung als eine Reihe von Übergängen von einem Zustand in einen anderen, bei denen das Ändern des Modells zur Erstellung einer neuen Version der Ansicht führt. In Zukunft werde ich einige feste Daten (ein Objekt, ein Array, einen primitiven Typ usw.) aufrufen, die sich auf ihre einzige Darstellung beziehen - ein Dokument .


Es gibt ein offensichtliches Problem: Für viele Werte des Modells müssen viele Optionen für das Dokument beschrieben werden. Hier werden üblicherweise zwei Ansätze verwendet:


  1. Vorlagen. Wir verwenden unsere bevorzugte Auszeichnungssprache und ergänzen sie mit Verzweigungs- und Schleifenanweisungen.
  2. Funktionen Wir beschreiben in unseren Funktionen unsere Zweige und Schleifen in unserer bevorzugten Programmiersprache.

Beide Ansätze werden in der Regel deklarativ deklariert. Die erste wird als deklarativ angesehen, da sie auf, wenn auch etwas erweiterten, Regeln der Auszeichnungssprache basiert. Die zweite - weil sie sich auf die Zusammensetzung von Funktionen konzentriert, von denen einige als Regeln fungieren. Bemerkenswerterweise gibt es derzeit keine klare Grenze zwischen Vorlagen und Funktionen.


Einerseits mag ich Vorlagen, andererseits wollte ich die Funktionen von Javascript irgendwie nutzen. Zum Beispiel so etwas:


createFromConfig({ data: { name: 'Alice' }, tag: 'div', class: 'clickable box', onClick: function () { alert('Click') } }) 

Das Ergebnis ist eine JS-Konfiguration, die einen bestimmten Zustand beschreibt . Um die vielen Zustände zu beschreiben, muss die Erweiterbarkeit dieser Konfiguration erreicht werden. Und was ist der bequemste Weg, eine Reihe von Optionen erweiterbar zu machen? Wir werden hier nichts erfinden - Überladungsmöglichkeiten gibt es schon lange. Wie es funktioniert, zeigt das Beispiel von Vue mit seiner Options-API. Aber im Gegensatz zu demselben Vue habe ich mich gefragt, ob der vollständige Zustand, einschließlich der Daten und des Dokuments, auf die gleiche Weise beschrieben werden kann.


Antragsstruktur und Deklarativität


Der Begriff „Komponente“ ist zu vage geworden, insbesondere nach dem Auftreten des sogenannten funktionale Komponenten. Im weiteren Verlauf der Struktur der Anwendung werde ich die Komponente als Strukturelement bezeichnen .

Sehr schnell kam ich zu dem Schluss, dass das Strukturelement (Komponente) kein Dokumentelement ist, sondern eine Entität, die:


  1. kombiniert Daten und Dokumente (Bindung und Ereignisse)
  2. verbunden mit anderen ähnlichen Entitäten (Baumstruktur)

Wie ich bereits erwähnt habe, müssen Sie für diese Zustände eine Beschreibungsmethode haben, wenn Sie die Anwendung als eine Reihe von Zuständen wahrnehmen. Darüber hinaus ist es notwendig, ein solches Verfahren so zu finden, dass es keine "falschen" Imperativoperatoren enthält. Wir sprechen über die sehr hilfreichen Elemente, die in die Vorlagen eingefügt werden - #if , #elsif , v-for usw. Ich denke, viele Leute kennen die Lösung bereits - es ist notwendig, die Logik auf das Modell zu übertragen und auf der Präsentationsebene eine API zu belassen, mit der Sie Strukturelemente über einfache Datentypen steuern können.


Unter Management verstehe ich das Vorhandensein von Variabilität und Zyklizität.


Variabilität (wenn-sonst)


Schauen wir uns an, wie Sie die Anzeigeoptionen am Beispiel einer Kartenkomponente in Chorda steuern können:


 const isHeaderOnly = true const card = new Html({ $header: { /*  */ }, $footer: { /*  */ }, components: {header: true, footer: !isHeaderOnly} //    }) 

Durch Festlegen des Werts der Komponentenoption können Sie die angezeigten Komponenten steuern. Und wenn wir Komponenten mit reaktivem Speicher verknüpfen, erhalten wir, dass unsere Struktur unter Datenmanagement fällt. Es gibt eine Einschränkung: Object wird als Wert verwendet, und die Schlüssel darin sind nicht geordnet, was die Komponenten einschränkt.


Zyklus (für)


Wenn Sie mit Daten arbeiten, deren Menge nur zur Laufzeit bekannt ist, müssen Sie die Listen iterieren.


 const drinks = ['Coffee', 'Tea', 'Milk'] const html = new Html({ html: 'ul', css: 'list', defaultItem: { html: 'li', css: 'list-item' }, items: drinks }) 

Der Wert der Option items ist Array, bzw. wir erhalten einen geordneten Komponentensatz. Das Binden von Gegenständen an den Speicher, wie im Fall von Komponenten, überträgt die Kontrolle auf die Daten.


Strukturelemente sind in einer Baumstruktur miteinander verbunden. Wenn wir die vorherigen Beispiele kombinieren, erhalten wir zum Anzeigen der Liste im Hauptteil der Karte Folgendes:


 //   const state = { struct: { header: true, footer: false, }, drinks: ['Coffee', 'Tea', 'Milk'] } //  const card = new Html({ $header: { /*  */ }, $content: { html: 'ul', css: 'list', defaultItem: { html: 'li', css: 'list-item' }, items: state.drinks }, $footer: { /*  */ }, components: state.struct }) 

Ungefähr auf diese Weise wird die Datenstruktur der Anwendung erstellt. Es ist ausreichend, zwei Arten von Generatoren zu haben - basierend auf Object und basierend auf Array. Es bleibt nur zu verstehen, wie die Umwandlung von Strukturelementen in ein Dokument erfolgt.


Wenn für uns schon alles erfunden ist


Im Allgemeinen befürworte ich die Tatsache, dass das Dokument-Rendering-System auf Browserebene implementiert werden sollte (wenn auch mindestens auf der gleichen VDOM-Ebene). Und unsere Aufgabe wird es nur sein, es sorgfältig mit dem Komponentenbaum zu verbinden. Denn egal wie schnell die Bibliothek wächst, der Browser hat es trotzdem mehr.


Ich habe ehrlich gesagt irgendwann versucht, meine Rendering-Funktion zu aktivieren, aber nach einer Weile habe ich aufgegeben, weil ich nicht schneller zeichnen kann als VanillaJS (leider!). Jetzt ist es in Mode, VDOM zum Rendern zu verwenden, und seine Implementierungen sind möglicherweise sogar im Überfluss vorhanden. Also, plus einer weiteren Implementierung des virtuellen Baums, habe ich beschlossen, ihn nicht zum Sparschwein des Githubs hinzuzufügen - nur das nächste Framework reicht aus.


Anfänglich wurde in Chorda ein Adapter für die Maquette-Bibliothek zum Rendern erstellt. Sobald jedoch Aufgaben "aus der realen Welt" auftauchten, stellte sich heraus, dass es praktischer war, eine Schublade für React zu haben. In diesem Fall können Sie beispielsweise einfach die vorhandenen React DevTools verwenden und keine eigenen schreiben.


Um VDOM mit Strukturelementen zu verbinden, benötigen Sie so etwas wie Layout . Es kann als Dokumentfunktion eines Strukturelements bezeichnet werden. Wichtig ist eine reine Funktion.


Stellen Sie sich ein Beispiel mit einer Karte vor, die einen Kopf, einen Körper und einen Keller hat. Es wurde bereits erwähnt, dass die Komponenten nicht bestellt sind, d.h. Wenn wir die Komponenten während des Betriebs ein- und ausschalten, werden sie jedes Mal in einer neuen Reihenfolge angezeigt. Mal sehen, wie das durch das Layout gelöst wird:


 function orderedByKeyLayout (h, type, props, components) { return h(type, props, components.sort((a, b) => a.key - b.key).map(c => c.render())) } const html = new Html({ $header: {}, $content: {}, $footer: {}, layout: orderedByKeyLayout //     }) 

Das Layout ermöglicht es Ihnen, die sogenannten zu konfigurieren Das Host-Element, dem die Komponente zugeordnet ist, und ihre untergeordneten Elemente ( Elemente und Komponenten ). Normalerweise reicht auch ein Standardlayout aus, aber in einigen Fällen erfordert das Layout das Vorhandensein von Wrapper-Elementen (z. B. für Raster) oder die Zuweisung spezieller Klassen, die wir nicht auf die Ebene der Komponenten anwenden möchten.


Eine Prise Reaktivität


Nachdem wir die Struktur der Komponenten deklariert und gezeichnet haben, erhalten wir einen Status, der einem bestimmten Datensatz entspricht. Als nächstes müssen wir die vielen Datensätze und die Reaktion auf ihre Änderung beschreiben.


Beim Arbeiten mit Daten haben mir zwei Dinge nicht gefallen:


  • Immunität. Eine gute Sache, um Änderungen im Auge zu behalten, ist die Versionierung für die Armen, die sich hervorragend für primitive und flache Objekte eignet. Sobald jedoch die Struktur komplexer wird und die Anzahl der Investitionen zunimmt, wird es schwierig, die Immunität eines komplexen Objekts aufrechtzuerhalten.
  • Substitution. Wenn ich ein Objekt in das Data Warehouse lege, kann ich auf meine Anfrage hin eine Kopie davon oder ein anderes Objekt oder einen Proxy im Allgemeinen zurückgeben, das / der strukturelle Ähnlichkeiten aufweist.

Ich wollte ein Repository, das sich wie unveränderlich verhält, aber darin veränderbare Daten enthält, die auch die Persistenz der Links aufrechterhalten. Im Idealfall sieht das so aus: Ich erstelle ein Repository, schreibe ein leeres Objekt hinein, beginne mit der Eingabe von Daten aus dem Antragsformular und nach dem Klicken auf die Schaltfläche "Senden" erhalte ich dasselbe Objekt (verlinke dasselbe!) Mit den ausgefüllten Eigenschaften. Ich nenne diesen Fall ideal, da es nicht oft vorkommt, dass das Speichermodell mit dem Präsentationsmodell übereinstimmt.


Eine weitere Aufgabe, die gelöst werden muss, besteht darin, Daten aus dem Speicher an die Strukturelemente zu liefern. Auch hier werden wir nichts erfinden und den Ansatz der Verbindung zu einem gemeinsamen Kontext verwenden . Im Fall von Chorda haben wir keinen Zugriff auf den Kontext selbst, sondern nur auf dessen Anzeige, den Scope . Darüber hinaus ist der Bereich der Komponente der Kontext für ihre untergeordneten Komponenten. Dieser Ansatz ermöglicht es Ihnen, verwandte Daten auf jeder Ebene unserer Anwendung einzugrenzen, zu erweitern oder zu ersetzen, und diese Änderungen werden isoliert.


Ein Beispiel für die Verteilung von Kontextdaten über einen Komponentenbaum:


 const html = new Html({ //     scope: { drink: 'Coffee' }, $component1: { scope: { cups: 2 }, $content: { $myDrink: { //      ,    drinkChanged: function (v) { //    drink   text this.opt('text', v) } }, $numCups: { cupsChanged: function (v) { this.opt('text', v + ' cups') } } } }, $component2: { scope: { drink: 'Tea' //      drink }, drinkChanged: function (v) { //    drink   text this.opt('text', v) } } }) //    // <div> // <div> // <div> // <div>Coffee</div> // <div>2 cups</div> // </div> // </div> // <div>Tea</div> // </div> 

Am schwierigsten zu verstehen ist, dass jede Komponente ihren eigenen Kontext hat und nicht den, der ganz oben in der Struktur deklariert ist, wie wir es normalerweise bei der Arbeit mit Vorlagen tun.


Was ist mit der Überladung von Optionen?


Sicherlich haben Sie es mit einer Situation zu tun, in der eine große Komponente vorhanden ist und eine kleine verschachtelte Komponente irgendwo tief im Inneren geändert werden muss. Sie sagen, dass Granulierung und Zusammensetzung hier helfen sollten. Außerdem müssen Komponenten und Architektur sofort entworfen werden. Die Situation wird sehr traurig, wenn die große Komponente nicht Ihnen gehört, sondern Teil einer Bibliothek ist, die von einem anderen Team oder sogar einer unabhängigen Community entwickelt wurde. Was wäre, wenn sie Änderungen an der Basiskomponente vornehmen könnten, auch wenn sie ursprünglich nicht geplant waren?


Normalerweise werden Komponenten in Bibliotheken als Klassen entworfen und können dann als Grundlage für die Erstellung neuer Komponenten verwendet werden. Aber hier ist ein kleines Feature versteckt, das ich nie gemocht habe: Manchmal erstellen wir eine Klasse, um sie nur an einem einzigen Ort anzuwenden. Es ist seltsam. Zum Beispiel bin ich es gewohnt, Klassen zum Schreiben zu verwenden, Beziehungen zwischen Gruppen von Objekten aufzubauen und sie nicht zu verwenden, um das Zerlegungsproblem zu lösen.


Mal sehen, wie Klassen mit der Konfiguration in Chorda arbeiten.


 //      class Card extends Html { config () { return { css: 'box', $header: {}, $content: {}, $footer: {} } } } const html = new Html({ css: 'panel', $card: { as: Card, $header: { //       title $title: { css: 'title', text: 'Card title' } } } }) 

Ich mag diese Option mehr als das Erstellen einer speziellen TitledCard-Klasse, die nur einmal verwendet wird. Und wenn Sie einen Teil der Optionen nutzen möchten, können Sie den Verunreinigungsmechanismus verwenden. Nun, niemand hat Object.assign abgebrochen.


In Chorda ist eine Klasse im Wesentlichen ein Container für die Konfiguration und spielt die Rolle einer besonderen Art von Verunreinigung.


Warum ein anderes Framework?


Ich wiederhole, dass es meiner Meinung nach mehr um das Denken und Erleben als um Technologie geht. Meine Gewohnheiten und DX verlangten Deklarativität in JS, die ich in anderen Lösungen nicht finden konnte. Aber die Implementierung eines Features brachte neue mit sich, und nach einer Weile passten sie einfach nicht mehr in den Rahmen einer Spezialbibliothek.


Derzeit befindet sich Chorda in der aktiven Entwicklung. Die Hauptrichtungen sind bereits sichtbar, aber die Details ändern sich ständig.


Vielen Dank für das Lesen bis zum Ende. Ich würde mich über Bewertungen freuen.


Wo kann ich sehen


Die Dokumentation


GitHub-Quellen


CodePen Beispiele

Source: https://habr.com/ru/post/de480856/


All Articles