VueJS ReactJS et Angular 2

Editer

Comme beaucoup je travaille de plus en plus sur du JavaScript et l'utilisation d'un framework Frontend est devenue indispensable. Jusqu'à maintenant, j'utilisais AngularJS, mais, suite à quelques problèmes concernant son utilisation, je me suis dit qu'il serait intéressant d'apprendre un nouveau Framework, mais lequel choisir ?

Le problème d'AngularJS 1

On peut commencer par se demander : pourquoi changer ? AngularJS est un très bon framework, c'est indéniable, mais sa conception me pose plusieurs problèmes.

  • La conception sous forme de controllers montre rapidement ses limites. Faire communiquer les éléments ensemble force l'utilisation de controllers imbriqués ce qui devient rapidement un joyeux bordel.
  • La détection des changements à travers des watchers pose aussi des problèmes lorsque l'application grandit. Lorsqu'une action est effectuée au sein du framework, tous les watchers sont lancés en boucles.
  • Le $scope qui a parfois un comportement inattendu (il est difficile de savoir à quel scope une propriété est attachée surtout dans les directives).

Les composants Web

Même s’il existe une très grande variété de Framework Frontend ils sont tous d'accord sur la manière d'organiser les choses : Les composants web. L'idée est de concevoir notre application sous forme de blocs indépendants réutilisables et fonctionnant de manière isolée. Les composants peuvent être considérés comme des balises HTML améliorées

<imagepicker images="images" onselect="displayImage">

Si tout le monde est d'accord sur l'approche pourquoi existe-t-il autant de Frameworks ? La problématique reste toujours la même : on souhaite rendre nos composants réactifs. Lorsque l'état d'un composant change, l'interface de ce dernier doit changer. C'est sur la mise en place de cette réactivité que les Frameworks ne sont pas d'accord.

Angular 2

Je ne parlerais ici que de la partie composant du framework, car, comme son petit frère, AngularJS 2 intègre de nombreux outils supplémentaires comme un module HTTP ou un validateur de Formulaire.

Au premier abord, Angular 2 semble approcher le problème de la même manière qu'avant. Des watchers sont attachés à chaque composant et dès qu'une modification est faite ils vérifient si des modifications ont été apportées. Plusieurs changements permettent cependant d'améliorer les performances de cette méthode :

  • Angular 2 n'implémente pas le "two way data-binding" par défaut (sauf pour le ngModel évidemment). Lorsqu'une modification est apportée il est donc inutile de faire plusieurs cycles, on vérifie les changements du composant racine vers les composants enfants une seule fois.
  • Angular 2 supporte les objets immutables. Si un composant ne dépend que d'objets immutables, alors aucune vérification ne sera faite lors d'une modification.

Plus d'informations sur le détecteur de changements

L'autre gros changement par rapport aux autres frameworks est l'utilisation du TypeScript par défaut. Ce langage apporte de nouvelles fonctionnalités au langage JavaScript telles que des annotations, le typage des variables, et les interfaces. Même s’il est tout à fait possible d'utiliser du JavaScript ES5, le framework incite fortement l'utilisation de ce nouveau langage dans ses exemples. Ce qui peut représenter un frein pour la compréhension du framework (il faudra comprendre le TypeScript avant Angular 2). Malgré tout ce choix de langue permet de rendre l'organisation du code plus propre et d'éviter certaines erreurs en amont.

Avantages :

  • Un framework complet
  • Le TypeScript permet une meilleure organisation du code avec les interfaces, le typage et les annotations
  • Un getting started proposant une introduction simple à AngularJS2

Inconvénients :

  • Une documentation pas encore complète
  • Le TypeScript peut être déstabilisant et rend le framework plus complexe à appréhender
  • Beaucoup de concept à comprendre avant d'être efficace
  • en Béta

ReactJS

ReactJS est le framework Frontend utilisé par Facebook. Son approche du problème est assez particulière et plus "manuelle".

var HelloMessage = React.createClass({
  displayName: "HelloMessage",

  render: function render() {
    return React.createElement(
      "div",
      null,
      "Hello ",
      this.props.name
    );
  }
});

ReactDOM.render(React.createElement(HelloMessage, { name: "John" }), mountNode);

La première chose qui saute aux yeux est l'utilisation de React.createElement(). Contrairement aux autres frameworks présentés ici, React n'interagit pas directement avec le DOM, mais travaille avec un VirtualDOM qui est une représentation JavaScript du DOM réel. Pour simplifier le travail avec ce VirtualDOM React propose d'ailleurs une syntaxe supplémentaire : le JSX. Ce VirtualDOM permet de limiter les interactions avec le DOM réel lors des évolutions de notre interface. Lorsque notre composant va changer d'état, un nouveau VirtualDOM sera généré et comparé au VirtualDOM de l'état précédent. Le résultat de cette comparaison permet à React de modifier le DOM de la manière la plus optimale possible. Cette approche offre plusieurs avantages :

  • Les composants ReactJS peuvent être testés sans avoir besoin du navigateur, car nous n'interagissons pas directement avec le DOM
  • Le VirtualDOM peut être connecté à autre chose qu'un navigateur (par exemple on peut utiliser React pour créer du code mobile natif).

Maintenant comment rend-on notre composant réactif ?

var Timer = React.createClass({
  getInitialState: function() {
    return {secondsElapsed: 0};
  },
  tick: function() {
    this.setState({secondsElapsed: this.state.secondsElapsed + 1});
  },
  componentDidMount: function() {
    this.interval = setInterval(this.tick, 1000);
  },
  componentWillUnmount: function() {
    clearInterval(this.interval);
  },
  render: function() {
    return (
      <div>Seconds Elapsed: {this.state.secondsElapsed}</div>
    );
  }
});

ReactDOM.render(<Timer />, mountNode);

Les composants commencent par initialiser un "state" qui est un simple objet JavaScript représentant l'état interne de notre composant. Lorsque l'on souhaite faire évoluer notre composant, il faudra modifier le state de manière explicite à l'aide de la méthode setState() en lui passant le nouveau state de notre composant. Lorsque le state est modifié, le virtualDOM est regénéré et la différence est ensuite appliquée au DOM réel.

Même si le virtualDOM limite les interactions avec le DOM réel, il apporte aussi un gros problème de performances. À chaque modification du state, le virtualDOM doit être regénéré pour être comparé, ce qui peut rapidement devenir problématique avec une application utilisant des centaines de composants. Il faudra penser aux performances dès le début de la conception de l'application en précisant par exemple si un composant doit être recalculé. React intègre ainsi une méthode shouldComponentUpdate() qui permet d'éviter le recalcul du VirtualDOM suivant une condition précise

shouldComponentUpdate: function(nextProps, nextState) {
  return this.props.value !== nextProps.value;
}

Avantages :

  • Le VirtualDOM permet de travailler avec autre chose que le DOM
  • L'évolution du state se fait de manière explicite
  • Une approche tout JavaScript

Inconvénients :

  • Le VirtualDOM rend parfois l'interaction avec les éléments web difficiles
  • La regénération du VirtualDOM à surveiller de près, ne pas hésiter à utiliser shouldComponentUpdate()
  • Plus "verbeux" que les autres frameworks

VueJS

VueJS permet de résoudre le problème de la création de composants web réactif avec une approche qui ressemble beaucoup à l'approche proposée par AngularJS :

<div id="demo">
  <p>{{message}}</p>
  <input v-model="message">
</div>
<script>
var demo = new Vue({
  el: '#demo',
  data: {
    message: 'Hello Vue.js!'
  }
})
</script>

Contrairement à React, VueJS est lié au DOM et utilise des attributs HTML spéciaux pour rendre le DOM réactif. On peut alors se demander l'intérêt d'avoir un framework aussi proche de la philosophie d'AngularJS. Là où VueJS se démarque, c'est sur sa manière de rendre notre HTML dynamique. Quand un objet est passé à un composant, VueJS va parcourir toutes ses propriétés et les convertir en getter/setter. Lorsque l'on modifie une valeur au sein d'un composant this.timer += 1, VueJS va être capable de notifier directement les watchers correspondants à cette propriété. Cette approche permet d'avoir un concept proche d'AngularJS tout en évitant le problème posé par des milliers de watchers lancés en permanence. Malgré tout, cette solution n'est pas parfaite et il faut être au courant du fonctionnement interne de VueJS pour éviter de chercher pourquoi un composant ne s'actualise pas :

  • On ne peut pas setter une valeur en utilisant un index de tableau items[1] = 3 , il faudra alors utiliser une fonction de VueJS (vm.items.$set(1, 3))
  • Les propriétés ajoutées ou supprimées d'un objet ne seront pas trackées. Il faudra ici aussi utiliser la fonction $set.

N'hésitez pas à faire un tour sur la documentation pour avoir une explication en profondeur de ce fonctionnement.

Avantages :

  • Un framework très simple à prendre en main
  • Une documentation complète et bien écrite
  • Des outils permettant de simplifier la création de projets vue-cli

Inconvénients :

  • VueJS patche tous les objets pour observer les modifications, ce qui peut avoir des effets de bords indésirables (quid des performances sur des objets énormes)
  • On a besoin du DOM pour tester nos composants

Flux, le chaînon manquant

Cette nouvelle architecture en composant pose tout de même un problème : comment faire communiquer ensemble nos composants de manière globale. Par exemple, sur Facebook, lorsque l'on reçoit un nouveau message, une notification s'affiche à plusieurs endroits sur le site. Nos composants doivent donc partager une sorte de state global. Facebook propose une solution à cette problématique sous la forme d'un pattern : Flux. Le principe de ce pattern est le suivant.

Un store représente des données, par exemple une liste de todos

var TodosStore = {
    todos: []
};

Un composant va lancer une action, par exemple createNewTodo :

createNewTodo: function( evt ) {

    AppDispatcher.dispatch({
        type: 'TODO_ADD',
        todo: { name: 'Faire les courses' }
    });

}

Le store souscrit à une série d'actions et modifie son contenu en fonction de l'action appelée

var TodosStore = {
    todos: []
};

AppDispatcher.register( function( action ) {

    switch( action.type ) {

        case 'TODO_ADD':
            TodosStore.totos.push( action.todo );
            TodosStore.trigger( 'change' )
            break;
    }

}); 

Le store émet un évènement pour informer d'un changement.
Le composant souscrit à cet évènement et va se mettre à jour en fonction. La méthode de mise à jour dépendra du framework utilisé.

TodosStore.bind('change', function () {
    this.setState({todos: TodosStore.todos});
}); 

Il n'existe cependant pas de "framework Flux", mais plusieurs librairies proposent des implémentations plus ou moins proches de l'idée originale :

L'avis de la fin

Au final, aucun framework ne propose la solution ultime à la problématique de la réactivité. Chaque solution proposée apporte aussi son lot de problème. Au final, le choix du framework doit se faire en fonction de votre affinité envers la solution proposée. Cette affinité dépendra du développeur (quel niveau d'automatisation recherché, le nombre d'outils fournis...). La multitude de frameworks permet de combler ces différents besoins. J'espère que cet article vous aura permis d'y voir plus clair afin de faire votre choix de manière plus réfléchie.