Archives mensuelles : juin 2015

ES6 : quelques nouveautés de la prochaine version de JavaScript – Part III

Cet article est la suite d’une série d’articles sur ES6 :

Avant de commencer, une petite information en terme de nommage, ES6 a été renommé en ES2015.

Promise

Définissons tout d’abord ce qu’est une promesse :
Une promesse représente l’éventuel résultat d’une opération asynchrone

La notion de promise (promesse en français) n’est pas nouvelle et existe dans différentes librairies sous différentes formes comme :

Les Promise seront inclus de manière native dans ES6.

Les promise permettent de gérer plus facilement les méthodes asynchrones et d’éviter un code difficile à lire et maintenable si on utilise des callback. En effet on arrive facilement à avoir du code comme cela :

step1(function (value1) {
    step2(function (value2) {
        step3(function (value3) {
            step4(function (value4) {
                step5(function (value5) {
                    step6(function (value6) {
                        // Do something with value6
                    });
                });
            });
        });
    });
});

C’est ce qu’on appelle pyramid of doom ou callback hell.

Un nouvel objet Promise est introduit dans ES6. Le constructeur prend en paramètre une fonction avec 2 fonctions en paramètre : resolved et reject. La fonction resolve est à appeler si il n’y a pas eu d’erreur avec les valeurs de retour. Elle permet d’indiquer que la promise est résolue permettant la suite des actions. La fonction reject est à appeler si une erreur est survenue.

Création d’une Promise

    var promise = new Promise(
        function (resolve, reject) { // (A)
            ...
            if (...) {
                resolve(value); // success
            } else {
                reject(reason); // failure
            }
        });

Une fois notre promise définie, nous allons pouvoir définir un handler sur cette promise afin de réaliser des actions sur la future valeur. L’enchaînement des actions se fait via la méthode then. Cette fonction prend en paramètre 2 fonctions: la 1ere pour le succès (resolve) et la deuxième lors d’une erreur (reject). Les erreurs peuvent être également gérés via la méthode catch.

Utilisation d’une promise

 httpGet('http://myServer.com/file.txt')
    .then(
        function (value) { //Success
            console.log('Contents: ' + value);
        },
        function (reason) { //Error
            console.error('An error occurred', reason);
        });

L’avantage des promises est qu’on peut facilement les chaîner, ou les composer.

Fetch, la nouvelle API pour faire des appels Ajax utilise les promises. (un article pour aller plus loin)

Il existe des polyfills afin de pouvoir les utiliser dès maintenant :

Quelques liens sur le sujet :

Destructuring

Le destructuring permet d’initialiser des variables en les extrayant à partir d’objet existant.

//Array : on extrait 3 variables m, d et y à partir d'un tableau
var [m, d, y] = [12, 21, 1981]; 

//object : on extrait les propriétés d'un objet
//Ici on définit une fonction qui extrait la liste des classe CSS d'un élément
var listOfCLass = function ({ classList } ) {
    Array.from(classList).forEach( (item) => console.log(item))
};
listOfCLass(document.body);

Une autre utilisation avec un module quand on ne veut importer qu’une partie d’un module. (cf chapitre module dans la partie II).

Quelques liens :

ES7

La future version d’EcmaScript est déjà en préparation et je vais parler de 2 des nouveautés qui sont à mes yeux les plus intéressantes.

Object.observe

Object.observe() est une nouvelle API qui permet de s’abonner aux changements effectués sur un objet. A chaque modification sur l’objet, un événement sera lancé contenant la liste des changements (ajout d’une propriété, modification d’une valeur, suppression d’une propriété) sur l’objet.

Voici un exemple tirée de la documentation de Object.observe sur MDN :

var obj = {
  foo: 0,
  bar: 1
};

Object.observe(obj, function(changes) {
  console.log(changes);
});

obj.baz = 2;
// [{name: 'baz', object: <obj>, type: 'add'}]

obj.foo = 'hello';
// [{name: 'foo', object: <obj>, type: 'update', oldValue: 0}]

delete obj.baz;
// [{name: 'baz', object: <obj>, type: 'delete', oldValue: 2}]

Cela permet notamment de facilement mettre en place du data-binding (Angular 2 l’utilise afin d’améliorer les performances et éviter le « dirty checking »).

Quelques liens sur le sujet :

Async/Await

ES6 simplifie, via l’introduction des promesses, grandement la gestion de code asynchrone. Async va encore plus loin afin de faciliter la lecture et la compréhension.
ES7 introduit 2 nouveaux mots clés : async et await.

async permet de définir une fonction comme étant asynchrone alors que await permet d’indiquer que l’on souhaite bloquer l’exécution du code jusqu’au retour de la fonction.

Voici un exemple d’utilisation :

async function getUserName() {
    return 'royto';
}

async function displayLogoutMessage() {
    var user = await getUserName();
    alert(`GoodBye ${user} !`);
}

Pour ceux qui viennent du monde .NET cette syntaxe ne devrait pas les surprendre 🙂

Quelques liens :

Liens sur ES6 / ES2015

Angular : Directive avec controller as, bindToController et tests

Une des bonnes pratiques promue par les experts angular est l’utilisation des controller en utilisant la syntaxe ‘controller as’ (des liens sur le sujet en bas de l’article).
En effet, cette pratique permet d’éviter certains écueils lié au prototypage JavaScript et permet de ne plus se coupler au scope.
La notion de scope n’existera plus dans angular 2 (de même que les controller d’ailleurs …) et il est donc conseillé de limiter son utilisation afin de faciliter une migration.

Les équipes d’angular travaillent sur les versions 1.x afin de faciliter la migration et contiennent donc des améliorations pour cela.

La version 1.3 a introduit la propriété bindToController qui permet d’automatiquement de rendre disponible les propriétés déclarés sur le scope dans le controller d’une directive.

La version 1.4 permet de définir directement sur bindToController les propriétés comme on le faisait sur le scope.

Directive avec controller as

Voici un exemple d’une directive tirée de mon application de volley ou l’on déclare une directive avec un controller en utilisant la syntaxe controller as et bindToDirective.

function matchScore() {
  return {
    templateUrl : 'views/directives/match-score.html',
    restrict : 'E',
    scope : true,
    bindToController : {
        match : '='
    },
    controllerAs : 'matchScore',
    controller : function () {
      return {
        scoreSets : function (set, team) {
          return this.match.score[set]
	    .filter(val => val === team)
            .length;
        }
      }
    }
  }
}

angular.module('volleyApp').directive('matchScore', matchScore());

Dans le vue de notre directive, il faut alors utiliser matchScore comme préfixe pour nos bindings.

<div class="list-group-item-text">
    <table class="table table-bordered score">
    <tbody>
    <tr>
        <th>Team</th>
        <th>Set 1</th>
        <th>Set 2</th>
        <th>Set 3</th>
        <th>Set 4</th>
        <th>Set 5</th>
    </tr>
    <tr id="scoreTeam1">
        <th><span id="team1Name">{{matchScore.match.teams.team1}}</span></th>
        <td class="set1"><span>{{matchScore.scoreSets(0, 1)}}</span></td>
        <td class="set2"><span>{{matchScore.scoreSets(1, 1)}}</span></td>
        <td class="set3"><span>{{matchScore.scoreSets(2, 1)}}</span></td>
        <td class="set4"><span>{{matchScore.scoreSets(3, 1)}}</span></td>
        <td class="set5"><span>{{matchScore.scoreSets(4, 1)}}</span></td>
    </tr>
    <tr id="scoreTeam2">
        <th><span id="team1Name"></span>{{matchScore.match.teams.team2}}</th>
        <td class="set1"><span>{{matchScore.scoreSets(0, 2)}}</span></td>
        <td class="set2"><span>{{matchScore.scoreSets(1, 2)}}</span></td>
        <td class="set3"><span>{{matchScore.scoreSets(2, 2)}}</span></td>
        <td class="set4"><span>{{matchScore.scoreSets(3, 2)}}</span></td>
        <td class="set5"><span>{{matchScore.scoreSets(4, 2)}}</span></td>
    </tr>
    </tbody>
</table>

Tests

Si on utilise pas controller as, les méthodes du controller sont accessible via le scope.

Si on utilise controllerAs, nous n’avons plus de scope pour accéder aux méthodes de notre controller. Comment alors tester nos méthodes de controller ?

Angular fournit des méthodes additionnelles à element (élément jQuery/jqLite) et fournit notamment une méthode element.controller qui permet de récupérer un controller. Il faut l’appeler avec le nom de la directive (et non par celui définit via controller as).

//on récupère le controller via element 
var ctrl = element.controller('matchScore');

//On teste nos méthodes
expect(ctrl.scoreSets(0, 1)).toBe(25);

Liens

JavaScript : Utiliser ES6 maintenant avec Babel

Je vous ai parlé dans des articles précédent de la nouvelle version de JavaScript, ES6/ES2015, Harmony.
J’avais évoqué des outils afin de pouvoir utiliser ces nouveautés dés maintenant, malgré le support partiel dans les navigateurs ou dans de vieux navigateurs.

Babel est transpileur qui va transformer du code ES6 en code compatible ES5 (version compatible dans la plupart des navigateur (IE ….). Babel est la fusion de 2 projets : 6to5 et esnext.

Il supporte l’ensemble des nouveautés de ES6 et même certaines fonctionnalités de ES7. Il a l’avantage de produire un code compréhensible et ne nécessite pas l’inclusion d’un script additionnel dans votre page (comme traceur, un autre transpileur).

Babel est basé sur node et il existe des plugins pour la plupart des task runner JavaScript comme Grunt, Gulp, …

Voyons comment l’utiliser avec grunt.

Installation et utilisation

Tout d’abord, installons le package grunt pour babel. L’installation se fait via la commande :

npm install grunt-babel --save-dev

Nous définissons une tache nommée babel dans notre Gruntfile.js, qui va prendre nos fichiers écrit en ES6 et les transpiler en ES5.

J’ai configuré une tache avec 2 configurations, une pour le dev et une pour générer un package, dist.

 //transpilation to ES5
  babel: {
    options: {
      sourceMap: true,
      blacklist: ["strict"]
    },
    dev: {
      files: [{
        expand : true,
        cwd: '<%= yeoman.app %>/scripts/',
        src: ['**/*.js'],
        dest: '<%= yeoman.dist %>/scripts/',
        ext: '.js'
      }]
    },
    dist: {
      files: [{
        expand : true,
        cwd: '<%= yeoman.tmp %>/concat/scripts',
        src: '*.js',
        dest: '<%= yeoman.tmp %>/concat/scripts',
        ext: '.js'
      }]
    }
  }

Il est possible d’activer des options. Dans mon cas, j’ai activé les sourcesmaps et supprimé l’option strict qui rajoute les « use strict » en début de fichier (ils sont déjà présent dans mes fichiers).

Il nous faut maintenant l’inclure dans nos taches grunt. Je l’inclus dans ma tache serve qui permet d’avoir un serveur web.

  grunt.registerTask('serve', function (target) {
    if (target === 'dist') {
      return grunt.task.run(['build', 'connect:dist:keepalive']);
    }

    grunt.task.run([
      'clean:server',
      'bowerInstall',
      'concurrent:server',
      'autoprefixer',
      'copy:dist',
      'babel:dev',
      'connect:livereload',
      'watch'
    ]);
  });

Bien entendu, nous pouvons configurer grunt pour transpiler notre code à la volée et recharger la page via watch et livereload pour un process de développement plus fluide :

 watch: {
  js: {
    files: ['<%= yeoman.app %>/scripts/{,*/}*.js'],
    tasks: ['newer:eslint:all', 'newer:babel:dev'],
    options: {
      livereload: true
    }
  },
  //others file types ...
}

Exemple de Code généré

Voici du code ES6 que nous allons transpiler avec babel (let, expression lamda, string templates, classes)

//Exemple avec une expression lambda
var double = x => x * 2;

//string template
var deux = 2;
let quatre = double(deux);
console.log(`${quatre} est le double de 
            ${deux}`);

//Exemple avec une classe
class Person {
  constructor(firstname, lastname) {
    this.firstname = firstname;
    this.lastname = lastname;
  }
  fullname() {
    return this.firstname + ' ' + this.lastname; 
  }
}

let julien = new Person('Julien', 'Roy');
console.log(julien.fullname());

Et voici le code généré par Babel

//Exemple avec une expression lambda
'use strict';

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

var double = function double(x) {
  return x * 2;
};

//string template
var deux = 2;
var quatre = double(deux);
console.log('' + quatre + ' est le double de \n            ' + deux);

//Exemple avec une classe

var Person = (function () {
  function Person(firstname, lastname) {
    _classCallCheck(this, Person);

    this.firstname = firstname;
    this.lastname = lastname;
  }

  _createClass(Person, [{
    key: 'fullname',
    value: function fullname() {
      return this.firstname + ' ' + this.lastname;
    }
  }]);

  return Person;
})();

var julien = new Person('Julien', 'Roy');
console.log(julien.fullname());

Liens

  • REPL: une page qui permet de tester la conversion de votre code