Det er mange store stilaser å velge blant for din frontendarkitektur, men trenger du all leamikken? I denne bloggposten forteller jeg om en enkel arkitektur som jeg har hatt mye glede av.
Her er de viktigste poengene:
- All data er samlet på én plass.
- Dataflyten er forutsigbar og ensrettet.
- UI-komponentene får alle dataene sine tilsendt.
- UI-komponentene er uavhenging av domene og kontekst.
- Handlinger kommuniseres fra UI-komponentene via data.
- De bevegelige delene er samlet på toppnivå i en main-funksjon.
¶Kort fortalt
App-en sparkes i gang av en main
-metode, som oppretter et sted å samle dataene.
Disse hentes, og sendes til en prepare
-funksjon som gjør domenedata om til
UI-data. UI-dataene rendres ved hjelp av generiske komponenter.
¶Dataflyt
Dataene dine kommer til klienten på ett eller annet vis. Jeg skal ikke begi meg inn på hvordan i denne bloggposten, annet enn å si at det ikke er komponentene som henter dem selv. Kanskje henter du dem med GraphQL, eller WebSockets, eller noen GET-requests - så lenge det gjøres sentralt, skal jeg ikke klage.
Når du har datene, så samles de på toppnivå på en plass som er definert av main-funksjonen. Det kan være i en database, i et atom, eller til nøds i et JS-objekt.
Uansett trenger du å vite når dataene har endret seg, slik at en oppdatering av UI-et kan sparkes igang.
Når dette skjer kalles en prepare
-funksjon med alle dataene, som gjør
domenedata om til UI-data. Disse UI-dataene sendes til en toppnivå komponent,
som tegner ut UIet med generiske komponenter.
Det er hele dataflyten. Når det kommer endringer til dataene, skjer alt dette om igjen. Virtual DOM-trikset (gjort populært av React) lar oss gjør dette uten store ytelsesproblemer.*
* Ut av boksen for ClojureScript, men store JavaScript-prosjekter må kanskje ty til immutable.js
¶Generiske komponenter
Dette er byggeblokkene våre. De implementerer designet vårt, men kjenner ikke til domenet. De kjenner ikke til konteksten de brukes i. De vet ikke hva slags handlinger som utføres når knapper trykkes på.
Dette gjør komponentene særdeles gjenbrukbare. Når vi går fra en
RegistrationButton
til en PrimaryButton
, så kan den brukes mange steder. Man
får et eget språk for designet, fristilt fra domenespråket.
Men hva da med actions? Skal ikke RegistrationButton
gjøre noe annet enn
SignInButton
? Jo, men hvilke handlinger som skal utføres sendes også inn til
komponenten som data.
Enkelt fortalt:
PrimaryButton({action: ["register-user"]})
Det eneste PrimaryButton
vet er at når den trykkes på, så skal action
puttes
på en event-bus. Denne overvåkes av main-funksjonen, som så gjennomfører handlingen.
Observer hvordan alle pilene strømmer ut fra main
, og én vei. Event bus-en er
grepet som inverterer avhengigheten slik at vi kan kommunisere handlinger uten å
innføre sirkulære avhengigheter.
PSST! Ove stilte et betimelig spørsmål når denne bloggposten ble publisert. Som svar har jeg skrevet litt mer om samspillet mellom generiske UI-komponenter.
¶Fra domenedata til generiske komponenter
Ettersom komponentene ikke snakker domenespråk, så trenger vi en tolk. Det er
prepare
-funksjonen. Den tar domenedataene fra den sentrale datakilden, og gjør
om til håndsydde data for nettopp det UIet vi ser på nå.
Dataene fra prepare
skal i så stor grad som mulig gjenspeile UI-et. Den bygger
en trestruktur som kan sendes rett ned til komponentene.
Dette gjør at selve komponent-koden kan være så godt som fri fra logikk. UI-kode er notorisk vanskelig å teste. Her kan vi koble oss på ett hakk over, og likevel få testet logikken.
¶Til slutt
Dette er en arkitektur jeg har brukt med glede på små og store prosjekter de siste fem årene, men hva får man egentlig?
- En dataflyt som er lett å følge.
- Gjenbrukbare komponenter som implementerer designet.
- Reproduserbart brukergrensesnitt pga én datakilde.
- Fri fra det evige rammeverkkjøret.