AngularJS : services et communication avec un serveur

Le précédent article d’introduction a présenté les premières pistes essentielles pour développer une application avec AngularJS. Les aspects présentation et navigation ont été évoqués mais il reste un point important à aborder : la notion de service en général et la communication avec un serveur en particulier.

Les services

Un fichier de services a à peu près la même structure qu’un fichier de contrôleurs. Les mêmes remarques sont applicables : on peut n’avoir qu’un seul fichier de services (ici services.js) ou plusieurs suivant les besoins.

'use strict';

/* Services */ var varServices = angular.module('myServices', ['ngResource']); varServices.factory('FirstService', ['module1',function(module1){
    this.function1 = function () {
         return ...;
     };
     this.function2 = function () {
         return ...;
     };
     return this;
  }]);

On remarque ici qu’on passe ngResource en paramètre lors de la déclaration du module. Ce n’est pas une obligation, mais généralement, un service va chercher des données en interrogeant un serveur distant, d’où le ngResource qui contient le service $resource qui permet de faire des requêtes asynchrones. A noter que ngResource n’est pas déclaré dans app.js. Le déclarer ici permet de le charger en plus des autres modules de app.js.

Un service, semblable en cela aux services Java, contient généralement un ensemble de fonctions ou méthodes (si l’on veut utiliser une terminologie plus orientée objet) appelables depuis l’extérieur, dans notre cas, depuis un contrôleur.

D’où le return this du service et les différentes méthodes de la forme this.var = function ().

Si l’on veut appeler FirstService depuis un contrôleur, il faut d’abord le passer en paramètre à celui-ci. L’appel est alors de la forme :

$scope.dummy = FistService.function1();

Venons en maintenant à la partie la plus intéressante, à savoir l’appel à un serveur distant (à voir comme un web service au sens le plus large du terme). Deux services sont proposés par AngularJS :

  • $http qui est le service de bas niveau et qui présente l’inconvénient d’imposer des callbacks de retour, ce qui en pratique permet pas d’exploiter directement toute la puissance du binding. On lui préférera :
  • $resource (basé sur $http) qui est un service de plus haut niveau, totalement asynchrone et qui renvoie une promise. C’est lui qu’on utilisera dans la suite.

Une remarque avant de commencer : en théorie, rien n’empêche d’utiliser directement $http ou $resource dans un contrôleur (puisque ce sont eux-mêmes des services). Toutefois, les best practices d’AngularJS conseillent de placer $http ou $resource dans un service dédié et d’appeler celui-ci depuis le contrôleur.

A noter que par défaut, $http et $resource utilisent le format JSON pour représenter les données. Il est toujours possible d’utiliser d’autres formats ou protocoles (SOAP pour attaquer des web services au sens strict, par exemple), mais il faut mieux faire au plus simple : JSON fait partie de l’écosystème Javascript et est un format autrement plus concis que le SOAP/XML, par exemple. JSON apporte ainsi beaucoup de facilités, notamment pour le binding, les composants graphiques pouvant directement être liés aux données échangées via les services.

$resource propose en interne une série de méthodes (get, save, query, delete, etc) qui sont suffisantes dans le cas où l’on attaque un serveur full REST, par exemple en utilisant JAX-RS dans le cas d’un serveur JEE. Se reporter à l’API AngularJS pour tous les détails sur $resource.

Dans ce cas un service utilisant $resource se déclare simplement ainsi :

varServices.factory('Server', ['$resource',
                          function($resource){
                           return $resource('JsonServlet', {});
                          }]);

« JsonServlet » correspond dans cet exemple à une servlet chargée du service REST en question. Plus généralement, le paramètre utilisé ici est l’adresse du service (url relative) et le fait que ce soit une Servlet Java n’a pas d’importance du côté du client AngularJS pour peu que le service réponde tel qu’attendu.

Dans le cas où l’on n’a pas un serveur full REST, il est possible de définir ses propres méthodes pour $resource. Un service utilisant $resource se déclare alors ainsi :

varServices.factory('Server', ['$resource',
      function($resource){
         return $resource('JsonServlet', {}, {
             getCustomer : {method:'GET', params:{subCmd: 
                    'getCustomer'}, isArray:false},
             getProducts: {method:'GET', params:{subCmd; 
                    'getProducts'}, isArray:true}
        });
      }]);

On a bien un service qui propose plusieurs méthodes (getCustomer et getProducts). A noter que le return this est inutile puisqu’on retourne $resource lui-même.

Les paramètres à passer à $resource sont donc ici :

  • Une url pour attaquer les services JSON du serveur. A noter qu’il s’agit d’une sous-url, AngularJS est initialement téléchargé depuis une certaine url (http://myhost/myUrl par exemple) et pour des raisons de sécurité ne travaille que sur cette url « de base ». Ici on a pris comme sous-url une Servlet, mais ce n’est pas une obligation, évidemment.
  • Des paramètres à passer à l’url (sous forme JSON) qui s’ajouteront aux paramètres lors de l’appel des méthodes. En général, cette partie est vide.
  • Une liste de méthodes pouvant être appelées (sauf, évidemment si l’on décide d’utiliser les méthodes internes par défaut de $resource, voir plus haut). Ces méthodes elles-mêmes peuvent avoir plusieurs paramètres :
    • Type de la requête (GET, POST, DELETE, etc)
    • Des paramètres supplémentaires sous forme JSON (ici, il s’agit d’un identifiant permettant à la servlet de savoir quelle opération effectuer).
    • Un indicateur permettant de savoir si le retour de la requête est un Array ou un Objet (en Javascript, malgré sa plasticité, on ne peut pas caster un Array en Objet et réciproquement). Le défaut est false (Objet).

Maintenant voyons comment s’effectue l’appel depuis un contrôleur :

$scope.products = Server.getProducts({Id: $routeParams.Id});

Il faut bien voir que la requête effectuée par $resource est asynchrone. Ce que retourne la méthode, donc ce qui se retrouve dans $scope.products, est une promise, c’est à dire en substance un objet vide qui ne sera rempli qu’au retour de la requête. Comme on l’a vu, il n’y a pas besoin de s’en préoccuper, la magie du binding se chargera d’afficher la valeur de $scope.products dans la vue associée lorsque la promise sera enfin remplie.

C’est toujours cette façon de procéder qu’il faut privilégier, le binding étant l’un des fondements d’AngularJS.

Toutefois, il peut arriver qu’on ait des traitements à effectuer sur le retour d’une requête (par exemple, puisqu’ici le retour est un Array, on pourrait avoir envie de le trier). Mais comment le faire, puisque $scope.products ne contient rien ? Dans ce cas, il faut utiliser une fonction de callback qui sera appelée au moment du retour de la requête. Il existe deux callbacks que l’on peut passer à une méthode de $resource : une appelée en cas de succès, l’autre en cas d’échec (dans cet ordre). Dans l’exemple qui va suivre, on n’utilisera que la callback de succès pour ne pas compliquer le code.

$scope.products = Server.getProducts({d: $routeParams.Id},
            function(products) {
              if (products && products.$resolved) {
                $log.info("array length is : "+products.length);
              }
            });

Ici on trace simplement dans la console la longueur du tableau lorsqu’il est effectivement réceptionné dans la callback.

De fait, en pratique, il est conseillé de passer en paramètre une callback de succès, même vide, ne fut-ce que dans le cas où elle s’avérerait utile dans un stade ultérieur du développement.

Partie serveur JEE

Le serveur doit typiquement savoir implémenter des services sur le protocole HTTP. Avec un serveur Java, il peut s’agir de services REST purs et durs basés sur JAX-RS ou plus généralement de services portés par une ou plusieurs servlets. Ici, comme nous sortons du domaine d’AngularJS, nous ne donnerons que quelques conseils d’ordre général. L’utilitaire Jackson peut être employé pour gérer le flux JSON.

Jackson apporte en effet un mapper qui permet de transformer un flux JSON en objet Java (ou en tableau d’objets) et vice-versa. Comme souvent, avec les serveurs JEE, on utilise un ORM de type Hibernate. En théorie, il est possible d’utiliser le mapper pour transformer le JSON en Entities Hibernate, mais ce n’est pas conseillé : même en jouant sur la lazyness, on n’est jamais bien certain de ce que contiennent réellement les Entities. Il est plus judicieux de passer par des DTO créés à partir des Entities. Dozer est un bon outil pour générer ces DTO.

De plus avec les ORM, on a souvent des problèmes de références en boucle et dans ce cas Jackson part en boucle infinie et donc en erreur. Soit une Entity A qui contient un Set<> d’Entities B. Et B qui contient une référence à A. Une fois A et B transformés en DTOs, si on essaie de sérialiser A, Jackson va générer une erreur (A contient B qui contient A qui contient B etc). Si AngularJS n’a pas besoin de la liste des B, le mieux est de faire avant la sérialisation DTO-A.setDTO-Bs(null). Tout dépend des besoins du client AngularJS et il n’existe donc pas de stratégie a priori.

Dans l’ensemble, on se trouve ici confrontés aux questions classiques liées à la conception de services web et ce n’est pas propre à AngularJS mais plutôt au fait que le client se trouve totalement découplé du serveur.

 

Trucs et astuces , , ,