Taille de texte par Defaut
Agrandir la Taille du Texte
Réduire la Taille du Texte
Options d'accessibilité
Informatique

Contrôleur Angular 1 vers Composant Angular 2

Romain
27 octobre 2015
Pas de commentaires

Depuis la ng-conf d’octobre 2014 et le fameux « rip-rip-rip » (scope, controllers, module…) tout le monde se demande si AngularJs 1 vaut encore le coup, s’il faut attendre la sortie du 2 ou tout simplement migrer vers React. La question est mal choisie ! Demandez-vous plutôt comment faire en sorte que votre appli angular 1 passe les années sans (trop) dépérir.

rip-rip-ripOn oublie souvent qu’AngularJs 1 a été créé dans les années 2009, qu’à ce moment là il n’y avait pas d’ES6, pas de web-worker et encore moins d’HTTP2. Qu’il a dû souffrir de rétrocompatibilité et donc de choix techniques maintenant dépassés, mais qui doivent encore subsister pour ceux qui ne veulent pas changer leur code.

Bref, je fais partie de ceux qui pensent qu’il est nécessaire que Google fasse table rase et qu’on reparte sur de bonnes bases. Imaginez un framework où les nombreux hacks ne sont plus nécessaires. Où changer une brique est très facile. Où faire du rendu server-side ou webworker-side est possible sans se prendre la tête. Ce sont les promesses de la team d’Angular et ils sont en bonne voie pour y arriver.

Mais alors, comment faire avec notre code, tout frais d’il y a moins de 6 mois et pourtant déjà legacy ? La réponse est dans l’update ! Si vous avez complètement manqué le train de l’update incrémental, voici un petit résumé, étape par étape, pour transformer vos bons vieux controlleurs en directive, puis en directive pouvant mimiquer un composant Angular 2. Enfin je vais vous montrer à quoi ça va ressembler dans la future version majeure d’Angular.

Pour ce faire nous allons suivre l’évolution d’une petite application affichant une liste de conférences IT en France (non-exhaustive). Vous passerez rapidement sur le code de l’application : le but ici n’est pas de discuter des bonnes pratiques d’Angular, du web ou du css.

Au commencement il y avait les controllers

Je ne sais pas pour vous, mais quand j’ai découvert Angular, j’ai appris les contrôleurs. Ces choses « magiques » qui permettent de découpler le rendu de notre vue de son code métier. C’était parfait, mais (car il y a un mais) mes contrôleurs sont devenus énormes.

C’est ainsi que mon application Angular commença à ralentir, doucement mais sûrement. Mon code était de plus en plus difficile à maintenir. Le refactoring d’une vue devenait un vrai fardeau. Parfois toucher à une vue en faisait crasher une autre, à un autre endroit de mon application. Si tous ces comportements vous paraissent familiers, continuez à lire, nous allons voir comment j’ai réglé le problème.

Le 2ème jour vinrent les services

C’est alors que je commence à découvrir les services d’Angular.

En plus de me permettre de découper plus proprement mon code (donc les parties métiers des parties purement visuelles), les services m’offrent la possibilité de réutiliser ce code. En effet, ce sont des singletons, et si je veux accéder à mes conférences dans une autre partie de mon application, il me suffit de m’injecter ce service. De plus ces services me permettent d’imaginer des moyens de communication inter-vues, de manière élégante et super facile à implémenter. Par exemple, voici un petit pattern event qui prévient mes autres vues qu’une conférence a été ajoutée :

Mais tous ces patterns montrent vite leurs limites. Mes contrôleurs et leurs services ont commencé à dépendre de beaucoup de choses : pour faire communiquer plusieurs vues entre elles j’ai dû recourir à ce qui s’apparente à de la sorcellerie : $scope.$emit, $scope.$watch et $scope.$apply.

Bref, j’ai rapidement senti qu’il me manquait quelque chose, quelque chose qui me rapproche du DOM et qui me permette de faire évoluer plus en douceur mon code.

Et le 7ème jour, les directives sont apparues

Si vous n’êtes pas familiers des directives et de la philosophie 0-Controller, je ne peux que vous conseiller l’excellentissime article de Marine sur le sujet.

Mais tandis que mon DOM devenait bien plus simple et bien plus lisible, une partie était toujours autant difficile à percevoir. Par où mes données transitent ? Comment faire pour expliciter leur transit et leur passage inter-directives ?

Le code intéressant est celui de la directive « confDashboard ».

app.directive('confDashboard', [
	'LittleSrv',
    function (LittleSrv) {
        return {
            restrict: 'E',
            template: ''+
            	'',
            scope: {},
            link: function ($scope) {
                $scope.confs = LittleSrv.getConfs();
                $scope.addConf = function(newConf) {
                    LittleSrv.addConf(newConf);
                };
            }
        };
    }
]);

En un regard sur le template je comprends les entrées/sorties de mes directives. Ma « CountDirective » prend un tableau de conférences tout comme « LittleDirective ». De plus, la deuxième directive me préviendra lorsqu’une nouvelle conférence sera ajoutée. Je n’aurai alors qu’à mettre à jour ma collection de conférences, et le double-binding d’angular mettra à jour la « CountDirective ».

On commence ici à se rapprocher de la philosophie des composants. Ma directive LittleDirective n’a pas à savoir ce que je veux faire de la nouvelle conférence. Ici je l’ajoute simplement dans un tableau, mais je pourrais l’envoyer à un serveur. Ou la poser dans un localStorage. Je pourrais également la faire valider avant de l’envoyer. Tous ces traitements n’ont rien à faire dans le composant qui a la responsabilité d’afficher la liste des confs et d’en ajouter une nouvelle.

Pendant ce temps, ControllerAs fait son apparition

En parallèle de mes réflexions et de mes implémentations, émerge un nouveau concept : ControllerAs. Si vous en lisez plus sur le net à son propos, vous verrez qu’il permet tout simplement de nommer nos contrôleurs afin de s’y retrouver plus facilement dans le DOM. Mais il a un deuxième énorme avantage à mon sens, permettre de ne plus utiliser « $scope » dans nos fonctions.

Vous voyez où je veux en venir ? Non ? Jetons un coup d’œil à Angular2.

Et le futur frappa à ma porte

J’ai implémenté notre petite application en Angular2 avec TypeScript.

Attention, tout le code ci-dessous a été réalisé dans wordpress, je ne garantis donc pas qu’il tourne. Le but est de montrer vers quoi nous devrons aller et non pas une application angular2 en tant que telle (ne vous inquiétez pas ça viendra sûrement).

Je fais partie de ceux qui pensent qu’ES6 et TypeScript sont de bonnes alternatives pour des entreprises qui ont des développeurs hétérogènes (en terme d’expérience mais aussi et surtout en terme de langages informatiques). Si vous faites partie des puristes qui pensent que cela défigure complètement ce magnifique langage qu’est le javascript, je vous comprends. Mais pensez que dans une entreprise et spécialement chez un éditeur (comme ici chez iTK) ce qui compte c’est la pérennité du code. Après 5 ans de développement, je peux vous garantir que rentrer dans un projet vanilla-js, ça fait mal à la tête…

//app.ts
import {Component, View, bootstrap} from 'angular2/angular2';
import {LittleSrv} from './LittleSrv';
import {LittleComponent} from './LittleComponent';

@Component({
  selector: 'confDashboard'
})
@View({
  template: ''+
             '',
  directives: [LittleComponent]
})
class ConfDashboardComponent {
  confs: Array;
  littleSrv: LittleSrv;

  constructor(littleSrv: LittleSrv) {
    this.littleSrv = littleSrv;
    this.confs = littleSrv.getConfs();
  }

  addConf(newConf) {
    this.littleSrv.addConf(newConf);
  }
}

bootstrap(MyAppComponent, [LittleComponent]);
//LittleSrv.ts
import {Injectable} from 'angular2/angular2';

@Injectable()
class LittleSrv {
  confs: Array = data;

  getConfs() {
    return this.confs;
  }

  addConf(newConf) {
    this.confs.push(newConf);
  }
}
//littleComponent.ts
import {Component, View, EventEmitter, NgFor} from 'angular2/angular2';

@Component({
  selector: 'littleComponent',
  inputs: ['confs'],
  outputs: ['onAddConf']
})
@View({
  template: ''+
             '
    '+ '
  • '+ ' '+ ''+ ''+ ''+ ''+ '
  • '+ '
', directives: [NgFor] }) class LittleComponent { confs: Array; newConf: any; onAddConf: EventEmitter = new EventEmitter(); addNewConf(newConf) { this.onAddConf.next(newConf); } }

Pour les plus attentifs d’entre vous, je n’ai pas implémenté le CounterComponent pour me concentrer sur le reste. Mais l’implémentation devrait ressembler très fortement à celle de LittleComponent. De plus je n’ai pas fait de type Conf à part entière, j’ai préféré utiliser any qui correspond grosso-modo à un object js : {}. Car ce n’est pas un article sur TypeScript mais bel et bien sur le passage d’Angular1 à Angular2.

Il faut savoir qu’en Angular2 nous pourrons taper notre code en ES5 (mais il ne faut pas avoir peur du nombre de lignes de code purement technique :D), ES6, TypeScript et Dart. Faites votre choix !

Les changements tu retiendras

Au final qu’avons nous changé ?

DI

L’injection de dépendances est clairement différente. Nous sommes passés de :

//old school
angular.module('app').factory('name', ['dep1', 'dep2', function(dep1, dep2) {

}]);

pour aller vers quelque chose qui ressemblera à :

//es6 module
import {Dep1} from './dep1'; 
import {Dep2} from './dep2';

class ServiceName { 
  
  constructor(dep1: Dep1, dep2: Dep2) {}

}

De plus vous remarquerez que les services en eux mêmes ne se déclarent plus de la même façon. Maintenant ce sont de simples classes js.

Template

Alors là, je suis d’accord sur le fait que le boulot qu’on va devoir fournir pour passer à Angular2 sera énorme. Nous avons rajouté des « # », des « * », des « […] » ou encore des « (…) ». Au début c’est très déroutant, voire ça pique les yeux. Mais après l’avoir utilisé un mois ou deux, je vois clairement les avantages de ce nouveau templating.

En un clin d’œil je suis capable de voir les inputs « [name] » et les outputs « (name) » de mes compos. Les « # » me permettent de savoir quelles sont les variables créées par le dom pour le dom (contrairement aux variables du composant côté js). Et enfin les « * » me permettent de repérer facilement les directives components angular2 (NgFor, NgIf…).

En résumé, si vous suivez les étapes, à l’heure actuelle des choses, le plus gros effort restera le template.

Mais avec le fameux shadow-dom, d’autres goodies viennent s’ajouter : la portée du css restreinte au composant, la génération du dom dans un web-worker, la non re-copie des éléments d’un composant et j’en oublie certainement. Je pense que le prix en vaudra largement la chandelle.

Cerise sur le gâteau : le rendu est bien plus rapide qu’Angular1, la complexité du binding passe de O(n) à O(1), je peux vous assurer que la différence est clairement visible !

Component

Pour finir les directives deviennent de vrais composants. Si votre directive est gérée par un controllerAs vous ne devriez pas avoir de souci à la transposer en classe ES6 (et/ou typescript). Si elle ne contient pas de logique métier (tout doit être dans des services), tout devrait se faire assez rapidement. Il suffira de remplacer ce que j’appelle les « décorations » (e.g. angular.module().directive…) par de nouvelles décorations bien plus simples (e.g. @Component et @View).

Nous passerons ainsi de :

//old school
app.directive('confDashboard', [
	'LittleSrv',
    function (LittleSrv) {
        return {
            restrict: 'E',
            template: ''+
            	      '',
            scope: {},
            link: function ($scope) {
                $scope.confs = LittleSrv.getConfs();
                $scope.addConf = function(newConf) {
                    LittleSrv.addConf(newConf);
                };
            }
        };
    }
]);

pour finalement simplifier vers :

//angular componant
import {Component, View, bootstrap} from 'angular2/angular2';
import {LittleSrv] from './LittleSrv';
import {LittleComponent} from './LittleComponent';

@Component({
  selector: 'confDashboard'
})
@View({
  template: `
             `,
  directives: [LittleComponent]
})
class ConfDashboardComponent {
  confs: Array;
  littleSrv: LittleSrv;

  constructor(littleSrv: LittleSrv) {
    this.littleSrv = littleSrv;
    this.confs = littleSrv.getConfs();
  }

  addConf(newConf) {
    this.littleSrv.addConf(newConf);
  }
}

Bien plus encore

Il faudrait une série d’articles pour vous présenter en profondeur tous les changements. Je pense notamment au router, au form, à http ou encore aux nouvelles promises ES6. Nous en parlerons probablement dans un ou plusieurs futurs articles.

Mon article, mon avis

Vous l’aurez compris, je suis super emballé par cette nouvelle version d’Angular. Je sais qu’il va falloir fournir un immense travail pour emmener notre code Angular1 vers Angular2.

Ce qu’il faut absolument faire, c’est suivre les évolutions constantes d’Angular1. Mettre à jour son code au plus vite et adapter ses patterns pour qu’ils correspondent au mieux à la future version d’Angular.

Voici un exemple tout bête où l’on voit un développeur d’Angular demander d’ajouter dans Angular1 un nouveau type « component » pour petit à petit migrer vers Angular2 :

petebacondarwin_ask_for_component_in_angular1

Ça prouve que la team d’Angular veut aider les développeurs à converger d’une version vers l’autre. Mais il est clair que si vous ne faites pas l’effort de suivre, votre application deviendra très rapidement obsolète.

No Pain, No Gain

Commentaires 0

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.