AngularJS
Best Practices

GDG DevFest Mediterranean 2015

Francesco Sciuti / Stefano Torresi

Chi sono questi due?

Difficile da dire senza usare brutte parole...
beh, siamo amanti e professionisti del web! Speriamo basti!

Perchè Best Practices?

L'utilizzo delle Best Practices aiuta a scrivere codice migliore, facilmente riutilizzabile ed in minor tempo.

Come per ogni altro stack, seguire le Best Practices è una forma mentis che si applica sin dalla fase progettazione di un'applicazione.

Scaffolding

Strutturare un'applicazione AngularJS e scegliere gli strumenti per evitare di impazzire dopo poco tempo!

Nelle prime fasi di un progetto si trascurano questi aspetti
ma nel lungo termine influenzerenno il codice e la manutenibilità.
Tutto ciò che serve è un po' di accortezza, un po' di disciplina
ed una banale preferenza per la sanità mentale, piuttosto che la pazzia.

ScaffoldingStruttura di un'Applicazione

Folders-by-Type vs Folders-by-Feature

Usare un approccio Folders-by-Feature piuttosto che il classico approccio Folders-by-Type 

ScaffoldingStruttura di un'Applicazione

Folders-by-Type

app/
  app.module.js
  app.config.js
  app.routes.js
  directives.js
  controllers/
      blog.js
      shell.js
      user.js
      ...
  directives/
      calendar.directive.js
      calendar.directive.html
      avatar.directive.js
      avatar.directive.html
  services/
      dataservice.js
      localstorage.js
  views/
      blog.html
      user.html
      sessions.html
      shell.html
      ...

Folders-by-Feature

app/
  app.module.js
  app.config.js
  app.routes.js
  components/
      calendar.directive.js
      calendar.directive.html
      avatar.directive.js
      avatar.directive.html
  layout/
      shell.html
      shell.controller.js
      ...
  blog/
      blog.html
      blog.controller.js
  services/
      data.service.js
      localstorage.service.js
  user/
      user.html
      user.controller.js

ScaffoldingNomenclatura

La regola fondamentale è la coerenza!

Le convezioni sui nomi (sia dei files che dei componenti di AngularJS) consentono di organizzare bene un progetto, rendendo facile la rintracciabilità e la redazione del codice.

ScaffoldingNomenclatura

Nomi dei files

Lo schema suggerito è feature.type.js
e per i test è feature.type.spec.js

Seguire uno schema nell'assegnazione dei nomi dei files consente di individuare a colpo d'occhio la funzionalità ed il tipo di codice contenuto in ogni singolo file.
Uno schema aiuta inoltre a gestire i processi di automazione (testing, concatenamento, minificazione, etc...).
calendar.directive.js
calendar.directive.spec.js
shell.controller.js
shell.controller.spec.js
blog.controller.js
blog.controller.spec.js
...

ScaffoldingNomenclatura

Nomi dei componenti di AngularJS

ComponenteConvenzioneEsempioNote
ControllerscamelCaseappBlogsControllerSuffisso Controller
Services/FactoriescamelCaseappDataServiceSuffisso Service
Evitare il prefisso $
DirectivescamelCaseappCalendarDirectiveSuffisso Directive

Seguire le convenzioni aiuta ad identificare facilmente e referenziare i componenti,
oltre che ad evitare eventuali collisioni di nomi.

angular
  .module
  .controller('appBlogsController', BlogsController);

  function BlogsController() {}

ScaffoldingPackage Manager

npm & Bower

Non possiamo fare a meno di un Package Manager!

ScaffoldingPackage Manager

NPM

Comunemente usato per la gestione di moduli Node.js, ma funziona bene anche per il front-end.

Bower

Creato esclusivamente per il front-end, puntando alla semplicità d'uso.


La più grande differenza è che NPM gestisce le dipendenze annidate mentre Bower richiede un albero delle dipendenze piatto.
La ragione per cui molti progetti usano entrambi è che sfruttano Bower per i pacchetti di front-end
e NPM per strumenti di sviluppo come Yeoman, Grunt, Gulp, JSHint, CoffeeScript, etc...

ScaffoldingTask Runner

Gulp vs Grunt

Facciamo fare il lavoro ripetitivo ad altri!

ScaffoldingTask Runner

Gulp

Gulp segue un approccio code first e lancia i processi in maniera concorrente usando stream di dati.

Grunt

Grunt è invece configuration-based ed opera in maniera sequenziale eseguento esplicitamente le operazioni di I/O.


Se da Grunt si passa a Gulp e non si torna indietro.
Più veloce nell'esecuzione dei tasks, oltre ad avere anche una migliore e flessibile modalità di configurazione.

ARCHITETTURA

AngularJS lascia abbastanza spazio all'interpretazione, in quanto è meno opinionated delle alternative.

ArchitetturaModuli

  • Un modulo principaleIl punto di partenza che funge da somma delle parti.
  • Un modulo per le funzioni comuniSeparare le funzioni comuni in un modulo dedicato, per evitare dipendenze circolari tra sotto-moduli.
  • Moduli piccoli ed autonomiLimitare le funzionalità di ogni sotto-modulo ad una singola area semantica.
  • Coerenza nelle nomenclatureUsare un prefisso proprietario per distinguere facilmente il vostro codice da quello di terzi (evitare "ng"!).

ArchitetturaModuli

Dichiarazione moduli
// Modulo principale
angular.module('app', [ 'app.common', 'app.blog', 'app.user' ]);

// Modulo comune
angular.module('app.common', []);

// Sotto moduli
angular.module('app.user', [ 'app.common', 'ngRoute' ]);
angular.module('app.blog', [ 'app.common', 'ngRoute', 'textAngular' ]);

ArchitetturaControllers

  • Delegare la business logic a servizi dedicatiLimitare le funzionalità dei controller al binding dei dati e al controllo delle view.
  • Usare la sintassi controllerAsPrevenire l'affollamento degli $scope e facilitare i riferimenti nidificati.
  • Usare servizi ed eventi per comunicazioni cross-controllerDisaccoppiare componenti interdipendenti il più possibile isolando i comportamenti collegati.

ArchitetturaControllers

Delegare la business logic

Fat controller

angular.controller('appBlogController', function($http) {
  $http.get('/api/blog-posts')
    .then(function(response){
      // convertire la risposta in dati rappresentabili
    })
    .then(function(error){
      // gestire errori http
    })
  ;
  // segue data binding...
});

ArchitetturaControllers

Delegare la business logic

Silm controller / Fat model

angular.controller('appBlogController', function(appBlogService) {
  appBlogService.getPosts().then(function(blogPosts){
    this.blogPosts = blogPosts;
  });
});

ArchitetturaControllers

Sintassi controllerAs

$scope affollati

angular.module('app.blog')
  .config(function($routeProvider) {
    $routeProvider.when('/blog', {
      template: 'blog.html'
      controller: 'appBlogController'
    });
  })
  .controller('appBlogController', function($scope, appBlogService) {
    appBlogService.getPosts().then(function(blogPosts){
      $scope.blogPosts = blogPosts;
    });
    $scope.deletePost = function(postId) { /* etc */ };
  });

ArchitetturaControllers

Sintassi controllerAs

controllerAs li snellisce

angular.module('app.blog')
  .config(function($routeProvider) {
    $routeProvider.when('/blog', {
      template: 'blog.html'
      controller: 'appBlogController'
      controllerAs: 'blog'
    });
  })
  .controller('appBlogController', function(appBlogService) {
    appBlogService.getPosts().then(function(blogPosts){
      this.blogPosts = blogPosts;
    });
    this.deletePost = function(postId) { /* etc */ };
  });

ArchitetturaControllers

Comunicazione Cross-controller
// nel modulo utente
angular.service('appUserService', function($http, $rootScope) {
  this.login = function() {
    $http.post('/api/login').then(function(response) {
      $rootScope.$broadcast('userLoggedIn', response.data);
    });
  };
});

// nel modulo blog
angular.controller('appBlogController', function($scope) {
  $scope.$on('userLoggedIn', function(event, data) {
    this.author = data.username;
    this.showNewPostButton = true;
  });
});

ArchitetturaServizi

  • Applicare il Single Responsibility Principle (cum grano salis)Limitare i motivi per i quali un servizio potrebbe dover cambiare.
  • Restituire una promise dalle funzioni che operano a loro volta su promisesPermettere la concatenazione di logiche asincrone.
  • Service, Factory e ProviderUsare la dichiarazione giusta per ogni circostanza.

ArchitetturaServizi

Promise chaining

async -> sync

angular.controller('appBlogService', function($http) {
  var data.blogPosts = [];

  this.getPosts = function() {
    $http.get('/api/blog-posts')
      .then(function(response){
          data.blogPosts = response.data;
      });
    return data.blogPosts;
  });
});

ArchitetturaServizi

Promise chaining

async -> async

angular
  .service('appBlogService', function($http) {
    this.getPosts = function() {
      return $http.get('/api/blog-posts')
        .then(function(response){
            return response.data;
        });
    });
  })
  .controller('appBlogController', function(appBlogService) {
    appBlogService.getPosts().then(function(blogPosts){
      this.blogPosts = blogPosts;
    });
  });

ArchitetturaServizi

Dichiarazione Servizi

Il più semplice: constructor function

function AppBlogService() { /* etc */ }

angular.service('appBlogService', AppBlogService);

ArchitetturaServizi

Dichiarazione Servizi

Più flessibile: factory

function AppBlogService() { /* etc */ }

angular.factory('appBlogService', function(dependency1, dependency2) {
  var someDependency;
  // preparazione di someDependency tramite le altre dipendenze...

  return new AppBlogService(someDependency);
});

ArchitetturaServizi

Dichiarazione Servizi

Massima configurabilità: provider

angular
  .provider('appBlogService', function() {
    this.config = { /* etc */ };

    this.$get = function($http) {
      return new AppBlogService(this.config, $http);
    }
  })
  .config(function(appBlogServiceProvider) {
  /* si può configurare il servizio prima che venga istanziato */
    appBlogServiceProvider.config = { /* etc */ };
  })
;

ArchitetturaDirettive

  • Modificare il DOM SEMPRE tramite direttiveLe direttive sono specificatamente progettate per intervenire sul DOM.
  • Evitate jQuery per quanto possibilejQlite e vanilla JS sono di solito più che sufficienti
  • Utilizzare appropriatamente controller e postLinkcontroller per riutilizzare la logica, postLink per accedere al DOM.
  • Utilizzare la sintassi controllerAsIn maniera analoga ai controller.
  • Coerenza nelle nomenclatureIl prefisso proprietario è ancora più importante nelle direttive perché è molto più alta la probabilità di collisione.

ArchitetturaDirettive

Un esempio pratico

angular.directive('appBlogPosts', function() {
  return {
    template: 'blog-posts.html',
    controller: 'appBlogController',
    controllerAs: 'blogCtrl'
    bindToController: true,
    scope: {},
    link: function(scope, element, attr, controller) {
      element.on('swipe-left', function() {
        var loadingImage = element[0].querySelector('img.loading');
        loadingImage = angular.element(loadingImage);
        loadingImage.addClass('visible');
        controller.loadNextPage().then(function(){
          loadingImage.removeClass('visible');
        });
      });
    }
  };
});

TEMPLATES

Alcuni piccoli accorgimenti ai templates possono migliorare sensibilmente la nostra applicazione!

TemplatesAccorgimenti

  • Usare ove possibile il one-way binding tramite ::Per prevenire un eccessivo numero di watcher nel digest loop.

    {{::boundOnce}}

  • Usare ng-repeat con track byEvita di ricreare gli elementi del DOM che risultano essere già presenti in lista, ad esempio dopo un aggiornamento del model.

    {{post.title}}

    {{post.description}}

  • Utilizzare appropriatamente ng-if, ng-show e ng-hideLa prima aggiunge o rimuove completamente l'elemento dal DOM mentre la seconda mostra o nasconde l'elemento tramite semplicei stili CSS.

TemplatesAccorgimenti

  • Prevenire FOUC (Flash Of Unstyled Content)Evitare il fastidioso effetto non voluto utilizzando soluzioni come la direttiva ng-cloak, usando ng-bind piuttosto che la sintassi {{ }} o lavorando con le direttive ng-show ed ng-hide
    
    

    {{post.description}}

    ...
    
    

    ...
  • Utilizzare sempre le direttive ng-Se presenti, preferire sempre le direttive come ng-href, ng-src, etc... piuttosto che i rispettivi attributi HTML per evitare comportamenti indesiderati prima del parsing da parte di $compile.
    ...
    
  • Valutare adeguatamente l'uso della direttiva ng-controllerAccoppiare il controller in una route consente a route diverse di invocare diversi accoppiamenti di controller e view.

TemplatesHTML Templates

Jade

Un linguaggio per la scrittura concisa di template HTML.

Tips

Alcuni accorgimenti di fondamentale importanza.

TipsGenerali

  • IIFE (Immediately Invoked Function Expressions)Evitare di intervenire accidentalmente sul global scope.
  • Named functionsLe named function offrono la possibilità di essere riuitilizzate.
  • Strict DI Mode (Strict Dependency Injection Mode)Permette di evitare errori di nomenclatura dei servizi, difficili da debuggare.

TipsGenerali

IIFE e named function
(function() {
    'use strict';
    function AppBlogService() { /* etc */ }

    angular.module('app.blog')
        .factory('appBlogService', appBlogService);
})();

TipsGenerali

Strict DI mode
<html ng-app="application" ng-strict-di>
oppure
angular.bootstrap(document, ['application'], { strictDi: true});
e poi
angular.service('appService', ['$http', function($http) { /* etc */ }]);

Links

  • https://plus.google.com/+AngularJS/

  • https://github.com/johnpapa/angular-styleguide

  • https://github.com/toddmotto/angularjs-styleguide

  • http://www.ng-newsletter.com/

  • http://www.html5today.it

THE END

Francesco Sciuti / Stefano Torresi


http://www.html5today.it


http://vroom.agency