Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Angular 1: les services

Poster un commentaire

Les services sont des éléments clefs de l’architecture Angular et très peu d’applications peuvent s’en passer puisque $scope ou $rootScope en sont.

Pour l’essentiel, un service est un singleton. Il sera disponible partout où l’application en a besoin: contrôleur, directive, filtre… Il sert à implémenter la logique métier de l’application.

 

Il est important de bien comprendre la différence entre un service et un contrôleur.

Le service a en particulier vocation à renvoyer les données de la page et les persister d’une requête à l’autre. Ce travail n’a pas sa place dans le contrôleur qui est instancié uniquement lorsque c’est nécessaire via la directive ng-controller. En conséquence chaque fois que l’on change de route ou recharge la page le contrôleur sera réinitialisé.

Angular fournit un service de cache que l’on peut utiliser directement dans le contrôleur, mais dans l’idée des architectures MVC, il est sain de garder le contrôleur le plus léger possible. Donc le plus souvent on va plutôt créer un service personnalisé qui va savoir récupérer les données et ira peut-être les pousser dans le cache

 

Au programme:

  • Créer un service Angular
  • Etude d’un service natif: $http
  • Etude de $resource
  • Surcharge d’un contrôleur

Créer un service Angular

Notre démo est très simple:

<!DOCTYPE html>
<html ng-app="monApplication">
<head>
<script src="./lib/angular/angular.min.js"></script>
<script>
function monControlleur($scope) {
   $scope.animaux = [{
      famille: "Chiens",
      nom: "Cocker"
   }, {
      famille: "Chats",
      nom: "Gouttière"
   }, {
      famille: "Poissons",
      nom: "Daurade"
   }, {
      famille: "Oiseaux",
      nom: "Colombe"
   }];
}
 
var app = angular.module("monApplication", []);
app.controller("monControlleur", monControlleur);
</script>
</head>
 
<body ng-controller="monControlleur">
<table border="1">
<tr>
   <th>Famille</th>
   <th>Nom</th>
</tr>
 
<tr ng-repeat="animal in animaux">
   <td>{{animal.famille}}</td>
   <td>{{animal.nom}}</td>
</tr>
</table>
 
</body>
</html>

Qui affiche:

2016-03-30_22-59-23

On va s’intéresser à la partie du code qui charge les données du tableau. Elles sont codées en dur, mais dans une application réelle on interrogerait une base de données, un service web, un fichier… Dans tous les cas le code responsable de la recherche des données serait certainement isolé dans un service spécifique.

C’est le genre de tâche que nous pouvons réaliser avec un service Angular.

 

Pour la démo je vais simplifier, mais pour bien faire on devrait créer un fichier à part appelé dataService.js qui contient le code implémentant le service.

Transformons les choses pour avoir ceci:

function monControlleur($scope, dataService) {
   $scope.animaux = dataService.data;
};
 
var app = angular.module("monApplication", []);
 
app.controller("monControlleur", monControlleur);
app.factory('dataService', function () {
   return {
      data: [{
            famille: "Chiens",
            nom: "Cocker"
            }, {
            famille: "Chats",
            nom: "Gouttière"
            }, {
            famille: "Poissons",
            nom: "Daurade"
            }, {
            famille: "Oiseaux",
            nom: "Colombe",
            }]
      };
});

La partie HTML n’a pas changée, c’est le JavaScript qui évolue.

Tout d’abord la recherche des données a été déplacée dans une méthode anonyme. Cette dernière est injectée dans Angular via la méthode factory() du module. Le premier paramètre est le nom du service.

 

Note: Le préfixe $ est réservé aux services natifs d’Angular. Pour le code personnalisé il est conseillé d’utiliser une autre sémantique.

 

Factory() assure la persistance du service. Ce n’est pas le contrôleur qui assure cette fonction pour les raisons invoquées en introduction. Le contrôleur récupère le service non pas en l’instanciant, mais par injection:


function monControlleur($scope, dataService) {...}

 

Techniquement parlant, un service n’est donc pas très difficile à développer. Regardons le cas d’un service natif très utilisé: $http.

Etude d’un service natif: $http

Ce service permet d’effectuer des requêtes HTTP via des objets XHR ou JsonP. $http retourne une promesse, ce qui permet l’écriture de code asynchrone de façon plus moderne que les fonctions de rappel. Une promesse c’est la Task de C#.

On va repartir de l’exemple qui précède. Cette fois les données vont être réclamées à un web service plutôt qu’être codées en dur dans le service.

 

Commencez par créer un service web avec la techno de votre choix. En MVC 6 on pourra avoir ceci:

[Route("[controller]")]
public class AnimalsController : Controller
{
   [HttpGet]
   public JsonResult GetAnimals()
   {
      string json = @"[{
         ""famille"": ""Chiens"",
         ""nom"": ""Cocker""
      }, {
         ""famille"": ""Chats"",
         ""nom"": ""Gouttière""
      }, {
         ""famille"": ""Poissons"",
         ""nom"": ""Daurade""
      }, {
         ""famille"": ""Oiseaux"",
         ""nom"": ""Colombe""
      }]";
 
      return new JsonResult(json);
   }
}

On s’intéresse au côté client maintenant. On conserve la partie HTML, c’est le code JavaScript qui évolue. Le service $http a bien entendu une syntaxe détaillée ici:

https://docs.angularjs.org/api/ng/service/$http

 

Dans sa forme la plus simple:


$http({
   method: 'GET',
   url: '/<uneUrl>'
});

 

On passe en paramètre un objet qui expose deux propriétés :

  1. method
    Le verbe HTTP
  2. url
    L’url du service Http

 

Il existe une syntaxe permettant de récupérer la réponse avec des méthodes success() et error() de la façon suivante:

$http({ method: 'GET', url: "/uneUrl" })
   .success(function (response, status) {
      // traitement de la réponse
      })
   .error(function (response, status) {
      // traitement d'une erreur
      });

Success est appelée si l’appel HTTP a réussi. Error est appelé s’il a échoué.

 

Important: Selon la documentation ces méthodes sont dépréciées au profit de la méthode then(). Cette méthode est l’équivalent de ContinueWith dans C#. Il n’y a pas de pattern async/await actuellement en JavaScript. Pas que je sache en tout cas!

Vu la quantité de code « ancienne mode » que l’on rencontre, je vais démontrer les deux cas de figure.

 

Le code va ressembler à ceci:

var app = angular.module("monApplication", []);
 
function monControlleur($scope, dataService) {
   dataService.getData(function (response) {
         $scope.animaux = JSON.parse(response);
      });
};
app.controller("monControlleur", monControlleur);
 
app.factory('dataService', function ($http, $log) {
   return {
      getData: function (callBack) {
         $http({ method: 'GET', url: "/animals" })
            .success(function (response, status) {
                  callBack(response);
               })
            .error(function (response, status) {
                  $log.error(response, status)
               });
         } // getData
      }; // return
});

 

Le plus difficile est de s’y retrouver dans les parenthèses et les accolades. J’ai injecté des méthodes anonymes dans les constructeur, mais si l’application était plus ambitieuse on pourrait créer des méthodes privée intermédiaires comme je l’ai fais pour le contrôleur.

On retrouve la structure déjà vue pour construire un service. La différence est que le service ne renvoi pas un objet (data), mais une fonction getData. Cette méthode attend une fonction de rappel (callBack) qui va être invoquée en cas de succès. La fonction est injectée par le contrôleur.

CallBack est très simple: elle ne fait que construire le modèle:


$scope.animaux = JSON.parse(response);

J’ai injecté le service $log. C’est a priori console.log(), mais passer par un service le rend injectable contrairement à console qui est déclaré dans le scope global. C’est quelque chose que l’on retrouve à plusieurs reprises dans Angular, on a par exemple des services comme $window ou $location.

Avec les explications fournies je ne pense pas qu’il soit difficile de comprendre le code. Il doit retourner la même chose que dans le chapitre qui précède.

 

Essayons avec then(). Then déclare le code qui sera renvoyé lorsque $http aura reçu une réponse du service. Ce code est appelé de façon asynchrone.

La syntaxe générale est la suivante:


$http({ method: 'GET', url: "/uneUrl" })
   .then(function(response) {
         // succès
      })
   .catch(function(error) {
         // erreur
      })
   .finally(function() {
         // tout le temps
      });

 

Le code devient alors:

var app = angular.module("monApplication", []);
 
function monControlleur($scope, dataService, $log) {
   var promise = dataService.getData();
 
   promise.then(function (response) {
             $scope.animaux = JSON.parse(response.data);
      })
         .catch(function (error) {
            $log.error(error);
         });
      };
   app.controller("monControlleur", monControlleur);
 
   app.factory('dataService', function ($http) {
      return {
         getData: function () {
            return $http.get("/animals");
         }
      };
   });

La structure du code n’est pas bien différente. On a toujours le service qui encapsule l’appel au service via $http. Mais cette fois il ne retourne pas directement l’objet, mais une promesse. Le contrôleur se charge de gérer le retour. La suite du code est inchangée. Notez l’utilisation de la méthode utilitaire get(), que l’on aurait aussi pu utiliser dans l’exemple précédent. Get() implémente bien entendu une requête GET.

 

A ce stade, le contrôleur n’est pas très satisfaisant. Il est tout de même très compliqué puisqu’il prend en charge toute la logique d’appel asynchrone. Un contrôleur ça doit toujours être très simple. Refilons le boulot au service.

On a alors besoin d’une méthode de rappel comme dans le premier exemple du chapitre. Faite très attention aux erreurs de closure lorsque l’on écrit du code asynchrone.

 

var app = angular.module("monApplication", []);
 
function monControlleur($scope, dataService) {
      var promise = dataService.getData(function (response) {
      $scope.animaux = JSON.parse(response);
   });
};
app.controller("monControlleur", monControlleur);
 
app.factory('dataService', function ($http, $log) {
   return {
      getData: function (callBack) {
         // pour éviter une erreur de closure
         var cb = callBack;
         var promise = $http.get("/animals");
 
         promise.then(function (response, cb) {
            callBack(response.data);
         })
         .catch(function (error) {
            $log.error(error);
         });
      }
   };
});

 

Evidemment l’affichage reste inchangé. On a donc un code contrôleur qui ressemble tout de même plus à ce que l’on doit attendre d’un contrôleur. Un service c’est une façade. Sinon il perd un peu de son intérêt.

 

Note: à titre d’exercice, testez ce qui se passe avec l’erreur de closure pour le voir au moins une fois. Ce type de problème en Angular vient presque toujours d’un problème de closure ou de cycle digest qui se déclenche plus tôt que prévu (ou pas du tout).

 

Je ne vais pas aller plus loin dans la démonstration, mais sachez que $http abrite d’autres fonctionnalités:

  • Transformation de la requête et de la réponse
  • Cache
  • Intercepteurs
    Pour ajouter des traitements globaux à toutes les requêtes (authentification par exemple)
  • Timeout

Le service $resource

$resource est un service de la famille de $http (il est d’ailleurs construit sur ce service), mais de plus haut niveau. Il est spécialisé dans la communication REST.

$resource propose une syntaxe optimisée pour REST. Un exemple va clarifier tout cela.

Tout d’abord on va compléter le contrôleur avec cette action:

[HttpGet]
public string GetAnimals(string id)
{
   string json = @"{""id"":""" + id + @""",""famille"": ""Chiens"",""nom"": ""Cocker""}";
 
   return new json);
}

Ce n’est pas du code de haut vol, mais ce n’est pas le côté service qui est important dans cette démo.

NgResource ne fait pas partie d’Angular, on doit donc le déclarer dans le script:


<script src="angular-resource.js"></script>

 

L’injecter dans le module:


var app = angular.module("monApplication", ['ngResource']);

Et bien entendu dans le service à la place de $http.

On va donc interroger notre nouvelle action, l’url REST sera de la forme:

/animals/<id>

 

$resource est en principe plus simple que $http, mais c’est pourtant celle que j’ai eu le plus de mal à faire fonctionner! Je vais donc détailler un peu plus les choses.  La syntaxe générale est la suivante:

$resource(url, [paramDefaults], [actions]);

  • url:
    url REST paramétrisée (voir plus loin)
  • paramDefaults:
    Fourniture de valeurs par défaut pour les url paramétrisées
    Les autres valeurs seront ajoutées à la chaîne de requête
  • actions:
    Une fonction JavaScript pour lancer une tâche spécifique. $resource expose 5 actions HTTP par défaut:get (GET, récupère une valeur)
    query (GET, récupère une collection de valeurs)
    save (POST)
    delete (DELETE)
    remove (DELETE)actions permet d’ajouter sa propre action par défaut à la liste

La documentation est située ici:

https://docs.angularjs.org/api/ngResource/service/$resource

On remarquera un oubli surprenant: il n’y a pas d’action genre update correspondant au verbe PUT. Il faudra donc l’ajouter soi-même si l’on en a besoin. C’est bizarre.

Un dernier point, la syntaxe de ces méthodes:

  • Get: Resource.action([parameters], [success], [error])
  • Autres: Resource.action([parameters], postData, [success], [error])

Je pense que le nommage est clair.

 

Nous sommes armés pour faire chauffer du code….

La parti HTML:

<body ng-controller="monControlleur">
<form name="recherche">
   <input placeholder="Id d'un animal" ng-model="animalId"/>
   <button type="submit" ng-click="search(animalId)">Chercher</button>
</form>
 
<table border="1">
   <tr>
   <th>Id</th>
   <th>Famille</th>
   <th>Nom</th>
   </tr>
    
   <tr>
   <td>{{animal.id}}</td>
   <td>{{animal.famille}}</td>
   <td>{{animal.nom}}</td>
   </tr>
</table>
 
</body>

Au démarrage il s’affiche:

2016-04-08_19-34-35

On saisit un id dans le formulaire de recherche et il s’affiche:

2016-04-08_19-35-35

Du moins si vous avez ajouté le code suivant:

var app = angular.module("monApplication", ['ngResource']);
 
function monControlleur($scope, dataService) {
   $scope.search = function (idAnimal) {
      if (!idAnimal) {
         return;
      }
 
      $scope.animal = dataService.getAnimal(idAnimal);
   };
};
app.controller("monControlleur", monControlleur);
 
app.factory('dataService', function ($resource, $log) {
   return {
      getAnimal: function (animalId) {
 
      // création d'une instance de la classe resource appelée 'ressource'
      var resource = $resource('/animals/:animalId', {animalId:'@Id'});
      // récupération de la ressource
      var animal = resource.get({ id: animalId }, null, function (getResponseHeaders) {
      $log.error(getResponseHeaders);
});
 
      return animal;
   }
};
});

L’appel REST a lieu dans la méthode search() déclarée dans le scope via la méthode getAnimal() du service. Allons dans cette méthode.

Notez la façon dont l’url est paramétrisée. Le paramètre est animalId et doit être préfixé par deux points (:):


var resource = $resource('/animals/:animalId', {animalId:'@Id'});

La requête proprement dite est lancée par la méthode native get() qui reçoit en paramètre la valeur pour l’id et une méthode d’erreur. On retourne le résultat.

Le truc qui m’a beaucoup troublé est ce que retourne get justement. Je m’attendais à voir l’objet émit par le service web, pas du tout. Il s’agit de l’instance de resource qui a entre autres paramètres ceux de la classe animal. L’instance expose également les méthodes non GET préfixées par $, par exemple $save(). La syntaxe générale est la suivante:

instance.$action([parameters], [success], [error])

Notez bien la présence de $.

 

Cela permet de faire facilement du CRUD: Je modifie mon objet et j’appelle sa méthode $save() pour le POSTer vers le serveur qui se charge d’enregistrer les modifications. C’est plutôt pratique.

 

Une alternative à l’exemple est d’utiliser les promises comme on à fait avec $http. Il suffit d’invoquer $promise qui retourne une promesse, vous trouverez une démonstration dans la documentation.

Surcharge d’un contrôleur

On a déjà indiqué que l’on doit éviter de nommer des services personnalisés avec le préfixe $ afin de ne pas risquer de surcharger un service natif.

Il y a une exception, c’est $exceptionHandler pour lequel c’est précisément la pratique courante. Voyons donc comment on fait. La documentation officielle est ici:

https://docs.angularjs.org/api/ng/service/$exceptionHandler

 

Ce service a pour tâche de traiter toutes les exceptions non interceptées. L’implémentation par défaut se résume à:


$log.error(...);

On peut avoir besoin de personnaliser ce comportement, par exemple loguer les exceptions ou modifier la façon dont s’effectue l’affichage.

Voici une démonstration:

var app = angular.module("monApplication",[]);
 
function monControlleur($scope) {
   throw {message: 'Lever une exception'};
};
app.controller("monControlleur", monControlleur);
 
app.factory('$exceptionHandler', function ($log) {
   return function (exception, cause) {
      $log.error('Une grosse boulette=>' + exception);
   };
});

La surcharge se fait exactement comme si on créait un service $exceptionHandler. Le code est très modeste car il se contente de modifier le message, mais cela suffit à notre démo.

Le plus simple est de lever une exception depuis le contrôleur et constater dans la console que l’on a bien le résultat attendu:

2016-04-08_22-39-44

Bibliographie

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s