Micro frontend: come governare il caos in un mondo di monoliti

13 minutes Leggi
28 Settembre 2021

Negli ultimi anni ha fatto sempre più breccia nello sviluppo frontend il concetto di micro frontend. Ma che cosa sono i micro frontend? Da dove nascono? 

In questo articolo analizzeremo le problematiche che abbiamo affrontato nello sviluppo di Mia‑Platform Console e come l’utilizzo dei micro frontend ci abbia aiutato a governarle. Affronteremo questo viaggio partendo da un mondo più conosciuto e ben consolidato, quello dei microservizi sul backend. Vedremo come i problemi che l’architettura a monolite porta siano simili ai problemi nei quale incorriamo anche sul frontend; e quindi come sia possibile sul frontend “seguire le orme” del lavoro effettuato sul backend con l’architettura a microservizi. Una volta comprese le problematiche e le possibili soluzioni, ripercorreremo i passi che abbiamo svolto in Mia‑Platform nell’utilizzo di questa architettura: le prime prove, i successivi ostacoli, la loro risoluzione.

 

Dal monolite ai microservizi

Un’applicazione web generalmente offre diverse funzionalità che possono essere fornite sulla stessa pagina o avere delle pagine dedicate. Tipicamente un sito web è sviluppato come un’unica codebase e un’unica applicazione per quanto riguarda il frontend.

Questa architettura è già stata affrontata sul backend con i cosiddetti “monoliti”. La serie di problematiche che si portano dietro i progetti sviluppati a monoliti ha portato alla nascita della ormai famosa architettura a microservizi.

I problemi che si affrontano con i monoliti lato frontend sono simili a quelli lato backend.

Vediamo quali sono gli svantaggi dell’utilizzo di un’architettura monolitica:

  • Codebase grande e disomogenea. Ogni sviluppatore ha il suo stile e modo di sviluppare, che rischiano di dare vita a una codebase troppo difficile da mantenere nel momento in cui il numero di sviluppatori cresce. Per quanto riguarda lo stile è possibile mitigarlo tramite analisi statica del codice (come un linter), anche se difficilmente si avrà comunque un codice uniforme. Sarà sempre necessario un forte controllo lato pull request al fine di mantenere una codebase omogenea. Inoltre, un codice sorgente disomogeneo sui pattern di sviluppo può rendere più difficoltosa la lettura e comprensione del codice. Un esempio di disomogeneità può essere ad esempio “funzionale vs oggetti” (analogo al “functional vs class component” in React).
  • Conoscenze della propria codebase. Su una codebase molto grande è probabile che ci siano parti di codice che si conoscono meno. Se il codice non è stato sviluppato seguendo delle buone pratiche al fine di isolare le singole funzionalità, si potrebbe incappare nel problema che per aggiungere una nuova funzionalità sia necessario mettere le mani su del codice su cui si ha poca confidenza.
  • Interdipendenze per i rilasci di feature. Avere tutte le funzionalità in un’unica codebase rende difficoltoso il rilascio di nuove feature appena sono pronte. Infatti, il rilascio di un servizio potrebbe essere bloccato dal fatto che un altro team sta lavorando su un’altra funzionalità, e il servizio non è rilasciabile in quanto instabile. Questo può essere evitato ricorrendo ai Feature Toggle, che permettono di mantenere il codice del vecchio comportamento. Tuttavia,  il loro utilizzo potrebbe essere molto dispendioso o difficoltoso in alcuni casi – come nel caso in cui la funzionalità sotto Feature Toggle abbia particolari side-effect, quali il cambiamento di una configurazione.
  • Scalabilità delle funzionalità. Dal momento che più funzionalità, con eventuali esigenze di performance diverse, convivono sullo stesso servizio, è impossibile scalarle in maniera indipendente. Infatti puoi scegliere di scalare il servizio intero ma non la singola funzionalità.

 

Divide et impera

Queste problematiche hanno portato alla definizione di quella che oggi chiamiamo un’architettura a microservizi.

Il principio cardine di un’architettura a microservizi è quello della singola responsabilità. Si divide il monolite in aree di responsabilità – idealmente un’area si occupa di una sola funzionalità di business – e ad ogni area è associato un microservizio specifico. Un esempio di architettura a microservizi è Mia‑Platform Console che presenta il servizio che si occupa del deploy, quello che si occupa di gestire la configurazione del progetto, quello che si occupa di fornire i dati runtime del proprio namespace, ecc. Ogni servizio si occupa di gestire una singola funzionalità che, insieme con le funzionalità degli altri servizi, compone l’intera applicazione.

Un’architettura di questo tipo permette di isolare architetturalmente le varie funzionalità, di scalarle individualmente in base alle proprie esigenze, e di avere dei microservizi riutilizzabili (ad esempio un microservizio che gestisce il pagamento può essere riutilizzato in diversi contesti business). Inoltre, permette di migliorare le performance, in quanto ogni modulo può essere sviluppato con il linguaggio di programmazione più performante per i compiti che deve svolgere.

 

I micro frontend

L’evoluzione delle applicazioni web degli ultimi anni, con una sempre maggiore complessità lato frontend, ha portato allo sviluppo di un approccio analogo a quello dell’architettura a microservizi. Nascono così i micro frontend. L’idea è quindi di dividere il proprio progetto in più parti, tendenzialmente in base alle funzionalità, ed implementare ognuna di loro in un’applicazione web dedicata. Al fine di mostrare i frontend all’utente in un unico sito web, si ricorre a un frontend “contenitore” che avrà il ruolo di orchestratore della visualizzazione di questi micro frontend.

Vediamo quindi come questo tipo di architettura risolve i problemi di cui abbiamo parlato precedentemente:

  • Codebase grande e disomogenea. Dividendo il progetto in più applicazioni, si avranno diverse codebase, ognuna delle quali sarà più piccola in quanto conterrà solo una parte delle funzionalità del progetto totale. Essendo più piccola e concentrandosi su alcune precise funzionalità, si avranno tendenzialmente anche meno sviluppatori che lavorano sulla singola codebase. Questo porta ad una maggiore semplicità di review del codice e migliore organizzazione della codebase.
  • Conoscenze della codebase. Essendo il singolo servizio più piccolo, e la codebase incentrata su una specifica funzionalità, è più probabile che gli sviluppatori conoscano la quasi totalità del servizio, che si occupa della funzionalità di cui loro sono responsabili. Il team del prodotto può essere diviso infatti in Feature Team, ognuno dei quali si occupa di specifiche funzionalità, ed è quindi responsabile di specifiche code-base. In Mia‑Platform abbiamo ad esempio diversi team che lavorano sul prodotto, ad ognuno dei quali sono assegnate specifiche funzionalità di Mia‑Platform Console: Marketplace, Dashboard, Dev Portal, Fast Data, Flow Manager, etc.
  • Interdipendenze per i rilasci di feature. Siccome funzionalità diverse sono su servizi differenti e indipendenti (ognuno con il proprio versionamento), sarà possibile rilasciare le singole funzionalità quando sono pronte, senza che il rilascio sia impedito dallo sviluppo di un’altra funzionalità. In Mia‑Platform questo ci permette ad esempio di rilasciare un eventuale fix sull’area Deploy mentre sono in corso lavori su altre aree della Console: questo è possibile perché l’area Deploy è un micro frontend a parte. I Feature Toggle possono comunque essere utili per gestire lo sviluppo di alcune feature “cross-area” o di cui si prevede lo sviluppo sul medio-lungo periodo.
  •  

    Il nostro approccio ai micro frontend

    iframe: la prima scelta

    Il primo passo che abbiamo fatto su Mia‑Platform Console verso un’architettura a micro frontend è stato di utilizzare gli iframe, un tag HTML che abbiamo usato al fine di incorporare dentro la console dei frontend con delle nuove funzionalità. Abbiamo fatto in modo di mostrare su uno specifico path un iframe con l’URL su cui è esposto il micro frontend che volevamo renderizzare.

    L’utilizzo di iframe, per quanto veloce da implementare, presenta un aumento della complessità nel momento in cui è necessario gestire la comunicazione, come il passaggio di dati, ad un micro frontend. Infatti, nonostante ogni micro frontend si occupi di una specifica funzionalità, in un contesto reale è spesso necessaria una comunicazione tra parti differenti dell’applicazione, ad esempio se necessiti in una sezione di mostrare informazioni censite da un’altro microfrontend (in un e-commerce potresti voler mostrare nella pagina dettaglio di un articolo che esso è già stato aggiunto al carrello).
    La comunicazione, a seconda di come è stata pensata l’applicazione, può essere gestita tramite un server (esempio: socket o dati persistenti su un database) o interamente lato client. 
    Nel caso di Mia‑Platform Console, che è nata come monolite per quanto riguarda il frontend, la comunicazione tra le differenti aree è sempre stata gestita lato frontend. Per la comunicazione tra micro frontend è stato mantenuto questo approccio.

    La comunicazione può avvenire in due direzioni: il micro frontend comunica al contenitore delle informazioni (ad esempio che è stata effettuata una modifica ad un sistema e che quindi la configurazione della Console è cambiata), oppure la comunicazione avviene dal contenitore al micro frontend nel caso in cui il microfrontend necessita di dati della cui gestione non è responsabile lui (ad esempio una lista di dati memorizzati nel contenitore).
    Nonostante sia comunque possibile effettuare una comunicazione tramite iframe, la complessità di utilizzare questo strumento per la comunicazione tra i micro frontend ci ha spinti a cercare soluzioni alternative.

    Di soluzioni per gestire i micro frontend senza l’utilizzo di iframe ce ne sono varie: Webpack Module Federation, Single SPA, Qiankun e molti altri. 
    Noi abbiamo scelto di usare Qiankun, il quale si basa su Single SPA.

     

    La scelta di utilizzare Qiankun

    Qiankun si occupa di fornire delle funzioni (lifecycle) da usare per montare o smontare i micro frontend in un applicativo contenitore. Queste funzioni verranno poi invocate dalla libreria stessa in base alle esigenze: se il micro frontend deve essere mostrato, allora invocherà la funzione per montarlo, se non deve essere mostrato invocherà quella per smontarlo. La decisione di mostrarlo o meno è determinata da delle regole che devono essere fornite nel momento in cui si fa il censimento della lista dei micro frontend che la libreria deve orchestrare.

    I vari micro frontend a questo punto condividono lo stesso DOM, e quindi la loro comunicazione può essere implementata in maniera semplice tramite l’utilizzo di eventi (tramite Event di JavaScript o librerie come rxjs), o l’implementazione di funzioni personalizzate da iniettare nella window.

     

    Le esigenze di comunicazione tra micro frontend in Mia‑Platform Console

    In Mia‑Platform Console abbiamo avuto la necessità di inserire una comunicazione tra micro frontend nel momento in cui abbiamo introdotto per la prima volta i micro frontend di Fast Data nell’area di Design. 
    Precedentemente usavamo i micro frontend solo a livello di sezioni principali (Deploy, Monitoring, Runtime, ecc), che non condividono alcuno stato applicativo. L’area di Design invece contiene varie sottosezioni (microservizi, endpoints, fast data, …), le quali gestiscono una parte dei dati legati alla configurazione del progetto che l’utente può modificare.
    Nel momento in cui abbiamo aggiunto l’area Fast Data abbiamo avuto la necessità di far sì che al click del bottone di salvataggio della configurazione, anche i suoi dati venissero salvati. Necessitavamo quindi di una comunicazione dal micro frontend Fast Data al contenitore.

    Il primo approccio che abbiamo scelto è stato quello di usare la window del browser, che è condivisa anche con il micro frontend, per condividere un oggetto che fornisce un’interfaccia API comune tra contenitore e micro frontend. Questa API consisteva in una serie di funzioni, come ad esempio una funzione “update” che fa sì che il micro frontend possa inviare la configurazione dei suoi dati aggiornati al contenitore, che si occupa quindi di conservarla e inviarla poi al backend nel momento in cui l’utente preme sul pulsante di salvataggio.

    Successivamente abbiamo incontrato un’esigenza analoga sull’area Flow Manager. Abbiamo scelto di cercare un approccio più standard al fine di gestire la comunicazione senza usare la window. La scelta è ricaduta su rxjs, una libreria che permette di sviluppare applicazioni reactive, cioè che reagiscono al verificarsi di certi eventi, tramite il pattern Observer: vi è un oggetto “osservato”, chiamato Subject e dei soggetti “osservatori”, chiamati Observers, che sono in ascolto dei cambiamenti di stato del Subject. Quando quest’ultimo invoca una funzione di aggiornamento, gli osservatori vengono notificati della modifica ai dati. Nel nostro caso avevamo il micro frontend che si sottoscriveva al contenitore (che ha istanziato l’oggetto osservato). 

    Questa architettura di comunicazione l’abbiamo successivamente implementata anche nel verso opposto. Infatti, per poter gestire la visibilità della selezione dell’ambiente su cui deployare in base alla sezione in cui ci si trova (deploy, history del deploy), abbiamo dovuto introdurre una comunicazione da micro frontend a contenitore. Vi è quindi il contenitore che si sottoscrive al micro frontend. Quando il micro frontend cambia sezione (ad esempio si passa alla sottosezione History della pagina di Deploy) viene notificato al contenitore il cambiamento, e questo si occuperà di nascondere la selezione dell’ambiente.

     

    Tanti servizi, tante risorse

    Siccome su Mia‑Platform Console non utilizziamo alcun CDN per lo storage dei file statici del frontend, l’utilizzo di un’architettura a micro frontend ha portato con sé un aumento delle risorse necessarie per il suo funzionamento. Infatti, per ogni micro frontend abbiamo dovuto creare un servizio che lo esponesse al fine di permettere a Qiankun di ottenere i file statici del micro frontend.

    Su Mia‑Platform Console avevamo quindi bisogno di 10 microservizi al fine di servire tutti i micro frontend che la compongono. Avere 10 microservizi significa avere minimo 10 pod a runtime sul proprio namespace, i quali quindi richiedono una certa quantità di risorse.

    Al fine di diminuire il consumo di risorse e la quantità di servizi a runtime, abbiamo deciso di aggregare tutti i micro frontend in un unico microservizio, il quale si occupa di servire ognuno di essi su uno specifico path. In questo modo lato frontend abbiamo mantenuto l’architettura a micro frontend, ma lato runtime sul cluster i micro frontend sono serviti tutti dallo stesso pod.
    Questo ci ha permesso di ridurre il numero di microservizi sul cluster necessari per il frontend di Mia‑Platform Console da 10 a 1.

     

    micro-lc 

    A partire dalla nostra esperienza abbiamo rilasciato micro-lc, un orchestratore di micro frontend che semplifica il lavoro di gestione di micro frontend attraverso un applicativo. micro-lc racchiude tutta la parte di backend e frontend, e ne permette l’estensione tramite plugin, necessario per sviluppare applicazioni frontend, velocizzando così il time‑to‑market delle nuove soluzioni. Il progetto è open-source e disponibile su GitHub.

     

    Conclusione

    L’aumentare della complessità dei frontend negli applicativi web moderni ha portato con sé sfide ingegneristiche sempre più interessanti. Negli ultimi anni sono infatti aumentati il numero di framework web come React, Angular, Vue.js e molti altri, ognuno con le sue peculiarità e i suoi punti di forza.
    I siti web sono passati da essere semplici pagine statiche degli anni ‘90, con il solo scopo di mostrare informazioni e con tutta la complessità gestita lato backend, ad essere veri e propri applicativi web con una forte logica di business anche lato frontend.

    L’esigenza di dominare questa complessità è quindi diventata sempre più mandatoria, sia da un punto di vista di codice, sia di gestione del lavoro. L’utilizzo di un’architettura a micro frontend è una delle risposte a questa complessità. Divide et impera.

New call-to-action
Torna all'inizio ↑
INDICE
Dal monolite ai microservizi
I micro frontend
Il nostro approccio ai micro frontend
Conclusione