Skip to content

Latest commit

 

History

History
212 lines (185 loc) · 13.3 KB

Riflessioni su AngularJS.md

File metadata and controls

212 lines (185 loc) · 13.3 KB

Riflessioni su AngularJS: per capire la logica della dashboard


1. Introduzione

Navigando nel codice della LPPM dashboard avrete notato che in un paio di punti appare una cosa di questo tipo:

.service('Dataview', function($rootScope){
    //...
    $rootScope.$broadcast('dataview.updated');
});

e alcuni di voi diranno: ma come? Non era una cosa da non fare mai? Io vi cito Fabrizio de André:

non spalancare le labbra ad un ingorgo di parole
le tue labbra cosl, frenate nelle fantasie dell'amore
dopo l'amore cosl, sicure a rifugiarsi nei "sempre",
nell'ipocrisia dei "mai"

Mi sono senza dubbio opposto a questa abitudine, frenandola in modo molto deciso. Il motivo è che, per esperienza, gli eventi, ad occhio inesperto, sembrano comodi e veloci ma troppo spesso li ho visti degenerare diventando uno strumento automatico di generazione di bordello.

Questo non significa che il modello publisher/subscriber sia sbagliato di per sè, nè ho nulla contro questo, volevo solo che venisse usato, come ogni strumento, nel modo giusto.

A volte ho avvitato viti con la punta di un coltello, perchè non avevo un cacciavite
Questo non significa che il coltello sia un cacciavite.
Nè che, in presenza di un cacciavite, userei un coltello.
Nè che taglierei la bistecca con un cacciavite.
Nè che io abbia nulla contro i coltelli o i cacciaviti.

2. Angular best practices

A parte il mio essere [manicheo] manicheismo, c'è anche una questione di best practices.

  • La comunicazione tra controllers avviene attraverso i servizi (documentatissimo). Usare solo gli eventi vuol dire fare un bordello.
  • Spesso non ho bisogno di un evento, ma è molto più espressivo, comprensibile, leggibile e tracciabile dire ad un servizio di fare qualcosa e lui lo fa ed eventualmente segnala (altri, a mia insaputa).
  • In generale, è buona pratica non iniettare $scope nei controllers. Per esperienza, se serve lo scope, serve una directive (e le sue link functions).
  • Non per nulla, i servizi (e le factory) non possono avere iniettato uno $scope, ma solo $rootScope
  • I controllers non sono altro che un'API per la UI per avere i dati. La logica per ottenere questi dati non va necessariamente (e sarebbe in effetti meglio evitare di metterla) nei controllers (anche se viene spesso più comodo, lo so)
  • $emit e $broadcast vanno capiti bene. Uno va in su, uno in giù. Ma su e giù di cosa? E qui si apre un vaso di Pandora: l'ereditariet prototipale degli$scopevs lo$scope.$parent` (no, non sono necessariamente la stessa cosa).
  • Il caso precedente è simile al onModelChanged che abbiamo messo in piedi in LPPM. LC, ho spinto per farlo a mano. Forse ho esagerato... ma non so.

3. Cos'è $rootScope

Quando una app viene su, viene creato un $rootScope. Questo è lo scope padre di tutti gli scope. Vive per sempre, dall'inizio alla fine della vita dell'applicazione. Su Stack Overflow è pieno di gente che ha preso questo concetto e ha detto: bene, allora io butto roba nel $rootScope e di fatto ce l'ho ovunque. Tradotto: creo dei globali. Che sono un male assoluto (e questo lo dico senza mezze misure e senza timore di essere smentito1).

$rootScope non è uno storage globale.

Perr di $rootScope si possono usare i $watch* e, perchè no, il $broadcast, che sono di fatto delle utility. Con parsimonia perr. E tenendo conto che:

  • $rootScope va bene iniettarlo in un servizio, ma cosl si fa solo $broadcast (segnalazione verso i figli)
  • Nei controller bisognerebbe fare di tutto per evitare di iniettare $scope. Perchè? Si veda dopo...
  • $emit, che nasce dal basso, quindi da uno $scope va limitato:
    • Non è sempre evidente chi sono i padri (vd directive e transclude)
    • Non funziona coi fratelli
    • Hanno inventato i servizi

4. Controllers e $scope

Anche se Javascript non è tecnicamente un linguaggio orientato agli oggetti, mi piace pensare di poter miscelare concetti di OOP per quel che riguarda l'architettura e concetti di funzionale per la parte più algoritmica. Alla fine AngularJS è un framework e come tale punta sull'architettura, con delle cose più zuccherose qui e lì.
Non solo, Angular2 e ECMA6 hanno il concetto di classi. Sone sempre classi ad ereditarietà prototipale, quindi un concetto diverso da quello a cui siamo abituati con altri linguaggi OO, come Java o C#. Questi sono comunque i motivi per cui mi piace l'idea di avere dei controller senza $scope iniettato, ma che usino la sintassi this e controllerAs. Non so se è più chiara, forse no, forse è anche più complessa, ma mi evita una cosa: confondere lo $scope con un posto in cui versare mille variabili e funzioni. Vorrei continuare a vederlo solo come il modo di comunicare con il framework.

Non solo, il controller è in sostanza solo una API con la UI (HTML) e deve fornire ad essa i dati e il modo per interagire con questi dati da parte dell'utente. Per il resto ci sono i servizi e, ancor più importante, le directives.

5. Directives

Secondo me le directives sono la prima cosa da capire di AngularJS. Non con gli esempietti stupidi, proprio i concetti dietro a:

  • scope isolato
  • transclusion
  • controller
  • pre e post link functions

Questi concetti aprono letteralmenet un mondo e mi hanno portato a vedere i controller come la parte di logica di una directive più che una cosa a parte e a si stante.

Quando si crea un controller all'inizio lo si appiccica su una elemento del DOM con ng-controller="myCtrl". Tutto bene, ma non si capisce cosa sta succedendo. Si sta trattando quel DOM element di fatto come una mega directive in puro HTML, quindi priva di aggiornamenti, di inizializzazione ed eventualmente... con quale... $scope?

Le uniche interazioni dinamiche sono attraverso l'uso di directive bult-in come le classiche ng-model e ng-repeat. Appena si vuole fare qualcosa di più, e purtroppo la rete è piena di questi esempi veramente di bassa qualità, è iniettare lo $scope nel controller ed usarlo da ll.

Iniettare lo $scope nel controller è solo una scorciatoia

Perchè quello $scope è anche di (alcuni) figli e di tutti gli elementi dentro l'elemento padre in cui abbiamo messo il controller. E' solo zucchero invece di una più complessa:

.directive('myDirective', function(){
    return {
        scope: false,
        controller: 'myCtrl',
        controllerAs: 'my',
        template: '<div></div>', // even better, templateUrl
        link: function(scope, elem, attrs, ctrl){
            //... here we have the scope, the current element and... the controller!!
        }
    };
});

Qui però si vede chiaramente tutto il mondo:

  • c'è dell'HTML
  • quell'HTML è passato alla link function attraverso elem, quindi ho accesso al DOM
  • viene instanziato un controller con cui posso parlare sia dalla link che dall'HTML
  • nella link ho lo $scope iniettato come variabile

Vista cosl, a cosa serve iniettare lo $scope nel controller? E non è solo questo:

  • Succede lo stesso con UI router
  • Succede lo stesso con $uibModal
  • due indizi fanno... quasi una prova.

6. Ma quindi... sti eventi?

Premesso tutto questo, gli eventi che girano negli $scope sono una feature utilizzabile, visto soprattutto un grandissimo vantaggio: le sottoscrizioni muoiono con lo $scope stesso. Quindi non serve deregistrarsi. Ripeto perr che secondo me servono due requisiti:

  • Si usa solo il $broadcast, ovvero vengono solo tirati dall'alto
  • Non se ne abusa, meglio una chiamata esplicita ed un API chiara.

Vediamo un esempio tratto da una storia vera: la dashboard. Attori:

  • Dataset: Dataset è il set completo dei dati (che arrivano dal server). E' un service
  • Dataview: è una view sul dataset, ovvero è il dataset al netto dei filtri. Dataview è l'unico che conosce il Dataset e la sua esistenza. Il resto del mondo conosce solo Dataview. E' un service che espone di fatto solo flush() che prende il dataset, lo fa filtrare dai filtri e mette a disposizione il risultato.
  • Sidebar: il controller a sinistra. Di fatto mostra i filtri, scatena l'edit (che è responsabilità dei filtri stessi) e chiede al Dataview di far flush() Quando cambio un filtro nella sidebar, la sidebar(controller) dice al Dataview (servizio) di fare flush()
  • Main il controller di destra. Chiede ai filtri di dar la descrizione dei filtri attivi (i tag in alto), si occupa di mostrare le diverse view (i report). Prende dal Dataview i dati quando disponibili e li inoltra alle view sotto. Non fa praticmente nulla.

Il problema h come far comunicare sidebar e main. La soluzione più semplice è: renderli un solo controller. Non vi è nulla di male a farlo cosl tranne che ha molte dipendenze iniettate, che anche a livello di DOM è molto "ingombrante" e che fa cose molto diverse dal setup dei filtri, alla gestione dei repot. Perr nessun problema sulla comunicazione. Perchè non l'ho fatto cosl?

Perchè guardando la pagina quelle due parti, sinistra e centro, sono due directive. O se volete due view dello stesso $state, come il menu, i dettagli e il QPL di LPPM. C'è anche una questione di ordine, non voglio controller giganti e di curiosità: come farli parlare diventa una sfida.
Quindi l'ho impostato cosl, non ho fatto le views dello $state nè due directive, ma stanno in un solo HTML con due controller dichiarati nell'HTML stesso. Passare alle directives o alla route e le view è banale da questo punto di partenza.

Ok, come segnalare che i filtri sono cambiati?
Soluzione 1: eventi puri

.service('Dataview', function($rootScope){
    //...
    $rootScope.$on('filters.changed', function(){
        // Refresh dataview data
        $rootScope.$broadcast('dataview.updated');
    });
})
.controller('Sidebar', function($scope){
    //...
    $scope.$emit('filters.changed');
})
.controller('Main', function($scope){
    //...
    $scope.$on('dataview.updated', function(){
        // refresh the local dataview that is shared with the below reports
    });
})

l' $emit va su su fino al $rootScope, viene pescato dal Dataview, che fa quel che deve e rilancia un altro evento. Sembra tutto bello e separato anche perchè la Sidebar è agnostica sul fatto che ci sia una Dataview o chissà chi altro in ascolto. Però:

  • se in ascolto dell'$emit è un fratello, non lo sente
  • con un evento è facile, anche se vi potrebbe servire fare debug e capire chi becca sto evento. Se ne mettete due o tre, poi ci sono eventi che si chiamo in sequenza, vi assicuro, in un attimo perdete il controllo di chi chiama cosa e in che ordine.
  • Il pub/sub è sincrono. Quindi hai voglia a mettere più operazione in sequenza.
  • Ma quindi se voglio rinfrescare i dati del Dataview devo tirare un evento che si chiama filters.changed? Allora forse sto evento lo deve tirare il Filter Engine stesso. Oppure si deve chiamare dataview.update.request ma a quel punto io so che serve al dataview. Boh.
  • Ho iniettato lo scope nei controller.
  • Mi sta meglio il $broadcast del servizio perchè lui mette a disposizione un array e sta segnalando ad altri che è cambiato. E' meglio di un $watch sull'array da parte dei figli perchè costa di meno e mi evita ancora di avere lo $scope.

Ho preferito una cosa più standard:

.service('Dataview', function($rootScope){
    //...
    this.flush = function(){
        // Refresh dataview data
        $rootScope.$broadcast('dataview.updated');
    });
})
.controller('Sidebar', function(Dataview){
    //...
    Dataview.flush(); // più esplicito
})
.controller('Main', function($scope){
    //...
    $scope.$on('dataview.updated', function(){
        // refresh the local dataview that is shared with the below reports
    });
})

Si potrebbe avere anche una directive dedicata ad ascoltare gli eventi provenienti dal dataview (e che quindi sta nel modulo con Dataview)

.directive('dataviewListener', function(){
   return {
        scope: {
            onDataviewUpdated: '&'
        }
        link: function(scope){
            scope.on('dataview.updated', onDataviewUpdated());
        }
   };
});

che implica nell'HTML una cosa cosl:

<div ng-controller="mainCtrl as main" 
     dataview-listener 
     onDataviewUpdated="main.onDataviewChanged()">
<!-- other stuff -->
</div>

Verboso? Forse, ma:

  • il nome dell'vento non lo sa nessuno, se non i membri di Dataview.Module
  • reuse: lo pur utilizzare chiungue abbia bisgno di sapere che è cambiata la dataview
  • ho disaccoppiato l'ascolto dell'evento (del modulo Dataview) dall'azione (la decide il controller in cui voglio agire)
  • Nessuno $scope nel posto sbagliato....

7. Save configuration

Lo stesso identico problema, anche più grande in realtà, si trova se voglio salvare le configurazioni.
La sidebar ha il bottone: faccio save. Cosa salvo:

  • Lo stato dei filtri
  • Lo stato dei report (quali, in che posizione e con quali settaggi)

Quindi:

  • Ogni filtro deve sapersi serializzare
  • Ogni report deve sapersi serializzare
  • La sidebar genera il tutto
  • Il main deve saperlo, perchè deve avvisare le view figlie.
  • Oppure le view figlie devono essere avvisate e il main manco lo sa... eventi? Pur essere un buon uso qui, oppure directive ad hoc come prima

Thanks to [Dillinger.io] dill for providing the sotware to easily write this document

Free Software, Hell Yeah!

Footnotes

  1. Questa è una di quelle cose per cui davvero userei il martello.