Micro-frontends og web-komponenter skapte kaos

Designsystemet til Statnett skapte konflikter, men Mathias Eek fant løsningen i syntaks-treet. – Kanskje svaret på ditt neste store problem ligger skjult der, også.

Mathias Eek er frontend-utvikler og konsulent i Bouvet Innlandet, og fikk en ordentlig nøtt da han skulle jobbe med Statnetts designsystem.
Publisert

✍ leserinnlegg

Dette er et leserinnlegg fra en ekstern skribent, som betyr at innholdet ikke nødvendigvis speiler kode24s meninger. Vil du også bidra? Send oss en epost på [email protected], eller les mer her!

Som konsulent kan man havne i prosjekter der teknologivalgene er tatt lenge før man kommer inn, og der det heller ikke er aktuelt å endre dem. 

Noen ganger kan det i tillegg vise seg at disse teknologiene kanskje ikke spiller helt på lag med hverandre.

Jeg ble for en tid tilbake hyret inn for å jobbe på Statnetts designsystem for interne fagapplikasjoner. Systemet hadde allerede eksistert i flere år, er designet med Stencil (et verktøy for å bygge web-komponenter), og var allerede godt integrert i et stort prosjekt som brukte microfrontends.

Om noen allerede lukter ugler i mosen, så gjorde i alle fall ikke jeg det på dette tidspunktet.

TL;DR: Web-komponenter registreres globalt i nettleseren, og kun én versjon av én komponent kan eksistere av gangen, noe som skaper versjonskonflikter i et microfrontend-miljø.

Litt bakgrunn

Microfrontends er på mange måter frontendens svar på microservices, der flere selvstendige applikasjoner (remotes) settes sammen i en rammeapplikasjon (shellet) og fremstår som én helhet for sluttbruker. 

Hovedformålet med dette er å begrense avhengigheter på tvers og isolere de ulike remotene fra hverandre.

Da jeg var ganske fersk på prosjektet, kom en frontendarkitekt bort til meg og lurte på om jeg ikke kunne løse dette problemet med at ulike versjoner av komponenter fra designsystemet "blødde over" mellom remotene. 

Min respons var: «Jo, det skal vi sikkert få til».

Omtrent ett år etter, og minst to-tre fulle arbeidsmånder sammenlagt, var vi endelig i mål.

Veien dit har vært lang, men veldig spennende og lærerikt.

Problem:

Web components registreres i et globalt register i nettleseren. 

Det fungerer fint i et enkelt prosjekt med én node_modules-mappe, men i microfrontends blir det et mareritt:

  • Bare én versjon per komponentnavn kan være aktiv.
  • Remotes overstyrer hverandre.
  • Brukeren får feil versjon avhengig av lastesekvens.
  • Hele prinsippet om isolerte microfrontends brytes.

Eksempel: Remote A har en my-button med blå bakgrunn, fra versjon 1.0 av designsystemet. Remote B har den samme komponenten, men i versjon 2.3 ble denne grønn. Om brukeren starter på Remote A, og bytter til Remote B underveis, får den likevel my-button med blå bakgrunn. Etter en refresh av siden, så får Remote B være først ut, og fargen er nå grønn. Du kan se for deg kaoset når dette skalerer til titalls komponenter.

Under følger en video der dette blir demonstrert: 

  • Her har vi tre apps i et microfrontendmiljø, der App 1 og 3 bruker en versjon av biblioteket, mens App 2 bruker en annen. 
  • Her kan vi se at den første siden vi går til, får lov til å registrere sine komponenter, og de påtvinges de neste sidene helt til man refresher. 

Med andre ord ser vi at hvilke versjoner av komponentene de ulike appene får, er svært uforutsigbart:

Midlertidig løsning:

  • Shellet får ansvar for å forhåndsregistrere alle komponenter fra designsystemet.
  • Alle remotes blir dermed tvunget til å bruke samme versjon som shellet.

Konsekvenser:

  • Krever tett koordinering mellom designsystemet og alle remotes.
  • Ingen remotes kan bruke nye funksjoner før shellet er oppdatert.
  • Oppdatering av shellet krever omfattende testing og tar flere uker.
  • En feil i én remote kan føre til rollback for alle.

Vi satte opp nightly builds med automatiske tester mot 10–12 remotes for å få mer forutsigbarhet, men møtte likevel utfordringer: flaky tester, gamle remotes med inkompatible versjoner, og vanskeligheter med å peke ut om feil skyldtes designsystemet eller remoten selv.

Et kritisk øyeblikk kom da en ytelsesbug i produksjon krevde rask fiks. På grunn av shell-strategien tok det to uker før en offisiell fiks var ute.

Med andre ord, et organisatorisk mareritt.

Typescript Abstract Syntax Tree Transformers

Den ideele løsningen var enkel på papiret: Hver remote burde ha unike komponentnavn

  • my-button-remote-a
  • my-button-remote-b

Da ville konfliktene forsvunnet. Men det fantes ingen ferdig løsning for dette.

Enter Typescript Abstract Syntax Tree Transformers:

For at datamaskinen skal forstå koden vi skriver, oversettes den til et AST (Abstract Syntax Tree) – et hierarki der alt i fila blir representert som noder: rot → funksjoner → statements → strenger/variabler osv.

Du kan selv se hvordan ASTen for din kode ser ut ved å lime den inn her: TypeScript AST Viewer

Under ser du et eksempel på hvordan litt kode blir som et AST (til høyre), og hvordan man konstruerer koden med AST Transformers

Typescript tilbyr transformers, som lar deg manipulere AST-et, og Stencil bruker allerede dette internt når det genererer web components, og det var slik jeg snublet over denne muligheten.

Ved å lage egne AST-transformers kunne vi:

  • Finne nodene som inneholder tag-names (komponentnavn).
  • Patche inn variabler som bestemmer unike navn per remote, som valideres runtime.
  • Gjøre dette på kompileringsnivå – helt usynlig for utviklerne.

Vi pakket dette som en Stencil-plugin (custom output target) som kjører etter build, og patcher den outputtede filene. Dermed slapp vi å endre designsystemet selv.

I tillegg kombinerte vi dette med PostCSS og csswhat for å patch’e CSS og querySelectors.

Resultatet: 

~200 linjer kode løste hele versjoneringsproblematikken.

Før transformering:


customElements.define('my-button', MyButton);
document.querySelector('my-button');
h('my-button');
const myCSS = `
  my-button {
    /* styles */
  }
`;

Etter transformering (der suffix leses av runtime):


import suffix from "../custom-suffix.json";

customElements.define('my-button' + suffix, MyButton);
document.querySelector(`my-button${suffix}`);
h('my-button' + suffix);
const myCSS = `
  my-button${suffix} {
    /* styles */
  }
`;

Koden ligger åpen her, og er også publisert på npm: stencil-custom-suffix-output-target

Foreløpig støtter bare løsningen Angular microfrontends, men både vi og teamet internt i Stencil håper at vi kan få inn støtte for React etter hvert.

Her ser vi en demo av den samme applikasjonen som tidligere, men nå med versjonering via løsningen over:

Demoen kan selv testes her, med en egen knapp for å skru av og på versjonering: angular-microfrontend-demo

Læring og muligheter

AST Transformers kan brukes til så mangt. 

I vår løsning legger vi til en runtime-validert variabel, men det kunne like så godt vært å legge til logging i hver eneste funksjon i hele prosjektet, eller konvertering fra et språk til et annet, som jeg vet at Typescript-gjengen har gjort under utviklingen av Typescript Go.

AST Transformers er en mye mer fleksibel og lesbar måte å patche filer på enn f.eks. å bruke regex.

AST-er kan også brukes til andre formål en patching, som for eksempel å lage egne linting rules. Jeg har gjort en test i vårt eget prosjekt der vi itererer igjennom treet og finner alle forekomster av en funksjon fra en dependency, og lager en egen linting-regel som tilsier at når denne funksjonen brukes, så skal alltid et spesifikt argument settes.

Med andre ord er AST Transformers også nøkkelen til å skrive sitt eget rammeverk (vi trenger flere, gjør vi ikke?) eller egne byggeverktøy.

Mulighetene er mange.

Oppsummering

Denne reisen ned i Typescripts indre har vært enormt lærerik. 

For meg har den vist at løsningen på store organisatoriske og tekniske problemer noen ganger ligger mye lavere enn man tror – helt nede i syntaxtreet.

Håper dette kan inspirere flere til å utforske AST-transformers (eller tilsvarende for andre språk). 

Kanskje svaret på ditt neste store problem ligger skjult der også.

Powered by Labrador CMS