Non molto tempo fa con un post di Nicola Malizia abbiamo visto le novità introdotte da JavaScript 6 (o meglio ECMAScript 6 o ES6 per gli amici). Nonostante la matrice di compatibilità di ES6 con i maggiori browser sia sempre più “piena”, si ricorre spesso ai transpiler (come o Babel) per usare oggi gran parte delle caratteristiche future di JavaScript. Vediamo quindi come usare oggi alcune delle caratteristiche di ES6 anche con AngularJS!
Perché ES6 e AngularJS
Perché parlare di AngularJS quando tutti parlano di Angular 2? Come prima risposta mi viene da pensare che sono due soluzioni parenti ma che vivranno a lungo in parallelo (per unirsi nuovamente in un futuro prossimo?). Di fatto ad oggi esistono molte applicazioni basate su AngularJS e dargli una rinfrescata in chiave ES6 non può che fare bene al debito tecnico, rendendo un’eventuale migrazione ad Angular 2 (temerari!!) un po’ meno dolorosa. Nel contempo, adottare anche la nuova naming convention di Angular 2 può aiutare a rendere più portabile (e anche più standard perché no) il nostro codice. Ma quali sono quindi gli artefatti AngularJS che possiamo aggiornare agilmente, per esempio, sotto forma di classi?
Prendendo quindi spunto dalla ormai celeberrima styleguide per AngularJS di John Papa, che ha ispirato poi una sua variante in ES6, vediamo come è facile portare alcuni concetti di AngularJS in ES6.
Prerequisiti
Possiamo creare un nuovo progetto come più ci piace, con il transpiler che preferiamo. Per questo post ho usato SuperNova Starter, perché ha già un sacco di feature, tra cui ng-annotate, per risolvere in modo più agevole la dependency injection di Angular.
Controllers
Fino a ieri scrivevo i controller così:
angular.module('weatherApp') .controller("WeatherController", [ '$scope', 'WeatherService', function WeatherController($scope, WeatherService) { $scope.pageTitle = 'Weather around me'; }])
{{pageTilte}}
Si trattava quindi di una funzione, con le proprie dipendenze esplicite come stringhe (per evitare problemi di minificazione), tra cui lo $scope
.
Ad oggi, alla luce delle best practice emerse nell’uso di Angular, possiamo riscrivere il controller così:
export default class WeatherController { /* @ngInject */ constructor(WeatherService) { Object.assign(this, {WeatherService}); this.pageTitle = 'Weather around me'; } }
- La funzione quindi diventa una classe, possibilmente esportata di default dal modulo (ovvero il file js che la contiene, in nomenclatura ES6)
- Possiamo evitare di usare lo
$scope
e incoraggiare l’uso delthis
(se nel template si usa la sintassi del “controllerAs“):{{w.pageTilte}}
In questo modo è molto più chiara l’appartenenza delle variabili quando si legge un template.
- A differenza di ES5, in questo caso le dipendenze devono essere associate a variabili interne alla classe. Per evitare boilerplate, l’uso di Object.assign dichiara implicitamente nella classe
this.WeatherService
. Altre eventuali dipendenze sono dichiarate separate da virgola. - Infine, grazie all’uso di ng-annotate (che poteva essere usato tranquillamente in ES5), evitiamo di dover specificare le dipendenze come stringa. Se non si volesse/potesse usare, è consigliabile specificare le dipendeze direttamente sull’injector:
WeatherController.$inject = ['WeatherService'];
Servizi
Notoriamente in Angular è possibile creare servizi sia tramite factory method:
angular.module('weatherApp') .factory('WeatherService', ['$http', function WeatherService($http) { function getLocationWeather() { ... } return { getLocationWeather: getLocationWeather } }])
che constructor function:
angular.module('weatherApp') .service('WeatherService', ['$http', function WeatherService($http) { this.getLocationWeather = function() { ... } }])
Passando ad ES6, possiamo usare solo questa seconda versione, perché Angular istanzierà per noi la classe (che dopo la compilazione non è altro che una funzione).
Il servizio diventa quindi una classe:
export default class WeatherService { /* @ngInject */ constructor($http) { Object.assign(this, {$http}); } getLocationWeather() { ... } }
dove valgono le considerazioni fatte per i controller.
Direttive
Nel caso delle direttive, AngularJS si aspetta una funzione che restituisce un object literal:
angular.module('weatherApp') .directive('weather', function WeatherDirective() { return { templateUrl: './weather.html', restrict = 'E', scope = { forecast: '<' }, controllerAs = 'ctrl', bindToController = true, controller = function WeatherDirectiveController() { ... } link: function link() { ... } }; });
Che possiamo trasformare in una classe del tipo
export default class WeatherDirective { constructor() { this.templateUrl = './weather.html'; this.restrict = 'E'; this.scope = { forecast: '<' }; this.controllerAs = 'ctrl'; this.bindToController = true; this.controller = WeatherDirectiveController } static selector() { return 'weather'; } link() { ... } } class WeatherDirectiveController { ... }
L’object literal atteso viene “splittato” in più parti: gli attributi di tipo funzione (come link
) diventano funzioni della classe, gli altri invece sono attributi di istanza (compreso il controller WeatherDirectiveController
, definito come classe).
In più possiamo aggiungere la funzione statica selector
, per definire in un posto solo qual è il selettore che identifica la direttiva.
Componenti
Da AngularJS 1.5 sono stati introdotti i componenti, che meritano tutto un approfondimento in un post a parte, che verrà pubblicato nelle prossime settimane. In poche parole, sono speciali direttive (semplificate) che introducono l’architettura a componenti nel mondo di AngularJS, mutuata in gran parte dall’esperienza di Angular2.
Moduli
Ancora non abbiamo visto però come aggregare in un modulo AngularJS le classi scritte. In AngularJS, un modulo è un insieme di controller, direttive, servizi e/o template che implementano una certa funzionalità. Come abbiamo visto in questo esempio, solitamente si era abituati a dichiarare un controller o un servizio direttamente sull’oggetto ritornato da angular.module.
Usando le classi invece, è più naturale pensare in termini di ES6, dove ogni file .js è un modulo a sé stante (da non confondere con il concetto di modulo di AngularJS!!) che esporta, ovvero rende pubblica, una certa funzionalità, che sia una classe o una funzione (o una stringa come vedremo a breve!).
Creare quindi un modulo AngularJS, significa adesso avere un modulo JavaScript (ovvero un file .js) preposto ad importare le classi che parteciperanno al modulo Angular. Si riesce quindi ad avere più facilmente separazione delle responsabilità. Immaginiamo infatti di avere la cartella del modulo organizzata come segue:
src/modules/weather --- weather.controller.js --- weather.service.js --- weather.directive.js --- index.js ...
E’ molto chiara la responsabilità di ogni file js. index.js è invece quello che fa da indice e definisce il modulo AngularJS:
import angular from 'angular'; import WeatherController from './weather.controller'; import WeatherService from './weather.service'; import WeatherDirective from './weather.directive'; export default angular.module('WeatherModule', []) .controller(WeatherController.name, WeatherController) .service(WeatherService.name, WeatherService) .directive(WeatherDirective.selector(), () => new WeatherDirective()) .name;
Da notare che solo nel caso delle direttive è necessario passare una factory function che istanzia la classe creata da noi.
Cos’è invece quel .name
che si ritrova spesso? In ES6, è possibile in questo modo chiedere alla funzione il suo nome come stringa: possiamo così usare questa tecnica per registrare un controller o un servizio in base al nome che abbiamo dato alla classe. Per le direttive invece è meglio specificare il selector (perché è indipendente dal nome della classe).
index.js esporta a sua volta il nome del modulo ("WeatherModule"): infatti anche su angular.module
viene richiamato .name
! Possiamo così a sua volta includere questo modulo AngularJS in un altro modulo semplicemente tramite un import, che implicitamente richiama index.js.
import angular from 'angular'; import Weather from './modules/weather'; angular.module('WeatherApp', [Weather]);
Facendo così, ci siamo liberati di gran parte delle stringhe presenti nelle dichiarazioni delle risorse di AngularJS.
Conclusioni
Abbiamo visto quindi come lavorare con il nostro caro AngularJS in modo più strutturato, in uno stile di programmazione “al passo con i tempi”. Per far questo però, abbiamo bisogno di strumenti di build piuttosto smart: se prima infatti bastava includere brutalmente un file js nella pagina html, adesso non è più sufficiente. Per scrivere codice di questo tipo abbiamo infatti bisogno non solo di un transpiler, ma anche di un Module Loader come per esempio Webpack, che affronteremo la settimana prossima grazie alla gentile collaborazione con Christian Chiama.
Se volete esplorare il sorgente usato per questo post lo trovate sul nostro profilo GitHub.