Formation AngularJS

Framework en profondeur + outils

Bertrand GAILLARD
Sapiens RWD - @SapiensRWD

Digest cycle

  • Secret du data-binding.
  • Ce qui donne le coté "magique".

1. expression dans HTML = watcher ajouté au scope.

{{ 'variable = ' + maVar }}

...
// code explicatif non fonctionnel !!

$scope.$watch(function() {
  return $parse(" 'variable = ' + maVar ")($scope);
}, function(newValue, oldValue) {
  // met à jour le contenu de l'élément du DOM ('element.text()') avec nouvelle valeur.
});

$scope.$watch(function() {
  return $parse(" getScore() ")($scope);
}, function(newValue, oldValue) {
  // met à jour le contenu de l'élément du DOM ('element.text()') avec nouvelle valeur.
});

$scope.$watch(function() {
  return $parse(" {'text-success': awesome, 'text-large': giant } ")($scope);
}, function(newValue, oldValue) {
  // met à jour les classes de l'élément du DOM ('element.addClass()' / 'element.removeClass()') avec nouvelle valeur.
});

$scope.$watch(function() {
  return $parse(" isTermsOk ")($scope);
}, function(newValue, oldValue) {
  // met à jour le DOM ('element.html()') en ajoutant ou supprimant les éléments enfants suivant si true/false.
});

2. Quand un "digest cycle" tourne, les expressions sont parsées et si résultat diffère du précédent, la fonction associée est lancée (dirty checking).

// code explicatif non fonctionnel !!
var Scope = function() {
  this.$$watchers = [];
  
  this.$watch = function(watcherFn, listenerFn) {
    this.$$watchers.push({
      watcherFn: watcherFn,
      listenerFn: listenerFn
    });
  };
  this.$digest = function() {
    this.$$watchers.foreach(function(watcher) {
      var newValue = watcher.watcherFn();
      var oldValue = watcher.last;
      if(newValue !== oldValue) {
        watcher.listenerFn(newValue, oldValue);
        watcher.last = newValue;
      }
    });
  };
}
Matthieu Lux - ngEurope

3. Un "digest cycle" n'est jamais lancé par $scope.$digest() mais par $scope.$apply().

// code explicatif non fonctionnel !!
var Scope = function() {
  //....
  
  this.$apply = function(exprFn) {
    try {
      exprFn();
    } finally {
      this.$digest();
    }
  };
}
app.controller('MainCtrl', function($scope, $timeout) {
  $scope.myVar = 'titi';
  
  setTimeout(function() {
    $scope.myVar = 'toto';
  }, 2000);
  // > la vue ne sera pas mise à jour..
  // Angular n'est pas averti de la maj !
  
  setTimeout(function() {
    $scope.$apply(function() {
      $scope.myVar = 'toto';
    });
  }, 4000);
  // > la vue sera mise à jour !
  
  $timeout(function() {
    $scope.myVar = 'toto';
  },6000);
  // > la vue sera mise à jour !
});

Dans l'écosystéme Angular, le $apply est appelé frequemment sans qu'on le voit:


  • Pour chaque interaction utilisateur:
    ngClick, ngDblclick, ngMouseenter, ...

  • Pour chaque opération asynchrone:
    $timeout, $interval, $q, $http, ...

A noter

  • Bon code = peu/pas de $apply.
  • Le $apply est principalement utilisé pour l'intégration de libraries externes à Angular.
  • Digest cycle = Digest loop car un cycle peut changer d'autres watchers (10 fois max).
  • Possibilité de faire des watchers en JS:

    app.controller('MainCtrl', function($scope) {
      $scope.myVar = 'titi';
      
      $scope.$watch(function() {
        return $scope.myVar;
      }, function(newValue, oldValue) {
        // la variable myVar vient de changer !
      });
      
      var unbinder = $scope.$watch('myVar', function(newValue, oldValue) {
        // la variable myVar vient de changer !
      });
      
      // plus tard, on supprime le watcher:
      unbinder();
      
    });

Les services

  • Un service = Simple objet JS avec propriétés et méthodes.
  • Utilise l'injection de dépendances.
  • Créé une seule fois lors du premier appel (Singleton).
  • Réutilisé lors des appels suivants pour toute la durée de vie de la SPA.
  • Accessible de partout quelque soit le module sur lequel ils sont créés ou appelés.
  • Peut être injecté dans controllers, filtres, services,..
  • Utilisation à volonté pour alléger au maximum les controlleurs.

Fonctionnement interne de DI

  • Chaque service vient avec un provider qui sert à le créer.
  • A l'initialisation d'un module, tous les providers des services sont réferencés.
  • C'est seulement lors de sa première injection que l'instance du service est créée grâce à son provider.
  • Lors des injections suivantes, Angular fournit directement l'instance, plus besoin du provider.
Dans la pratique, chaque provider posséde une méthode $get qui retourne l'instance et peut comporter des propriétés ou méthodes servant à configurer l'instanciation du service associé.

Différentes déclarations de services

Il existe 5 méthodes pour déclarer un service mais dans tous les cas, ils s'injectent et s'utilisent de la même manière et il en existe toujours une seule instance.

  • module.provider()
  • module.factory()
  • module.service()
  • module.value()
  • module.constant()

factory() et service() sont juste des manières plus simples d’écrire un provider().

1/5: PROVIDER()

  • Permet de paramétrer le service au démarrage du module.
  • Fonction $get obligatoire retournant l'instance du service.
var app = angular.module('myapp');
                        
app.provider('MyService', function() {
    var scale = 1;
    
    this.setScale = function(newScale) {
      scale = newScale;
    };
    
    this.$get = function ($window) {
        return {
            getScreenWidth: function() {
                return $window.innerWidth * scale;
            },
            getScreenHeight: function() {
                return $window.innerHeight * scale;
            }
        };
    };
});

app.config(function(MyServiceProvider) {
  MyServiceProvider.setScale(10);
});

app.controller('AppCtrl', function($scope, MyService) {
  var _width = MyService.getScreenWidth();
});

2/5: FACTORY()

  • Simplification de la méthode provider().
  • Equivalent à la méthode $get précédente.
  • Provider automatiquement créé par Angular.
app.factory('MyService', function($window) {
    var scale = 1;
    
    return {
        getScreenWidth: function() {
            return $window.innerWidth * scale;
        },
        getScreenHeight: function() {
            return $window.innerHeight * scale;
        }
    };
});

app.controller('AppCtrl', function($scope, MyService) {
  var _width = MyService.getScreenWidth();
});

3/5: SERVICE()

  • Simplification de la méthode provider().
  • Fonction directement instanciée avec un new pour créer l'instance du service.
  • Pas besoin de return un objet, tout ce qui est attaché à this sera accessible par le biais du service.
app.service('MyService', function($window) {
    var scale = 1;
    
    this.getScreenWidth = function() {
        return $window.innerWidth * scale;
    };
    
    this.getScreenHeight = function() {
        return $window.innerHeight * scale;
    }
});

app.controller('AppCtrl', function($scope, MyService) {
  var _width = MyService.getScreenWidth();
});

4/5: VALUE()

  • Stocker un objet JS de type object ou bien de type primitif (booléen, string, number).
  • Inaccessible par les provider lors de la phase de configuration du module.
  • Peut être modifié pendant l'éxecution de l'application.
  • Impossible d'utiliser l'injection de dépendances pour utiliser d'autres services.
app.value('GoogleMapAPI_KEY', '5VUhR4kqvVvhXRH4BIAN')

app.value('VideoData', {
    title: 'Ters Akan şelale!',
    description: 'Bu işte bir yanlışlık var diye İngiltere',
    url: 'http://www.dailymotion.com/video/x28i2n3_ters-akan-selale_news'
})

app.controller('AppCtrl', function($scope, VideoData) {
  $scope.video = VideoData;
});

5/5: CONSTANT()

  • Fonctionne comme value() mais est accessible lors de la phase de configuration du module.
  • Malgré leur nom, elles peuvent être modifiées.
app.constant('UselessData', {
    scale: 42
});

app.config(function(MyServiceProvider, UselessData) {
  MyServiceProvider.setScale(UselessData.scale);
});

app.controller('AppCtrl', function($scope, UselessData) {
  $scope.scale = UselessData.scale;
});

Exemple de déclaration de services

playerApp.value('Params', {
  URL_API: 'https://api.dailymotion.com/videos',
  URL_EMBED: 'http://www.dailymotion.com/embed/video',
  VIDEOS_PER_PAGE: 12,
  VIDEOS_AUTOPLAY: true,
});

playerApp.factory('Dailymotion', function($http, $sce, Params) {
  var getApiUrl = function(terms, page) {
      var api_params =  '?fields=id,title,description,duration_formatted,created_time&sort=relevance';
      var api_limit =   '&limit=' + Params.VIDEOS_PER_PAGE;
      var api_page =    '&page=' + page;
      var api_search =  '&search=' + terms.split(' ').join('+');
      var api_jsonp =   '&callback=JSON_CALLBACK';
    
    return Params.URL_API + api_params + api_limit + api_page + api_search + api_jsonp;
  };
  
  var Service = {
    getResults: function(terms, page) {
      return $http({
        method: 'JSONP',
        url: getApiUrl(terms, page),
        cache: true,
        transformResponse: function (data, headers) {
          data.nbPages = Math.ceil(data.total / data.limit);
          return data;
        }
      });
    },
    
    getIframeUrl: function(videoId) {
          var url = Params.URL_EMBED + '/' + videoId + '?autoPlay=' + Params.VIDEOS_AUTOPLAY;
          return $sce.trustAsResourceUrl(url);
    }
  };
  
  return Service;
});

Exemple d'utilisation de services

playerApp.controller('VideoPlayerModalCtrl', function($scope, $modalInstance, Dailymotion, video) {
  $scope.video = video;
  $scope.iframeUrl = Dailymotion.getIframeUrl(video.id);
  
  $scope.close = function () {
    $modalInstance.close();
  };
});
En pratique, factory() et value() sont utilisés dans la plupart du cas.

Services fournis par AngularJS

  • Couche d'abstraction utile pour fonctionner dans l'écosystéme AngularJs et pour être mocké lors des tests.

  • Commencent tous par $ (conventions):
    $http, $location, $filter, $window, $timeout, $interval, $animate, $rootScope, $document,..

  • Providers associés pour les configurer au démarrage:
    $httpProvider, $locationProvider, $filterProvider, $animateProvider,..

L'authentification

Types d'authentification

  • Cookie
  • WSSE
  • JWT (JSON Web Token)

REST doit être sans état (stateless)

"Chaque requête d’un client vers un serveur doit contenir toute l’information nécessaire pour permettre au serveur de comprendre la requête, sans avoir à dépendre d’un contexte conservé sur le serveur. Cela libère de nombreuses interactions entre le client et le serveur."

COOKIE

Ajout d'un cookie avec un ID de session sur le navigateur de l'utilisateur aprés l'authentificaion.

Le serveur lit ce cookie à chaque requête suivante pour identifier l'utilisateur.

WSSE

Génération d'un "passwordDigest" coté client à partir du login + pwd + salt qui est ajouté à chaque requête REST.

Possibilité de faire nativement avec Symfony2

JWT (JSON Web Token)

Requête d'authentification qui retourne un "JWT access token" à durée limité qui est ajouté à chaque requête REST.

Bundle Symfony2 LexikJWTAuthenticationBundle.

Solution de plus en plus utilisée et trés bien pour les clients mobiles.

Les performances

  • Limiter le nombre de watchers (scroll infini,..) en découpant son application.
  • Limiter les watchers sur les tableaux.
  • Utiliser ngRepeat avec 'track by'.
  • Différence entre ngIf / ngSwitch et ngShow / ngHide.
  • Utiliser le 'one-time binding' le plus souvent possible.

    {{ ::title }}

  • Possibilité d'utiliser $scope.$digest() au lieu de $scope.$apply(). [Avec précaution!]
    Le digest cycle parcourera seulement le scope courant et ses descendants plutôt que toute l'application.
  • Limiter l'utilisation des filters.
  • Sur la prod, passer debugInfoEnabled à false:

    app.config(function($compileProvider) {
      $compileProvider.debugInfoEnabled(false);
    });

Librairies indispensables

  • ui-router
  • restangular
  • angular-translate
  • ui-bootstrap
  • angular-strap
  • angular-http-auth
  • ngAutoComplete
  • ...

Plus sur ngmodules.org

Surcouches d'Angular pour applications mobiles hybrides:

Les outils

Bower

Equivalent de Composer pour les libraires Front.


bower init                              // démarrage d'un projet bower
bower install jquery --save             // installation nouvelle librairie front
bower install jquery#2.15.5 --save      // installation d'une version spécifique
bower uninstall restangular --save      // desinstallation une librairie
bower list [--offline] [--paths]        // listing des librairies installées
bower search bxslider                   // recherche d'une librairie
bower update angular --save             // mise à jour d'une librairie déjà installé
bower info angular-google-maps          // liste les versions disponibles
bower cache clean                       // suppression cache bower

SASS

  • Méta-langage utilisé pour générer des feuilles de style au format CSS.
  • Utilisation de variables, mixins, imbrication,..
  • Pas indispensable mais indispensable!
  • Compass est une librairie SASS comprenant de nombreux mixins.
@mixin box-shadow($properties...) {
  -moz-box-shadow: $properties;
  -webkit-box-shadow: $properties;
  -o-box-shadow: $properties;
  box-shadow: $properties;
}
    
$color_start: #f5f3f4;
$color_hover_start: #F4F4F4;

@for $i from 0 through 15 {
  $darken_value: min(($i + 1) * 6, 100);
  $darken_value_more: min($darken_value + 8, 100);
  $darken_value_more_more: min($darken_value + 20, 100);
  
  &.level-#{$i} {
    background: darken($color_start, $darken_value);
    border: 1px solid darken($color_start, $darken_value_more);
    @include box-shadow(inset 0px 0px 10px 0px darken($color_start, $darken_value_more_more));
    
    &:hover {
      border-color: #666666;
    }
        
    .options {
      border-bottom: 1px solid darken($color_start, $darken_value_more);
    }
  }
}
.level-0 {
  background: #e7e2e5;
  border: 1px solid #d5ccd0;
  -moz-box-shadow: inset 0px 0px 10px 0px #b9abb2;
  -webkit-box-shadow: inset 0px 0px 10px 0px #b9abb2;
  -o-box-shadow: inset 0px 0px 10px 0px #b9abb2;
  box-shadow: inset 0px 0px 10px 0px #b9abb2; }
  .level-0:hover {
    border-color: #666666; }
  .level-0 .options {
    border-bottom: 1px solid #d5ccd0; }

.level-1 {
  background: #d9d2d5;
  border: 1px solid #c7bbc1;
  -moz-box-shadow: inset 0px 0px 10px 0px #ab9aa2;
  -webkit-box-shadow: inset 0px 0px 10px 0px #ab9aa2;
  -o-box-shadow: inset 0px 0px 10px 0px #ab9aa2;
  box-shadow: inset 0px 0px 10px 0px #ab9aa2; }
  .level-1:hover {
    border-color: #666666; }
  .level-1 .options {
    border-bottom: 1px solid #c7bbc1; }

.level-2 {
  background: #cbc1c6;
  border: 1px solid #b9abb2;
  -moz-box-shadow: inset 0px 0px 10px 0px #9d8993;
  -webkit-box-shadow: inset 0px 0px 10px 0px #9d8993;
  -o-box-shadow: inset 0px 0px 10px 0px #9d8993;
  box-shadow: inset 0px 0px 10px 0px #9d8993; }
  .level-2:hover {
    border-color: #666666; }
  .level-2 .options {
    border-bottom: 1px solid #b9abb2; }
    
...

Grunt

  • Exécuteur de tâches écrit en JS.
  • Fonctionne avec NodeJS.
  • Permet de faire absolument tout!
  • 4403 tâches référencées sur gruntjs.com
  • Utile pendant le développement pour éxecuter les tâches répétitives et pour la production.
  • Paramètrage dans un fichier Gruntfile.js
  • Utilisation de Gulp également possible.

Tâches Grunt indispensables

grunt-contrib-concat

Concaténer des fichiers en un seul (JS,..).

grunt-contrib-copy

Copier des fichiers (fonts, templates,..).

grunt-ng-annotate

Réécrie toutes les injections de dépendance pour la minification.

app.controller('MainCtrl', function($scope, MyCustomService) {
  /* code du controlleur */
});


app.controller('MainCtrl', ['$scope', 'MyCustomService', function($scope, MyCustomService) {
  /* code du controlleur */
}]);


function MainCtrl($scope, MyCustomService) {
  /* code du controlleur */
}
MainCtrl.$inject = ['$scope', 'MyCustomService'];
app.controller('MainCtrl', MainCtrl);

grunt-contrib-uglify

Uglify un/des fichiers JS.

grunt-contrib-sass

Compile ses fichiers SASS en CSS.

grunt-contrib-cssmin

Minifie un/des CSS.

grunt-angular-templates

Minifie les templates HTML angular et les insert directement dans $templateCache.
Evite de nombreuses requêtes ajax.

app.run(["$templateCache", function($templateCache) {
  $templateCache.put("src/app/templates/page1.tpl.html",
    "

Page1: {{ myVar }}

" ); $templateCache.put("src/app/templates/page2.tpl.html", "

Page2: {{ myVar }}

" ); }]);

grunt-contrib-watch

Tourne en tâche de fond et exécute d'autres tâches quand certains fichiers sont modifiés.


Exemples d'utilisation classique:

  • Concaténer tous les fichiers JS dés qu'un fichier JS est modifié.
  • Générer un fichier CSS dés qu'un fichier SCSS est modifié.
  • ...

A vous de créer vos propres tâches qui sont des enchainements des autres tâches.

L'organisation

  • Un module pour chaque fonctionnalité.
  • Un fichier par 'élément' (controller, directive, filter,..).
  • Pour gros projet, utilisez la syntaxe à point:
    app.controller('app.library.PhotosListCtrl', function(...) {});
  • Commencez par créer toute ses tâches Grunt.
  • Utilisez angular.bootstrap() au lieu de ng-app pour faire un beau loading.