Anleitung zur Benutzeroberfläche im Jahr 2018

Ein rein funktionaler Ansatz für die UI-Komposition mit ES6 / TypeScript

Update 2019: Schauen Sie sich dieses und verwandte Projekte in dieser laufenden Blog-Reihe genauer an: Von Regenschirmen, Wandlern, reaktiven Strömen und Pilzen ...

Die Leichtigkeit, Substantive in Verben umzuwandeln, ist ein fast unvergleichliches Merkmal der englischen Sprache und ein zentraler Grundsatz des Folgenden: Die Idee, die Komposition und die kontinuierliche (Neu-) Erstellung einer Benutzeroberfläche in ein Verb, eine Aktion, einen Prozess umzuwandeln , eine Funktion.

Unter UI- und UX-Enthusiasten scheint es nie einen Mangel an "Next-Generation-Frameworks" zu geben, die mehr Effizienz, Einfachheit und Zufriedenheit in all Ihren Komponenten und Vorlagenanforderungen versprechen . Seien Sie bitte auch nicht zu schnell mit Ihrem Browser-Zurück-Button - Sie könnten es später bereuen ... :)

Apropos Templating (zumindest in 95% der Fälle): Dies ist der erste Punkt, den ich ansprechen und ernsthaft hinterfragen möchte, warum die große Mehrheit der Designer und Entwickler dies immer noch für eine großartige Idee hält. Frameworks wie React, Angular, Vue usw. haben unsere kleine Welt im Sturm erobert, und so sehr ich die vielen Neuerungen begrüße, die sie an den Tisch gebracht haben, und so sehr sie sich auch intern unterscheiden, sie alle umfassen HTML im wahrsten Sinne des Wortes .

Auf den ersten Blick ist das natürlich völlig selbstverständlich. HTML ist die einzige Möglichkeit, einen Browser dazu zu bringen, uns zu zeigen, was wir erstellen möchten, und ich habe nach 23 Jahren Arbeit damit immer noch keinen Streit damit. Da es sich jedoch um eine Auszeichnungssprache handelt, ist sie mit der anderen Standardzutat für die Erstellung moderner Benutzeroberflächen - JavaScript - nicht besonders gut (und sollte es auch nicht). Die (offensichtliche) Lösung, für die sich alle großen Player entschieden haben, bestand darin, neue Dateiformate zu zaubern, die es Frontend-Entwicklern ermöglichen, Ausschnitte von HTML-ähnlichen Markups über ihre Quelldateien zu streuen, und dann wiederum eine beeindruckende Reihe von Werkzeugen zu benötigen. Engineering, Dokumentation, Schulung, projektspezifische Komponentenbibliotheken, Editorenunterstützungsprojekte, Parser, Compiler, Quellkartengeneratoren und Gerüsthelfer - jede mit endlosen Abhängigkeiten und einem Arbeitsaufwand von Millionen von Mannjahren. Und das alles, um diese frankensteinische Ehe aus reaktiviertem, kantigem, überarbeitetem und mit Markierungen versehenem JavaScript auf magische Weise wieder in… JavaScript umzuwandeln. Und das alles, weil wir anscheinend nicht auf die Verwendung von HTML verzichten können, um unsere Benutzeroberflächen zu definieren. Und das alles, weil unser Framework einen etwas anderen Ansatz als das „andere“ verwendet und daher eine ähnliche Doppelarbeit erfordert. Und das alles, denn obwohl JavaScript die beliebteste und am weitesten verbreitete Programmiersprache der letzten 10 Jahre ist, können wir uns immer noch nicht dazu durchringen, die immer noch alte Idee von PHP-Templating-Engines aufzugeben und unsere zu übersehen Benutzeroberflächen für das, was sie wirklich sind:

In CS speak: Abgeleitete Ansichten reiner Daten

Im Ernst, dies ist nichts Neues und es ist die Essenz des MV * -Designmusters, auf dem fast alle modernen UI-Frameworks basieren. Ich glaube jedoch, dass wir irgendwo entlang der „V“ -Teile (iew) dieser Muster kollektiv die falsche Richtung eingeschlagen haben (eher in unserer Aufregung um eine bessere Zukunft verpasst haben) und vergessen haben, dass Daten nur Daten sind, was in einem Turing bedeutet. Für eine vollständige Sprache wie JavaScript sollte es leicht möglich sein, diese abgeleiteten Ansichten zu erstellen, ohne auf magische HTML-Injektionspillen zurückgreifen zu müssen. Nicht nur das, sondern vielleicht auch viel eleganter und kraftvoller, nur indem Sie die Host-Sprache in vollem Umfang nutzen. Denken Sie auch daran, dass sich keines der genannten Frameworks für HTML als Laufzeitformat interessiert. Jeder manipuliert das Browser-DOM über JavaScript-Befehle, Befehle, die aus seinen HTML-ish-Template-Snippets vorkompiliert wurden. Das Vorhandensein von HTML-Syntax ist daher fast ausschließlich für Authoring- und Code-Generierungszwecke vorhanden und dient als Annehmlichkeit und als flache Übergangskurven gegenüber früheren Ansätzen. MVC ging es um die Trennung von Bedenken. Gruppe von vier. Unternehmensmuster für Dummies. Die Trennung von Bedenken im Software-Design muss nicht unbedingt die Trennung von Technologien bedeuten, wie dies jetzt bei uns der Fall ist. Was ist, wenn wir HTML die gleiche Behandlung oder Rolle geben, die vor einiger Zeit für JavaScript vorgeschlagen wurde:

HTML ist (nur) eine UI-Assemblersprache

JavaScript hat einen langen Weg zurückgelegt, um das zu werden, was es heute ist, und viele der jüngeren Ergänzungen der Sprache haben seine größte offene Wunde (ad) verkleidet und sie auf einen bloßen, gelegentlichen wunden Punkt reduziert, d. H. Mit Daten arbeiten. Dies gilt sowohl für Datenstrukturen als auch für den Datenfluss. ES6-Spread-Operator, Iteratoren, Generatoren, Maps, Sets, Vorlagenliterale - für die Zwecke dieses Artikels sind nur einige von ihnen relevant (Hinweis: Es sind keine Vorlagenliterale!) Und ich habe mich oft gefragt, warum der folgende Ansatz nicht mehr geworden ist verbreitet. Aber zuerst noch etwas zum Stand der Technik:

Markenidentitätsgenerator für das Leeds College of Music (2012–2013), erstellt in Clojure, OpenGL & OpenCL. Die gesamte Benutzeroberfläche besteht aus einer einzelnen, tief verschachtelten Clojure-Hashmap, die verschiedene Konfigurationsoptionen, Layouts, Wertebeschränkungen, ausgelöste Ereignisse usw. vollständig beschreibt / einschränkt.

Das obige Beispiel für Leeds College of Music war keine Webanwendung und in historischer Hinsicht keineswegs besonders, aber es war das erste Mal, dass ich eine Benutzeroberfläche erstellt habe, die vollständig auf reinen Daten basiert und nur eine einzige, verschachtelte Hashmap zum Ausdrücken verwendet Sowohl die Konfiguration als auch der aktuelle Status jeder einzelnen Komponente. Anstelle von DOM-Elementen wurden diese UI-Komponenten durch eine einzige rekursive Transformationsfunktion zusammengestellt, die andere Funktionen für jeden Baumzweig aufrief, um verschiedene OpenGL-VBOs zu erzeugen. Anstelle von CSS wurden Shader für das Layout und das Theming verwendet. Die Idee ist jedoch die gleiche für Web-Apps:

In Mathe sprich: ui = f (s)

Ihre Benutzeroberfläche ist das Ergebnis der wiederholten Anwendung einer Transformationsfunktion f auf den sich ständig ändernden internen Status Ihrer App.

Die Kernidee aller Komponenten-Frameworks besteht darin, die wörtliche Verwendung der Anweisungen zum Generieren der Benutzeroberfläche (z. B. HTML-Markup oder OpenGL-Aufrufe) wegzulassen, damit wir unsere Benutzeroberflächen in Form von Funktionspaketen ausdrücken können. Wie bereits erwähnt, bestand der bisher übliche Ansatz darin, diese Ausschnitte aus "Lowlevel" -HTML in speziellen Quelldateien zu verbergen, die während der Vorverarbeitung in irgendeiner Weise eingeschlossen wurden. Was aber, wenn wir HTML komplett umgehen und ausdrücken können:

Benutzeroberflächenkomponenten, die nur Vanille-JavaScript verwenden?

Der funktionale Ansatz zur UI-Zusammensetzung ist genau das, was React & Co. Das tun wir eigentlich schon intern, aber wir müssen immer noch das Markup für unsere Komponenten in einem Format ausdrücken, das für JS im Grunde genommen eine Blackbox ist und uns zwingt, durch all diese verschiedenen Rahmen zu springen. Meine zwei wichtigsten Erkenntnisse aus den 7 Jahren, die ich bisher mit Clojure / ClojureScript verbracht habe, waren:

  1. Die bewusste Erkenntnis, dass „Einfachheit“ in Softwarekreisen weitestgehend nur als Dienst am Komfort und an bestehenden Gewohnheiten interpretiert und aktiv gefeiert wird. Es ist systemisch, aber nicht allgegenwärtig. Z.B. Rich Hickeys ruhige Klarheit des Denkens und die Fähigkeit, einen Schritt zurück zu gehen, um die vorherrschenden Designentscheidungen zu überdenken, haben viele andere innerhalb und außerhalb der Clojure-Community dazu veranlasst, den Status Quo der gängigeren Sprachcamps in Frage zu stellen, und wir haben mehrere wichtige Neuerungen von der Clojure-Community gesehen schnell übergreifen und in andere Sprachen verpflanzt werden.
  2. Clojure ist ein Dialekt von Lisp, einer Sprache, in der oft keine klare Trennung zwischen Daten und Code besteht. In dieser Sprache wird sogar Quellcode buchstäblich als rekursive Datenstruktur codiert und verarbeitet. Ich habe gelernt, S-Ausdrücke (in all ihren Formen) zu bewerten ) als der ultimative und einfachste Ansatz zur Codierung baumbasierter Daten, z nicht nur UI-Beschreibungen.

Einige argumentieren, dass JavaScript und HTML ebenfalls zum Lisp-Stammbaum gehören (auch wenn sie weit entfernten Verwandten ähneln), aber es ist nicht zu leugnen, dass beide konzeptionell zum Teil aus Lisps S-Ausdrücken stammen. Da wir in JS nur Arrays oder Objekte auf diese wörtliche Weise erstellen können, beschränken wir uns darauf, nur diese beiden syntaktischen Formen zu verwenden, und spielen das Spiel der "S-Ausdrücke", um eine Benutzeroberfläche zu erstellen:

js: ["div", "hallo welt"]
html: 
hallo welt
js: ["div # foo.warning.blink", "grüß dich!"]
html: 
grüß dich!
js: ["div", {id: "foo", class: "warning blink"}, "howdy!"]
html: 
grüß dich!

Laut einem Tweet meines Freundes Jack Rusher (danke für die Korrektur!) War es Phil Wadler von der Universität Edinburgh, der 1999 in Lisp Pionierarbeit geleistet hat, aber meine erste Begegnung war James Reeves 'Clojure-Bibliothek hiccup (2009), die Später wurde auch die Art und Weise beeinflusst, wie Reakt-Komponenten in Reagent (und anderen) definiert werden können. Ich bin beiden Projekten zutiefst dankbar, da sie mir geholfen haben, meine Sicht auf die Erstellung von Benutzeroberflächen vollständig zu ändern.

[“Tag”, {attribs} ?, body, [“tag”, {attribs}?….]…]

In dieser Konvention definieren Vanilla JS-Arrays Elemente / Komponenten. Der erste Wert wird als Element-Tag (mit etwas Unterstützung für Emmet) und ein optionales JS-Objekt als zweiter Wert zum Definieren beliebiger Attribute verwendet. Alles, was danach kommt, wird als der Körper / die Kinder des Elements betrachtet.

Das Schöne an diesem Ansatz ist nicht nur seine wahre Einfachheit und sein minimaler Charakter:

  1. Viel wichtiger ist, dass wir die Komponente jetzt in muttersprachlichen Konstrukten ausgedrückt haben und die Fähigkeit erhalten haben, diese Komponenten mit dem vollen Arsenal, das unsere Sprache zu bieten hat, zu generieren, zu transformieren und im Allgemeinen zu handhaben.
  2. Da es sich bei der Komponente um reine Daten handelt, kann sie nicht nur für Browserzwecke in eine beliebige Form umgewandelt werden. Das Schreiben von Serialisierern / Transformern für diese einfache Konvention ist trivial.

Wenn Sie diese Schönheiten als Funktionen verpacken, können Sie schnell eine Standardbibliothek mit benannten, wiederverwendbaren und zusammensetzbaren Komponentenfunktionen erstellen, um komplexe Benutzeroberflächen zu erstellen. Während des Erstellens erhalten wir Autovervollständigung, Standardparameter, Dokumentzeichenfolgen und (in TypeScript / Flow) die Möglichkeit, unsere gesamte Benutzeroberfläche stark zu tippen. Win-Win!

/ **
 * @param href link target
 * @param body link body
 * /
const link = (href, body) => ["a", {href}, body];
/ **
 * @param src Bild-URL
 * @param alt (optional)
 * /
const img = (src, alt = "no desc") => ["img", {src, alt}];
link ("http://thi.ng/hiccup-dom", "hiccup-dom");
link ("http://thi.ng/hiccup-dom", img ("foo.png"));

Wenn wir Wertesequenzen transformieren müssen, können wir die Standardsprachmerkmale verwenden und mithilfe von Closures und funktionaler Komposition anpassbare Verhaltensweisen erzeugen:

const li = (body) => ["li", body];
const list = (type) => (items, tx = li) => [type, ... items.map (tx)];
// verschiedene Listentypen erstellen
const ol = Liste ("ol");
const ul = list ("ul");
ol (["Alice", "Bob", "Charlie"]);
// ['ul', ['li', 'alice'], ['li', 'bob'], ['li', 'charlie']]
// Benutzerdefinierte Listenelementfunktion verwenden
ul (["alice.jpg", "bob.png", "charlie.gif"], (src) => li (img (src)));
// ['ul',
// ['li', ['img', {src: "alice.jpg"}]],
// ['li', ['img', {src: "bob.png"}]],
// ['li', ['img', {src: "charlie.gif"}]]]

Bisher sind unsere Komponenten nur statisch. Fügen wir jedoch einen lokalen Status hinzu (dies bedeutet jedoch nicht, dass dies unbedingt erforderlich ist!), Indem Sie Verschlüsse verwenden.

Das Ergebnis sehen Sie hier.

// statische Komponente mit Parameter
const greeter = (name) => ["h1.title", "hallo", name];
// Komponente mit lokalem Status
// beachte auch, wie diese Funktion eine andere zurückgibt
// dazu später mehr ...
const counter = () => {
  sei i = 0;
  return () =>
    ["button", {onclick: () => (i ++)}, "clicks:" + i];
};
// Root-Komponente ist nur ein statisches Array
const app = ["div # app", Begrüßer ("Welt"), Zähler (), Zähler ()];

Sie haben sich vielleicht gefragt, wie der obige Code möglicherweise eine tatsächlich funktionierende HTML-Version hervorgebracht hat. Hier ist deine Antwort ...

Wir stellen vor: thi.ng/hdom

Offensichtlich fehlt etwas zwischen der Konstruktion unseres DOM als verschachtelte Arrays und der Darstellung auf dem Bildschirm. Um weitere 1000 Wörter aus diesem Artikel zu speichern, sehen Sie hier ein Diagramm:

Die Bibliothek thi.ng/hdom verwaltet die 3 Hauptverarbeitungsschritte unterhalb der Falte. Kein Wunder, dass dies React sehr ähnlich sieht. Sobald hdom gestartet ist, wird normalerweise eine Update-Schleife mit (normalerweise) 60 fps ausgeführt, die wiederum unser Root-Komponenten-Array oder unsere Root-Komponenten-Funktion rekursiv ausführt und dann das echte DOM nur dann aktualisiert, wenn und wo es unbedingt benötigt wird. Alle Funktionen, die in ein Komponentenarray eingebettet sind, werden als Teil des Baumnormalisierungsschritts aufgerufen und ihr Ergebnis als Komponente verwendet. Der Zähler aus dem vorherigen Beispiel ist eine Demonstration dieser "faulen Ausführung". Alternativ kann man Komponentenobjekte mit Lebenszyklusmethoden definieren (d. H. Init (), render (), release ()), um lokale Setup- / Teardown-Aufgaben für Komponenten auszuführen.

Diese Bibliothek soll jedoch nicht mit React vergleichbar sein, da sie einen engeren Bereich aufweist und auch viel leichter ist. Das vom Benutzer bereitgestellte Array ist das virtuelle DOM. Es gibt kein virtuelles Ereignissystem. Derzeit gibt es nur einen Teil der Methoden für den Komponentenlebenszyklus (und meiner Erfahrung nach werden sie bisher nur gelegentlich benötigt).

Ein kurzer Überblick über die Vorteile der GitHub-Readme:

  • Nutzen Sie die volle Ausdruckskraft von ES6 / TypeScript, um Komponenten zu definieren, zu kommentieren und zu dokumentieren
  • Saubere, funktionale Komponentenzusammensetzung und Wiederverwendung
  • Keine Meinung über die Behandlung des App-Status und / oder den Ereignisfluss
  • Keine Vorverarbeitungs- / Vorkompilierungsschritte
  • Keine Schritte zum Parsen / Interpolieren von Zeichenfolgen
  • Weniger ausführlich als HTML, was zu kleineren Dateigrößen führt
  • Statische Komponenten können als JSON verteilt werden (oder dynamisch zusammengestellte Komponenten basierend auf JSON-Daten)
  • Unterstützt SVG, beliebige Elemente, Attribute, Ereignisse
  • CSS-Konvertierung von JS-Objekten
  • Geeignet für serverseitiges Rendern (indem dieselbe Datenstruktur an thi.ng/hiccup’s serialize () übergeben wird)
  • Ziemlich schnell (siehe Benchmark-Beispiel unten)
  • Nur ~ 10 KB wurden minimiert

Obwohl ich dieses Projekt Anfang 2016 gestartet habe, habe ich in den Ferien erst vor kurzem mehr Zeit gefunden, um es für den öffentlichen Konsum vorzubereiten. Ich bin auf der Suche nach Feedback und Beiträgen anderer interessierter, gleichgesinnter Benutzer, um die künstliche Komplexität von derzeit mehr zu vermeiden beliebte Ansätze.

Zu Beginn enthält das übergeordnete Umbrella-Mono-Repo einige kleine, leicht verdauliche, kommentierte Beispiele, von denen einige hdom mit anderen verwandten Bibliotheken der thi.ng-Sammlung kombinieren und Folgendes behandeln:

  • App-Status (thi.ng/atom)
  • Datentransformation über Wandler (thi.ng/transducers)
  • reaktive ströme (über wandler) (thi.ng/rstream, thi.ng/rstream-log)

Beispiele:

  • Aufgabenliste mit Undo / Redo-Funktion (Quelle)
  • Komponentengenerierung aus JSON (mit Echtzeiteditor / Vorschau, Quelle)
  • SVG Partikelsystem (Quelle)
  • Stresstest (Quelle)

Noch ein kurzer Hinweis zum Stresstest: Auf einem MBP 2016 mit einer Konfiguration von 192 Zellen läuft dieser noch mit ~ 58-60fps. In dieser Konfiguration löst jede Zelle 4 DOM-Aktualisierungen für jeden einzelnen Frame aus, sodass insgesamt 768 DOM-Mutationen vorhanden sind. Es ist aus einem bestimmten Grund ein Stresstest. Wenn Sie jedoch eine schlechte Leistung erzielen, bedenken Sie bitte, dass dies in einer Standardbenutzeroberfläche höchst unwahrscheinlich ist. Btw. Hier sind einige weitere FPS-Berichte, die über Twitter gesammelt wurden.

Trotzdem benutze ich diese Bibliothek seit einem Jahr für ein umfangreiches Computerdesign-Tool (40KLOC), um zu beweisen, dass sie sich gut für IRL-Anwendungen mit komplexen Benutzeroberflächen eignet.

Ausblick

Da dies ziemlich lange dauert, werden wir uns in einem Folgebeitrag eingehender mit Komponentenfunktionen, Lebenszyklus-Hooks, verfügbaren Status- und Ereignisbehandlungsoptionen befassen und lernen, wie große Duplikationen von manueller Daten- und Komponententransformationsarbeit mithilfe von wiederverwendbarem Material beseitigt werden können Wandler…

Wenn Sie an diesem Projekt interessiert sind, melden Sie sich bitte!

Vielen Dank!