BazarJS: le nostre critiche ad Angular

Angular oggi è senza dubbio il Framework Javascript per eccellenza®: la sua popolarità supera quella qualunque competitor, e di molto.

È tutto oro quel che luccica? In questo post cercheremo di descrivere la nostra esperienza diretta col framework. Una nuova puntata della serie #BazarJS sullo sviluppo di Single Page Applications.

Tempo di lettura: 11 minuti

Angular si descrive come un toolkit per migliorare l'HTML. Permette di estendere proprio l'HTML con nuovi vocaboli — che in Angular prendono il nome di direttive — in grado di trasformare un documento statico in un template dinamico minimizzando, e a volte eliminando, la necessità di scrivere codice Javascript.

È senza alcun dubbio il framework front-end più popolare tra quelli attualmente disponibili su mercato. È supportato da un team interno a Google, caratteristica che gli regala credibilità istantanea. Angular è talmente popolare da meritarsi un proprio acronimo… è infatti parte dello stack MEAN, composto da MongoDB, Express, AngularJS e Node. Non è un caso che, in questo momento, Angular sia una competenza particolarmente richiesta nel mondo del lavoro.

In Cantiere abbiamo avuto modo di lavorare per quasi nove mesi su Angular: un tempo sufficiente per potersi fare un'idea abbastanza completa dei suoi vantaggi e svantaggi. Piuttosto che produrre l'ennesimo tutorial su Angular, abbiamo preferito arrivare al punto, e descrivere il nostro punto di vista su quelle che ci sembrano essere le maggiori criticità di questo framework.

Problema #1: Scope inheritance e dynamic scoping

Questo è senza dubbio il problema più frequente e snervante per qualsiasi sviluppatore che si trova ad utilizzare Angular. Prendiamo queste righe di codice come riferimento:

<input type="text" ng-model="obj.prop" />
<div ng-if="true">
  <input type="text" ng-model="obj.prop" />
</div>

Domanda: nel secondo input tag, obj.prop si riferisce alla medesima variabile del primo input? La risposta, purtroppo, è che è letteralmente impossibile poterlo dire con certezza leggendo il codice: dipende dallo stato del programma a runtime. Non ci credi? Prova tu stesso: se inizi a scrivere all'interno del primo input, i due condivideranno la variabile. Se parti dal secondo input, i due avranno vita indipendente.

Come è possibile? La motivazione sta nella modalità con la quale Angular gestisce lo scoping delle variabili. ng-if è una direttiva che introduce un nuovo scope, che eredita prototipicamente dallo scope più esterno.

Scrivendo la prima volta sul primo input, la variabile obj.prop viene inizializzata sullo scope esterno, dunque grazie all'ereditarietà prototipale viene propagata anche allo scope interno. Viceversa, scrivendo per la prima volta nel secondo input, la variabile viene inizializzata all'interno dello scope interno, senza venire condivisa con lo scope esterno.

Semplificando e astraendo il concetto dai dettagli implementativi Angular, qualcosa di questo genere:

Ogni volta che mi trovo a dover spiegare, mio malgrado, questo concetto a nuovi sviluppatori non posso che domandarmi: tutto ciò ha senso? Fortunatamente non devo rispondermi da solo: la materia è stata discussa e formalizzata da decenni.

Si definisce lexical scoping uno scope determinabile leggendo unicamente il codice sorgente; Quando invece lo scoping è dipendente dallo stato del programma, questo si definisce dinamico. Tornando alla nostra domanda, la risposta che decenni fa ci si è dati è no, il dynamic scoping non ha senso. La quasi totalità dei linguaggi moderni implementa lexical scoping, in quanto estremamente più predicibile e maneggiabile.

Problema #2: Dirty checking

Angular supporta il cosiddetto data-binding, ovvero la possibilità di collegare frammenti del DOM a variabili Javascript: una volta impostato un binding, un'eventuale modifica alla variabile stessa è in grado di venire riflessa sul DOM senza alcun intervento da parte dello sviluppatore.

Angular non è certo l'unico framework a supportare questo concetto: la particolarità sta nel come Angular ottiene questo risultato. Supponiamo di voler generare un semplicissimo timer:

La modifica del valore di una variabile dello scope viene propagata alle viste solo dopo aver notificato ad Angular la volontà di applicarla attraverso il metodo $scope.$apply(). Per evitare di riempire il codice applicativo di queste chiamate, Angular mette a disposizione una libreria di oggetti onnicomprensiva il cui unico scopo è quello di chiamare al momento giusto $scope.$apply() al posto nostro. Nel nostro caso, possiamo sfruttare il service $interval:

Questa imposizione dall'alto di service come $interval è già opinabile di per sé, ma il punto fondamentale però è il seguente: Angular, non sapendo precisamente quali e quante variabili siano cambiate dall'ultima fase di render eseguita, è costretto ad effettuare un dirty-check su ogni binding attivo nella pagina, alla ricerca di valori che differiscano da quelli precedentemente salvati ed applicati sulla vista. Nel caso siano effettivamente presenti modifiche, vengono triggerati i change-listeners associati, ma questi ultimi potrebbero a loro volta aver apportato modifiche allo scope! Dunque il processo si ripete, fino a quando non vengono rilevati ulteriori cambiamenti.

Non serve un master in computer-science per rendersi conto di come si tratti di una operazione estremamente costosa e poco ottimizzata: una minima modifica allo scope è in grado di provocare il trigger multiplo di centiaia/migliaia di check sull'intero applicativo. Proprio per questo motivo, la community Angular ha da sempre stabilito, come buona pratica, quella di limitare il numero totale di binding presenti in un'applicazione a 2000. Superata questa soglia, non si garantiscono le performance.

Per evitare fraintendimenti, è importante ricordare come questa sia stata una scelta cosciente del team Angular, che ha cercato un trade-off a loro giudizio “accettabile” tra performance e comodità di scrittura per lo sviluppatore. Il problema è che il limite teorico risulta essere banale da superare su applicazioni mediamente complesse, e fino alla versione 1.3 di Angular, non erano presenti alternative ufficiali per aggirare il problema.

ECMAScript 7 introduce il metodo Object.observe(), in grado di agganciare change-listeners al cambiamento di valori su semplici oggetti Javascript, offrendo quindi un'alternativa più performante all'attuale fase di dirty-checking… ma ad oggi non sappiamo neanche quale potrebbe essere una possibile data di pubblicazione dello standard, figurarsi sapere quando questa funzionalità verrà implementata sui browser.

Problema #3: Dependency injection

Angular, tra le sue opinioni forti, si porta obbligatoriamente dietro anche un proprio sistema di gestione delle dipendenze, basato sul concetto di dependency injection:

var myApp = angular.module('MyApp', []);

myApp.factory('sum', function() {
  return function(a, b) {
        return a + b;
    };
});

myApp.controller('MyCtrl', function ($scope, sum) {
  $scope.foo = sum(1, 3);
});

L'injector Angular è capace di effettuare un'introspezione sui nomi dei parametri passati alle funzioni che definiscono un modulo, e di rendere quindi disponibili le relative dipendenze.

Cosa c'è di male? Beh, in un nostro precedente articolo abbiamo già avuto modo di descrivere nel dettaglio i due sistemi standard di module loading del mondo Javascript, CommonJS/AMD. Angular forza la mano su un meccanismo alternativo totalmente custom, e inferiore rispetto alle alternative già esistenti.

Il sistema di dependency injection di Angular inizia già a faticare non appena si inizia a parlare di minificazione del proprio codice, dovendosi inventare una scomoda sintassi alternativa per aggirare il problema, ma, punto ben più importante, rende totalmente inutilizzabile il prezioso lavoro effettuato da strumenti come Browserify o Webpack, capaci:

  • di gestire non solo le dipendenze Angular interne al nostro applicativo, ma anche quelle di terze parti (Bower, npm);
  • di suddividere il codice applicativo su più bundle, scaricabili in maniera asincrona dal client.

Problema #4: Inutile complessità

Ricordo bene la sensazione di depressione e sofferenza provata durante la prima (delle innumerevoli) letture della pagina relativa ai service objects di Angular. Ommioddio… providers, values, factories, services, constants…? A cosa servono? Perché sono necessarie 2000 battute e 5 modi differenti per definire un banale modulo di logica Javascript?

Dopo giornate di ricerca e sperimentazione, la conclusione è stata tragica: non ci sono particolari differenze. Sono tutti, più o meno, la medesima cosa. Tutti e cinque i concetti potrebbero essere tranquillamente raggruppati sotto un'unica identità. Non sto scherzando: all'interno di uno dei progetti Angular sviluppati internamente, composto da circa 200 file Javascript per un totale di circa 10.000 righe di codice, siamo tranquillamente riusciti ad utilizzare esclusivamente factories.

Non è l'unico punto in cui Angular sembra cercare volutamente di risultare il più oscuro possibile. Altri esempi?

  • Cosa vuol dire creare una direttiva E, A, piuttosto che EA?
  • Non esiste un modo più intelleggibile e semantico di descrivere la modalità di binding di una variabile che utilizzare simboli come =, &, =* e @?

Problema #5: Server-side rendering

La scelta di Angular di utilizzare l'HTML della pagina come linguaggio di templating attraverso l'uso di “direttive” annegate sotto forma di classi, attributi o tag, oltre a presentare un ampio numero di problematiche “minori”, si porta dietro un ostacolo insormontabile: l'incapacità di produrre applicazioni isomorfe.

Buona parte della logica di un'applicazione Angular risiede nell'HTML della pagina, tra i suoi ng-if, ng-repeat e similari: l'idea di poter far girare una applicazione Angular server-side è semplicemente impossibile, per design.

Certo, servizi come prerender.io permettono di aggirare il problema, ma si tratta a tutti gli effetti di un workaround, che a sua volta si porta dietro problematiche relative a cache-expiration.

Problema #6: Angular 2

I punti esposti non sono sufficienti? Se allora vi dicessi che la prossima major version di Angular farà terra bruciata di ciò che oggi conosciamo senza essere minimamente retro-compatibile con l'esistente?

Gli sviluppatori del team Angular, a cinque anni di distanza dal primo rilascio pubblico, hanno imparato molte lezioni e si sono resi conto di come le astrazioni sulle quali oggi si fonda il framework siano insufficienti e confuse. La decisione, annunciata all’ng-conf è quindi stata quella di scrivere la prossima major version del framework from-scratch.

È una scelta coraggiosa ed tutto sommato encomiabile, considerando che Google ha comunque garantito un lungo periodo di mantenimento per Angular 1.x: attenderemo fiduciosi la pubblicazione di questo nuovo progetto (che in comune con l'attuale probabilmente avrà solo il nome)… nel frattempo, però, sarebbe semplicemente folle utilizzare Angular 1.x su nuove applicazioni.

Conclusioni

Nonostante le parole poco lusinghiere finora espresse nei suoi riguardi, è importante sottolineare come Angular sia comunque un'alternativa accettabile per la produzione di applicazioni client-side.

La sua enorme popolarità ha consentito la nascita di un enorme numero di moduli di terze parti, in grado di dare una grossa mano nell'arrivare in tempi rapidi a propotipi funzionanti.

Una volta superata la curva di apprendimento iniziale, aver trovato una propria “formula” di strutturazione del codice e aver imparato a gestire le idiosincrasie di direttive , scope, ed ng-models, Angular è in grado di offrire tutto ciò che serve per realizzare applicazioni in tempi di sviluppo accettabili.

Si torna alla domanda fondamentale però: ne vale la pena? La risposta è no, almeno nel nostro caso. Non ci fermiamo però alla sola critica e ci sembra doveroso raccontare quella che per noi risulta una valida alternativa.

Prossima puntata? React!

La prossima settimana faremo la conoscenza di React, per molti versi un totale alieno nel mondo dei framework Javascript, con un approccio al problema della costruzione di SPA totalmente differente da quello Angular e di qualsiasi altra libreria o framework precedentemente sviluppato. Talmente alieno da averci convinto appieno.

Seguici su Twitter o tramite feed RSS per rimanere aggiornato sulla prossima uscita!

Hai trovato questo post interessante?Scopri chi siamo

Made with Middleman and DatoCMS, our CMS for static websites