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.

MainDomene-datapreparegeneriskekomponenterUI-dataDOM

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.

MainEvent busDomene-datapreparegeneriskekomponenterUI-dataDOMHandlinger som dataovervåker og utfører

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.