facebook

Blog

Resta aggiornato

Dall’analisi al confronto: le funzionalità
Blazor vs Angular: quale scegliere? Seconda parte
martedì 11 Maggio 2021

Dopo aver confrontato i due framework dal punto di vista dell’esperienza di sviluppo, analizziamo adesso le funzionalità che entrambi mettono a disposizione in termini di componenti, infrastruttura e maturità. 

Mentre Angular è basato su ECMAScript e su un insieme di librerie appositamente create per dare vita al framework, Blazor poggia sul .NET Core framework, ereditandone tutte le funzionalità e peculiarità. A differenza del .NET Framework classico, .NET Core fornisce nativamente Dependency Injection, Logging e supporto alla configurazione, funzionalità che Blazor eredità direttamente, così come tanti aspetti avanzati come la Reflection, LINQ e tutto quello che conosciamo del mondo .NET. Uniti alla forte tipizzazione di C#, questi aspetti possono fare la differenza in tanti scenari business, a patto ovviamente di conoscerli. 

Dependency Injection 

Uno dei metodi più diffusi per implementare il principio dell’inversione di controllo (IoC), è utilizzare una Factory astratta che fornisca le dipendenze alle classi che ne hanno bisogno. In programmazione si usa per rendere gli elementi della propria soluzione bassamente accoppiati, in modo da rendere sostituibili le implementazioni delle dipendenze. Oltre ad essere una pratica molto utile per avere codice di alta qualità, manutenibile e testabile, è anche un modo molto interessante per sostituire, anche a caldo, implementazioni di una funzionalità sulla base delle proprie esigenze. Questa tecnica è chiamata Dependency Injection ed è implementata nativamente dai moderni framework di sviluppo software. 

Tipicamente, una dipendenza è un’ istanza di una classe che serve al nostro componente per eseguire delle operazioni, ad esempio invocare il back-end per recuperare delle informazioni, oppure una classe di servizio che esegue dei calcoli. 

Un fatto interessante delle librerie di dependency injection, è che ci permettono sia di definire le dipendenze, sia il loro ciclo di vita, risolvendo a catena tutte le dipendenze di cui avete bisogno. Questo significa che se abbiamo un componente che ha bisogno di una classe per fare qualche operazione, invece di istanziare direttamente la classe, chiede alla Factory di fornirgliela: sarà quindi la Factory a decidere, sulla base della sua configurazione, se istanziare una nuova classe o restituire una istanza già creata precedentemente. 

Tipicamente, una gestione del ciclo di vita che crea sempre una nuova istanza ad ogni richiesta, viene detta Transiente. Se viene create invece una istanza sulla base di un certo contesto, ad esempio se viene creata una istanza una sola volta nell’arco di una stessa richiesta HTTP e restituisco sempre la stessa a chiunque la richieda nell’arco di quella richiesta, il ciclo di vita viene chiamate Scoped, dove lo scope, in questo caso, è la richiesta HTTP. Se invece viene creata una istanza una sola volta e restituita sempre la stessa, si parla di Singleton. 

Teniamo presente che se la classe che deve essere istanziata ha, a sua volta, delle dipendenze, la Factory risolve anche quelle, creando un vero e proprio albero di oggetti che accoppiano dinamicamente le parti dell’applicazione. 

Se vi state chiedendo perché dovreste fare una cosa del genere invece di implementare tutto nei metodi del componente stesso, vi suggerisco di partire per il lungo viaggio nell’ingegneria del software. Ma vi faccio notare che, a parte tutti i benefici in termini di qualità, la possibilità di configurare il ciclo di vita di un oggetto ha il piacevole effetto collaterale di poter creare degli oggetti da condividere tra più componenti, fornendo un meccanismo di comunicazione tra elementi della vostra UI che non si trovano in relazione padre/figlio.  

Sia Angular che Blazor forniscono strumenti per la dependency injection, nonostante siano molto diversi tra loro. Angular implementa un meccanismo gerarchico, basato su provider, in cui è possibile definire chi fornisce le dipendenza a chi. Possiamo ad esempio indicare che un modulo è il provider di specifiche dipendenze: questo significa che tutti i componenti di quel modulo possono ricevere la stessa istanza della classe desiderata. In alternativa, posso definire un nuovo provider a livello di componente: in questo caso il componente crea l’istanza per se e i suoi figli (i componenti contenuti all’interno di esso). Le classi iniettate in Angular vengono dette Service, e sono identificate dall’annotazione @Injectable. In realtà questa annotazione è necessaria solo se la classe definisce a sua volta delle dipendenze.

Blazor, essendo basato su .NET Core, non ha bisogno di implementare una libreria per la dependency injection, dato che il framework la fornisce già nativamente. Quindi possiamo utilizzare tutto quello che già sappiamo del motore di dependency injection di .NET, compresa la possibilità di sostituirlo con un motore custom di nostra preferenza.  

Facciamo giusto una piccola precisazione per la gestione del ciclo di vita degli oggetti in Blazor Server e Blazor WebAssembly. In Blazor Server l’applicazione è, a tutti gli effetti, una applicazione ASP.NET Core, quindi il concetto di Singleton, Transient e Scoped resta invariato. In una applicazione Blazor WebAssembly, il codice gira nel browser, quindi Scoped e Singleton tendono a confondersi, dato che, finché non aggiornate la pagina, gli oggetti Scoped hanno la stessa durata degli oggetti Singleton. 

CSS Isolation

Anche la CSS isolation è presente in entrambi i framework: abbiamo cioè la possibilità di scrivere regole CSS che si applicano a un particolare componente, quindi il resto dell’applicazione non ne sarà impattata. Ad esempio, possiamo decidere che nel nostro componente i titoli siano rossi, senza per questo far diventare rossi i titoli degli altri componenti dell’applicazione.

Nel caso di Angular potete avere un array di file CSS, SAAS o LESS associati al vostro componete, dichiarando la lista nelle definizione del componente stesso (proprietà styleUrls dell’annotazione Component).  

Nel caso di Blazor, invece, si lavora ancora una volta di convenzione, denominando il file <NomeComponente>.razor.css. Questo significa anche che al momento potete avere un unico file e deve necessariamente essere un CSS. Ovviamente potremmo intervenire in fase di build per generare questo file da un file SAAS o LESS, e potremmo a quel punto avere più file, ma il framework e i suoi tool non ci supportano direttamente, almeno per il momento. 

Piccola nota personale: usate la CSS Isolation solo se ne avete strettamente bisogno o state realizzando componenti da ridistribuire in una libreria. In una applicazione reale mi sento di consigliare l’uso di regole di CSS comuni, basandosi magari su un framework come Bootstrap. La parte di stilizzazione è spesso complessa, e parzializzare le regole ostacola sia la manutenibilità che l’ingresso nel team di una figura che si occupa solo della stilizzazione.

Gestione delle Form 

In Angular avete a disposizione ben due librerie diverse per la gestione delle form. La prima, presente fin dalla prima versione, è chiamata Form Template, e vi fornisce tutta una serie di direttive per risolvere la creazione e la gestione di form sulla parte di markup del vostro componente. E’ la stessa libreria che vi consente di definire i famosi binding bidirezionali tra un vostro oggetto e la form stessa, che rende semplice, negli scenari più comuni, la gestione della cattura dell’input dell’utente e la sua validazione, per la quale potete usare i validatori di HTML5 o crearne di personalizzati.

A questa libreria è stata poi aggiunta la possibilità di creare form reattive, da cui prende il nome, Reactive Forms, in cui non avete più i binding bidirezionali (le performance sentitamente ringraziano), ma la possibilità di sfruttare l’approccio reattivo per tenere traccia dello stato della form e intervenire a ogni cambiamento. In questo caso la definizione della form avviene tutta lato codice, fornendo delle direttive per “agganciare” la form creata sugli elementi di markup. 

In Blazor abbiamo invece una serie di componenti ed eventi già pronti per la creazione di form. Una form viene delimitata dal componente <EditForm></EditForm>, e al suo interno possiamo inserire i principali elementi di UI già forniti o creati appositamente. Abbiamo la classica casella di testo (InputText), casella numerica (InputNumber), date picker (InputDate), e similari. Tutti questi componenti vi permettono il binding bidirezionale con un oggetto utilizzando l’attributo @bind-Value, dopo avere specificato l’istanza dell’oggetto con la proprietà Model dell’EditForm. Utilizzando il componente <DataAnnotationValidator /> potete aggiungere il supporto per la validazione sulla base delle Data Annotation della classe da cui è stato creato l’oggetto che conterrà i vostri dati.  

Potete trovare maggiori informazioni qui, ma notiamo subito che questa è una delle killer feature di Blazor per chi ha un back-end .NET: potete infatti condividere queste classi, decorate con le data annotation, tra back-end e front-end in una libreria condivisa e centralizzare così non solo la definizione di questi oggetti, ma sfruttare le Form Blazor per la validazione front-end, e il Model Binder di ASP.NET per ripetere le validazioni anche lato back-end, operazione che dovreste in ogni caso fare.

Blazor, come Angular, nasce principalmente per gestire applicazioni Business, quindi in realtà c’è tanto altro dietro le quinte, tra cui creare componenti Form personalizzati, intervenire nel processo di binding e validazione utilizzando EditContext invece di specificare semplicemente il Model, fino alla generazione dinamica della UI sfruttando la Reflection e le Expression di LINQ. Se vi va di dare un occhio, ho tenuto una sessione al WebDay organizzato da UgiDotNet sull’argomento: Generazione dinamica della UI con Blazor WebAssembly

Integrazione con il Back-end 

Angular fornisce una libreria specifica anche per l’integrazione con il back-end, grazie alla quale potete iniettare nei vostri service un HttpClient che vi permette di invocare API HTTP in maniera molto semplice. La libreria fornisce anche la possibilità di infilarsi nel processo di invocazione per fare qualcosa prima e/o dopo una chiamata, come ad esempio loggare o aggiungere degli header sulla richiesta (il classico token JWT ad esempio), definendo quelli che il framework chiama HTTP Interceptor. Maggiori info qui.

In Blazor potete utilizzare il client HTTP di .NET Core, comprensivo del suo motore di serializzazione/deserializzazione JSON e di tutto quello che sapete su questa libreria, che sicuramente avrete già usato in altri scenari. Per Blazor vengono forniti anche alcuni Extension Methods per semplificare l’invocazione di API REST, maggiori info qui.

Anche in Blazor potete inserirvi nella pipeline di invocazione HTTP, definendo in fase di configurazione del client HTTP dei message handler usando il metodo di estensione AddMessageHandler() della libreria Microsoft.Extensions.DependencyInjection. Anche in questo caso non si tratta di una libreria specifica di Blazor, ma del client HTTP. Potete approfondire qui

Inoltre, sfruttando la libreria gRPC-Web, potete utilizzare gRPC per invocare il back-end con entrambi i framework. 

JS Interop e JS Isolation

Come sapete, il codice Typescript con cui scriviamo le applicazioni Angular viene convertito in JavaScript prima dell’esecuzione. Nel caso di Blazor WebAssembly invece, il codice C# viene convertito in codice IL, jittato in WebAssembly dal runtime Mono, quindi nessuna interazione con JS. 

Ma dato che WebAssembly e JavaScript vengono eseguiti entrambi nel JavaScript Runtime, è possibile da .NET invocare una funzione JavaScript   e da JavaScript invocare un metodo .NET statico o di istanza. 

Prima della versione 5 di .NET l’organizzazione del codice JavaScript a supporto delle applicazioni Blazor era basato sull’inclusione di uno o più file JS nella index.html, agganciando all’oggetto window le funzioni da invocare. Oltre ad essere scomodo per scenari medio/grandi, questo approccio non permette la parzializzazione del codice JavaScript sulla base delle esigenze, rendendolo il più delle volte poco manutenibile e richiedendo il suo intero download all’avvio. 

Con .NET 5 è stata introdotta la JS Isolation, cioè la possibilità di associare dei file JS a specifici componenti, che ne impongono il download quando attivati, modularizzando la loro scrittura. Questa funzionalità in realtà è stata introdotta per fornire supporto ai moduli ECMAScript in Blazor, dato che i browser supportano questo standard di default. Vi lascio un articolo di approfondimento per la sintassi e le modalità d’uso: https://khalidabuhakmeh.com/blazor-javascript-isolation-modules-and-dynamic-csharp

Lazy Load 

In applicazioni molto grandi può essere un grande vantaggio parzializzare il download del nostro codice in base a strategie diverse dal singolo download iniziale. Questa funzionalità è chiamata Lazy Load e viene fornita sia da Angular che da Blazor (a partire da .NET 5). 

Il supporto di Angular per il Lazy Load è completo: potete tirare giù interi moduli on-demand configurando il router. In questo modo solo quando farete un primo accesso alla rotta configurata, il codice sarà scaricato ed eseguito. Potete quindi prevenire il download di interi moduli se l’utente non ha accesso a quella rotta o finché non vi accede. Con l’introduzione di Ivy, potete anche fare Lazy Load di singoli componenti, invece che dell’intero modulo, come ha chiaramente illustrato Salvatore nel suo articolo su Angular 9.

In Blazor al momento il supporto al Lazy Load è piuttosto limitato, e richiede la suddivisione del codice che si vuole parzializzare in diverse DLL, andando a specificare a livello di progetto che si vuole rendere il download di quelle DLL Lazy. Ci viene fornito un oggetto LazyAssemblyLoader, che possiamo iniettare dove ci serve, per attivare la funzionalità. 

Nonostante sia un primo approccio funzionante, il Lazy Load fornito in Blazor è ancora acerbo e non permette degli automatismi che sgravino il programmatore dalla gestione del download sulla base del routing, che si spera arrivi, insieme alla compilazione AOT, con .NET 6. 

Programmazione reattiva

Un discorso a parte merita la programmazione reattiva, che pervade tutto il framework Angular grazie a RxJS.

Questa libreria introduce la possibilità di gestire stream di dati nel tempo, utilizzando il pattern Observer: grazie alla possibilità di lavorare con degli oggetti Observable, possiamo utilizzare tutta una serie di operatori e crearne di personalizzati, per reagire quando qualcosa succede nella nostra applicazione. 

Nonostante tutta questa potenza di fuoco, questa è proprio la parte meno conosciuta e sfruttata dai programmatori Angular. Inoltre, volendo, esiste la controparte Rx.NET per portare gli Observable e i suoi operatori anche in Blazor, anche se, il più delle volte, non se ne sente la necessità a causa dei delegate e degli eventi forniti nativamente da .NET. 

State Management

Discorso diverso invece, sono le applicazioni per le quali può essere un valore aggiunto l’utilizzo di una libreria di State Management. In un approccio completamente reattivo, possiamo immaginare di avere localmente (nel browser intendo) uno store centralizzato che rappresenta lo stato dell’applicazione a cui è possibile sottoscriversi per essere notificati dei cambiamenti. Questo store può essere modificato solo seguendo una serie di funzioni pure (per evitare side effects) che trattano strutture dati immutabili. 

Mi dispiace banalizzare il concetto perché è molto interessante, magari ne parleremo in un altro articolo, ma a mio parere ha senso per applicazioni molto grandi che hanno una logica locale complessa, perché rende la manutenibilità dell’applicazione più sostenibile, a fronte di un debito tecnico più alto.

In Angular esistono librerie ormai diventate standard de facto per gestire un approccio del genere, come NgRx, che ha una maturità e diffusione tale da poterci basare le proprie soluzioni abbastanza a cuor leggero. 

Resta comunque la possibilità di gestire lo stato applicativo anche in applicazioni semplici, semplicemente andando a mantenere in memoria dei service o sfruttando l’accesso agli storage del browser. Vi lascio un link alla documentazione ufficiale per farvi un’idea. 

Conclusioni

Possiamo concludere che, nonostante la differenza di anzianità, i due framework si equivalgono nella maggior pare degli scenari business medio/piccoli in cui vengono utilizzati. Negli scenari medio/grandi invece Angular, grazie al supporto di RxJS che potenzia drasticamente tutte le sue librerie e un Lazy Load più completo, si posiziona sicuramente meglio. 

Ma se il grosso delle logiche da implementare sono CRUD e il back-end è scritto in .NET, la possibilità di condividere librerie tra back-end e front-end, su cui centralizzare regole di validazione e di generazione dinamica della UI, grazie a attributi standard e custom, fa pendere inevitabilmente la bilancia verso Blazor.

Comunque, c’è ancora da valutare un aspetto importantissimo, le performance, in tutte le sue sfaccettature, quindi restate sintonizzati per la terza e ultima parte di questo confronto all’ultimo bit. 

Happy Coding