Sådan bruges UI i 2018

En rent funktionel tilgang til UI-sammensætning med ES6 / TypeScript

Opdatering 2019: Se et dybere kig på dette og relaterede projekter i denne igangværende blogserie: Af paraplyer, transducere, reaktive strømme og svampe ...

Bekvemmeligheden ved at omdanne navneord til verb er et næsten uovertruffen træk i det engelske sprog, og det er et centralt punkt i det, der følger: Idéen om at omdanne sammensætningen og kontinuerlig (re) oprettelse af en brugergrænseflade til et verb, en handling, en proces , en funktion.

Blandt UI- og UX-entusiaster synes der aldrig at være en mangel på "næste generations rammer", der lover mere effektivitet, enkelhed og lykke i alle dine komponent- og templeringsbehov, så vær ikke overrasket over at læse om endnu en af ​​dem . Vær venligst heller ikke for hurtig med din browser-tilbage-knap - du fortryder det måske senere ... :)

Apropos templatering (i det mindste hvordan det er gjort 95% af tiden): Det er faktisk det første punkt, jeg gerne vil behandle og alvorligt stille spørgsmålstegn ved, hvorfor langt de fleste designere og udviklere stadig mener, at dette er en god idé ™. Rammer som React, Angular, Vue osv. Har overtaget vores lille verden med storm, og så meget som jeg bifalder de mange nyskabelser, de har bragt til bordet, og så meget som de adskiller sig internt, omfavner de alle HTML på den mest bogstavelige måde .

Ved første øjekast er dette selvfølgelig helt naturligt. HTML er den eneste måde at få en browser til at vise os, hvad vi vil bygge, og jeg har stadig ikke nogen konflikt med det efter 23 år at have arbejdet med det. Men da det er et markeringssprog, spiller det ikke rigtig godt (heller ikke var det nogensinde ment) med den anden standardingrediens, der blev brugt til at opbygge moderne brugergrænseflader - JavaScript. Så den (åbenlyse) løsning, som de store spillere alle har besluttet sig for, var at trylle frem nye filformater, så frontend-udviklere kunne drysse uddrag af HTML-ish-udseende markering over deres kildefiler og derefter igen kræve en imponerende række værktøjer, ingeniørarbejde, dokumentation, uddannelse, projektspecifikke komponentbiblioteker, redaktørstøtteprojekter, parsers, compilers, kildekortgeneratorer, stilladshjælpere, hver med uendelige afhængigheder og i alt millioner af årsårsindsats. Og det hele for magisk at omdanne dette frankenske ægteskab med reageret, vinklet, vue-markeret, markeret JavaScript tilbage til ... JavaScript. Og alt det, fordi vi tilsyneladende ikke kan opgive at bruge HTML til at definere vores UI'er. Og alt det, fordi vores rammer bruger en lidt anden tilgang end den “anden” og derfor kræver en lignende dobbeltarbejde. Og alt dette, selvom JavaScript er det mest populære og mest anvendte programmeringssprog i de sidste 10 år, kan vi stadig ikke bringe os selv til at give op på den overordnede stadig gamle idé om templeringsmotorer i PHP-æraen og ikke se vores UI'er til hvad de virkelig er:

I CS tale: Afledte synspunkter på rene data

Alvorligt, dette er ikke noget nyt, og det er essensen af ​​MV * designmønster smagene næsten alle moderne UI-rammer er baseret på. Jeg tror dog, at vi et eller andet sted langs ”V” (iew) dele af disse mønstre kollektivt tog den forkerte drejning (mere som savnede det i vores spænding over en lysere fremtid) og glemte, at data bare var data, hvilket betyder i en Turing- komplet sprog som JavaScript, skal det være let muligt at oprette disse afledte visninger uden at skulle ty til HTML magiske injektionspiller. Ikke bare det, men også måske gøre så meget mere elegant og magtfuldt, blot ved at bruge værtssproget i det fulde omfang. Husk også, at ingen af ​​de nævnte rammer faktisk bekymrer sig om HTML som et runtime-format. Hver enkelt manipulerer browser-DOM via JavaScript-kommandoer, kommandoer, der blev forudkompileret fra deres HTML-ish-skabelonuddrag. Derfor findes tilstedeværelsen af ​​HTML-syntaks næsten rent til oprettelse og kodegenereringsformål og er der for en opfattet bekvemmelighed og lavvandede overgangskurver fra tidligere tilgange. MVC handlede om adskillelse af bekymringer. Gang of Four. Enterprise mønstre til Dummies. Adskillelse af bekymringer inden for softwaredesign burde ikke nødvendigvis betyde adskillelse af teknologier, som det er tilfældet for os nu. Hvad hvis vi giver HTML den samme behandling eller rolle, som blev foreslået til JavaScript for et stykke tid tilbage:

HTML er (bare) et UI-samlingssprog

JavaScript er kommet langt for at blive, hvad det er i dag, og mange af de nyere tilføjelser til sproget har (annonce) klædt det største åbne sår og reduceret det til et rent, lejlighedsvis ømt sted, dvs. arbejde med data. Og dette er tilfældet både med hensyn til datastrukturer og dataflyt. ES6 spredt operatør, iteratorer, generatorer, kort, sæt, skabelon bogstaver - i forbindelse med denne artikel er kun nogle af dem relevante (antydning: det er ikke skabelonlitterære!), Og jeg har ofte undret mig over, hvorfor den følgende fremgangsmåde ikke er blevet mere almindelige. Men først nogle mere kendte teknikker:

Brand-identitetsgenerator til Leeds College of Music (2012–2013), bygget i Clojure, OpenGL & OpenCL. Hele brugergrænsefladen er konstrueret ud fra et enkelt, dybt indlejret Clojure-hashmap, der fuldstændigt beskriver / begrænser forskellige konfigurationsindstillinger, layout, værdibegrænsninger, begivenheder fyret osv.

Ovenstående eksempel for Leeds College of Music var ingen webapplikation og er på ingen måde særlig i historisk forstand, men det var første gang, jeg byggede et UI helt baseret på rene data, kun ved hjælp af et enkelt, indlejret hashmap til at udtrykke både konfiguration og aktuelle tilstande for hver enkelt komponent. I stedet for DOM-elementer blev disse UI-komponenter samlet ved en enkelt rekursiv transformationsfunktion, som kaldte andre funktioner for hver trægren til at producere forskellige OpenGL VBO'er. I stedet for CSS blev shaders brugt til at udføre layout og tema. Idéen er dog den samme for webapps:

I matematik tale: ui = f (s)

Din brugergrænseflade er resultatet af gentagne gange at anvende en transformerende funktion f over den kontinuerlige ændrede interne tilstand i din app.

Kerneidéen i alle komponentrammer er at abstrahere den bogstavelige brug af de faktiske UI-genereringsinstruktioner (f.eks. HTML-markup eller OpenGL-opkald) for at give os mulighed for at udtrykke vores UI'er i form af pakker med funktionalitet. Som tidligere påpeget har den hidtil almindelige tilgang været at skjule disse uddrag af “lavt niveau” HTML i specielle kildefiler, pakket ind på en eller anden måde under forbehandlingen. Men hvad nu hvis vi fuldstændigt omgår HTML og kan udtrykke vores:

UI-komponenter, der kun bruger vanilje JavaScript?

Den funktionelle tilgang til UI-sammensætning er nøjagtigt hvad React & co. faktisk allerede gør internt, men vi er stadig fast ved at udtrykke markupen for vores komponenter i et format, der stort set er en blackbox for JS og tvinger os til at hoppe gennem alle disse forskellige bøjler. Mine to vigtigste erfaringer fra de syv år, der er brugt med Clojure / ClojureScript indtil videre, har været:

  1. Den bevidste anerkendelse af, at "enkelhed" i softwarekredse i vid udstrækning bare tolkes og aktivt fejres som primært serveringsfaciliteter og eksisterende vaner. Det er systemisk, men ikke gennemgribende. For eksempel. Rich Hickeys rolige klarhed i tankerne og evnen til at tage et skridt tilbage for at overveje herskende designvalg har ført til, at mange andre i og uden for Clojure-samfundet har stillet spørgsmålstegn ved status quo for de mere mainstream sproglejre, og vi har set flere centrale innovationer fra Clojure-samfundet spild hurtigt og bliver transplanteret på andre sprog.
  2. Clojure er en dialekt af Lisp, et sprog med ofte ingen clearcut-adskillelse mellem data og kode, et sprog, hvor selv kildekoden bogstaveligt er kodet og behandlet som rekursive datastrukturer i sig selv, har jeg lært at værdsætte S-udtryk (i alle deres former ) som både den ultimative og mest enkle tilgang til at kode træbaserede data, f.eks ikke kun UI-beskrivelser.

Nogle argumenterer for, at JavaScript og HTML også hører til Lisp-slægtstræet (omend mere som fjernede fætre), men det er ubestrideligt, at begge begrebsmæssigt låner delvis fra Lisps S-udtryk. Da vi i JS kun kan oprette arrays eller objekter på denne bogstavelige måde, lad os begrænse os til kun at bruge disse to syntaktiske former og spille spillet med "S-udtryk" til at opbygge en UI:

js: ["div", "hej verden"]
html: 
hej verden
js: ["div # foo.warning.blink", "howdy!"]
html: 
js: ["div", {id: "foo", klasse: "advarselsblink"}, "howdy!"]
html: 

Ifølge en tweet af min ven Jack Rusher (tak for korrektionen!) Var det Phil Wadler på Edinburgh University, der pionerer i denne tilgang i Lisp tilbage i 1999, men mit første møde var James Reeves 'hik (2009) Clojure bibliotek, som senere påvirkede også måden, hvordan React-komponenter kan defineres i Reagent (og andre). Jeg er dybt, dybt taknemmelig for begge projekter, da de har hjulpet mig med at ændre min mening fuldstændigt om, hvordan man bygger UI'er.

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

I denne konvention definerer vanilla JS-arrays elementer / komponenter. Den første værdi bruges som elementtaggen (med en vis understøttelse af Emmet) og et valgfrit JS-objekt, da den anden værdi bruges til at definere vilkårlige attributter. Alt efter betragtes som elementets krop / børn.

Skønheden ved denne tilgang er ikke kun dens ægte enkelhed og minimale karakter:

  1. Meget vigtigere er, at vi nu har udtrykt komponenten i modersprogskonstruktioner og har opnået evnen til at generere, transformere og generelt håndtere disse komponenter med det fulde arsenal, som vores sprog har at tilbyde.
  2. Da komponenten er almindelige data, kan den omdannes til enhver form, ikke kun til browserformål. At skrive serialisatorer / transformere til denne enkle konvention er trivielt.

Indpak disse skønheder som funktioner, og vi kan hurtigt opbygge et standardbibliotek med navngivne, genanvendelige og komponerbare komponentfunktioner til at opbygge komplekse brugergrænseflader. Under forfatter giver dette os autofuldførelse, standardparameter, doc-strenge og (i TypeScript / Flow) med mulighed for stærkt at indtaste hele vores brugergrænseflade. Win-win!

/ **
 * @param href linkmål
 * @ Param body link body
 * /
const link = (href, body) => ["a", {href}, body];
/ **
 * @param src image URL
 * @param alt (valgfrit)
 * /
const img = (src, alt = "no desc") => ["img", {src, alt}];
link ("http://thi.ng/hiccup-dom", "hicdom-dom");
link ("http://thi.ng/hiccup-dom", img ("foo.png"));

Hvis vi har brug for at transformere sekvenser af værdier, kan vi bruge standardsprogfunktionerne og bruge lukninger og funktionel sammensætning til at producere tilpassbare adfærd:

const li = (body) => ["li", body];
const list = (type) => (varer, tx = li) => [type, ... items.map (tx)];
// oprette forskellige listetyper
const ol = liste ("ol");
const ul = liste ("ul");
ol (["alice", "bob", "charlie"]);
// ['ul', ['li', 'alice'], ['li', 'bob'], ['li', 'charlie']]
// Brug brugerdefineret listepunktfunktion
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"}]]]

Indtil videre er vores komponenter bare statiske, men lad os tilføje en vis lokal tilstand (ikke at antyde, at dette er noget, man skal gøre!) Ved at bruge lukninger.

Du kan se resultatet her.

// statisk komponent m / param
const greeter = (navn) => ["h1.title", "hej", navn];
// komponent m / lokal tilstand
// Bemærk også, hvordan denne funktion returnerer en anden
// mere om dette senere ...
const counter = () => {
  lad i = 0;
  retur () =>
    ["knap", {onclick: () => (i ++)}, "klikker:" + i];
};
// rodkomponent er bare en statisk matrix
const app = ["div # app", greeter ("world"), counter (), counter ()];

Du har måske spekuleret på, hvordan ovenstående kode muligvis har produceret en faktisk fungerende HTML-version. Her er dit svar ...

Vi præsenterer thi.ng/hdom

Der mangler naturligvis noget mellem konstruktionen af ​​vores DOM som indlejrede arrays og at få vist noget på skærmen. For at gemme yderligere 1000 ord fra denne artikel er her et diagram:

Thi.ng/hdom-biblioteket håndterer de 3 store behandlingstrin under folden. Det er ingen overraskelse, at dette ligner meget React. Når først startet, kører hdom normalt en opdateringssløjfe ved (normalt) 60fps, som igen udfører rekursivt vores rodkomponent-array eller -funktion og derefter kun opdaterer den rigtige DOM, når & hvor det er absolut nødvendigt. Eventuelle funktioner, der er integreret i en komponentgruppe, kaldes som en del af trænormaliseringstrinnet, og deres resultat anvendte en komponent. Tælleren fra det foregående eksempel er en demonstration af denne 'doven henrettelse'. Alternativt kan man definere komponentobjekter med livscyklusmetoder (dvs. init (), render (), release ()) for at køre komponenters lokale opsætning / nedrivningsopgaver.

Dette bibliotek er dog ikke beregnet til at kunne sammenlignes med React, da det har et mere snævert omfang og også er meget mere let. Den bruger, der leveres, er den virtuelle DOM. Der er ikke noget virtuelt begivenhedssystem. Der er i øjeblikket kun en undergruppe af komponentlivscyklusmetoder (og efter min erfaring er de hidtil kun nødvendige nogle gange).

En kort oversigt over fordelene ved GitHub readme:

  • Brug ES6 / TypeScript's fulde udtryksevne til at definere, annotere og dokumentere komponenter
  • Ren, funktionel komponentsammensætning og genbrug
  • Ikke-bedømt om apptilstandshåndtering og / eller begivenhedsstrøm
  • Ingen forbehandlings- / forudkompileringstrin
  • Ingen streng parsing / interpolationstrin
  • Mindre ordbog end HTML, hvilket resulterer i mindre filstørrelser
  • Statiske komponenter kan distribueres som JSON (eller dynamisk komponere komponenter, baseret på JSON-data)
  • Understøtter SVG, vilkårlige elementer, attributter, begivenheder
  • CSS-konvertering fra JS-objekter
  • Velegnet til serversides gengivelse (ved at videregive den samme datastruktur til thi.ng/hiccup's serialisering ())
  • Ganske hurtigt (se benchmarkeksempel nedenfor)
  • Kun ~ 10 KB minificeret

Selvom jeg startede dette projekt i begyndelsen af ​​2016, har jeg kun for nylig fundet mere tid i løbet af ferien for at gøre dette klar til offentligt forbrug og leder efter feedback og bidrag fra andre interesserede, ligesom brugere, der sigter mod at undgå den kunstige kompleksitet i øjeblikket mere populære tilgange.

For at komme i gang er der flere små, fordøjelige, kommenterede eksempler i den overordnede paraply-mono-repo, hvoraf nogle kombinerer hdom med andre relaterede biblioteker i thi.ng-samlingen med adresse:

  • app-tilstand (thi.ng/atom)
  • datatransformation via transducere (thi.ng/transducers)
  • reaktive streams (via transducere) (thi.ng/rstream, thi.ng/rstream-log)

Eksempler:

  • Todo-liste med fortryd / forny funktion (kilde)
  • Komponentgenerering fra JSON (w / realtime editor / preview, kilde)
  • SVG-partikelsystem (kilde)
  • Stresstest (kilde)

En anden kort note om stresstesten: På en MBP 2016 med en konfiguration på 192 celler kører dette stadig ved ~ 58-60fps. I denne konfiguration udløser hver celle 4 DOM opdateringer hver enkelt ramme, så en samlet sum af 768 DOM-mutationer. Det er en stresstest af en grund, men hvis du får dårlige resultater, skal du huske, at dette er en meget usandsynlig ting, der vil ske i et standard UI. Btw. Her er nogle flere FPS-rapporter indsamlet via Twitter.

Når det er sagt alt dette, har jeg brugt dette bibliotek til et stort beregningsdesignværktøj på arbejdspladsen (40KLOC) det sidste år, så har bevis for, at det skalerer godt til IRL-applikationer med komplekse brugergrænseflader ...

Outlook

Da dette bliver temmelig lang, vil vi i et opfølgningsindlæg tage et dybere kig på komponentfunktioner, livscykluskroge, tilgængelige muligheder for tilstand og begivenhedshåndtering og lære at udslette stor dobbeltarbejde af manuelle data og komponenttransformationsarbejde ved hjælp af genanvendelig transducere ...

Igen, hvis du er interesseret i dette projekt, skal du kontakte!

Tak!