|
Publié le par Chloé MAHALIN

Angular 4

Créer un service de requête injectable

Il est très rare de voir des site web ayant un client auto-porteur. En général, on développe un client en même temps qu'un serveur. Dans la même équipe. Oui oui, c'est mieux. Ca évite de se retrouver avec un service côté serveur qui ne correspond pas aux besoins de la vue côté client.

Donc nous allons voir dans cet article comment faire des SERVICES côté client ! Ces services, qui sont injectés dans les composants angular via la directive "providers", auront pour tâche de faire des requêtes et de traiter les réponses.

Faire un service unique

Si l'on a qu'un seul service, il va contenir, et les méthodes de requêtes, et le traitement des réponses. A la fin, ça ressemble à ceci :

myservice.ts

import { Injectable } from '@angular/core';
import { Http, Response, Headers, RequestOptions, URLSearchParams } from '@angular/http';

import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { Observable } from 'rxjs/Observable';

import { Entity } from '../entities/entity';

@Injectable()     // Cette classe doit pouvoir être injectée dans un composant. C'est ici qu'on le déclare.
export class MyService {

  private url = 'api/entity';  // URL to web API
  private http: Http;

  constructor (http: Http) {
    this.http = http;
  }

  getAll(): Observable<Entity[]> {         // Voici la démonstration d'une requête GET simple. Elle retourne une liste d'éléments qui seront
    const url = `${this.url}/getAll`;      // traités dans la méthode extractListFromResponse(res: Response)
    return this.http.get(url)
            .map(res => super.extractListFromResponse(res))
            .catch(super.handleError);
  }

  create(entity: Entity): Observable<Entity> {  // Voici une méthode POST. Les informations de l'entité sont données dans le body
    const url = `${this.url}/create`;           // de la requête. La réponse attendue est une seule entité.

    const headers = new Headers({ 'Content-Type': 'application/json' });
    const options = new RequestOptions({ headers: headers });

    return this.http.post(url, entity, options)
            .map(res => super.extractSingleEntityFromResponse(res))
            .catch(super.handleError);
  }

  protected extractSingleEntityFromResponse(res: Response): Entity {
    this.manageErrorCodes(res);

    if (res.json() != null) {
      return res.json() as Entity;              // Ceci est une mauvaise désérialisation. Je vous invite à lire l'article sur la 
    } else {                                    // sérialisation et la désérialisation.
      throw new Error(res.status + ' : Response is malformed : ' + res.text());
    }
  }

  protected extractListFromResponse(res: Response): Entity[] {
    this.manageErrorCodes(res);

    if (res.json() != null) {
      return res.json() as Entity[];
    } else {
      return new Array<Entity>();
    }
  }

  protected manageErrorCodes(res: Response): any {
    if (res.status == 203) {
      throw new Error(res.status + ' : This request has non authorized content : ' + res.text());
    } else if (res.status == 204) {
      throw new Error(res.status + ' : No content response : ' + res.text());
    } else if (res.status == 404) {
      throw new Error(res.status + ' : 404 not found : ' + res.text());
    }
  }

  protected handleError(error: Response | any): any {     // En cas d'erreur, le retour attendu est une exception avec un message.
    let errMsg: string;
    if (error instanceof Response) {
      errMsg = `${error.status} - ${error.statusText || ''} ${error}`;
    } else {
      errMsg = error.message ? error.message : error.toString();
    }
    return Observable.throw(errMsg);
  }

}

Le retour des méthodes get(url:sring) et post(url: string, entity: any, options: RequestOptions) est un observable. C'est une classe Angular, qui contient la référence vers la requête, mais pas la réponse. Cela permet des traitement asynchrones. Vous pouvez, par exemple, lancer le chargement d'une liste au démarrage de votre application sur le navigateur et n'afficher le contenu qu'une fois que votre serveur aura répondu. De cette manière, votre utilisateur est toujours en mesure de naviguer et de cliquer dans son navigateur et son expérience utilisateur est beaucoup plus fluide.

Mutualiser le code entre plusieurs services

Lorsque l'on traite plusieurs entités, il est souvent plus simple de mutualiser du code entre les différents services, notamment le traitement des responses, que l'on voudra aussi homogène que possible. Voici comment exploiter typescript dans cet objectif :

Définition de la classe abstraite parent :

abstractService.ts

import { Response } from '@angular/http';

import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import { Observable } from 'rxjs/Observable';

export abstract class AbstractService<T> {

  constructor() {
  }

  protected abstract convertToEntity(input: any): T;

  protected extractSingleEntityFromResponse(res: Response): T {
    this.manageErrorCodes(res);

    if (res.json() != null) {
      return this.convertToEntity(res.json());
    } else {
      throw new Error(res.status + ' : Response is malformed : ' + res.text());
    }
  }

  protected extractListFromResponse(res: Response): T[] {
    this.manageErrorCodes(res);

    if (res.json() != null) {
      const currentJsonString: Object[] = res.json();
      return currentJsonString.map(jsonObject => {return this.convertToEntity(jsonObject);});
    } else {
      return new Array<T>();
    }
  }

  protected manageErrorCodes(res: Response): any {
    if (res.status == 203) {
      throw new Error(res.status + ' : This request has non authorized content : ' + res.text());
    } else if (res.status == 204) {
      throw new Error(res.status + ' : No content response : ' + res.text());
    } else if (res.status == 404) {
      throw new Error(res.status + ' : 404 not found : ' + res.text());
    }
  }

  protected handleError(error: Response | any): any {
    let errMsg: string;
    if (error instanceof Response) {
      errMsg = `${error.status} - ${error.statusText || ''} ${error}`;
    } else {
      errMsg = error.message ? error.message : error.toString();
    }
    return Observable.throw(errMsg);
  }
}

Définition des services héritant de l'abstraction :

entity1Service.ts

import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions } from '@angular/http';

import 'rxjs/add/operator/map'
import 'rxjs/add/operator/catch';
import { Observable } from 'rxjs/Observable';

import { Entity1 } from '../entities/entity1';
import { Entity1Serializer } from '../serializer/entity1Serializer';

@Injectable()     // Cette classe doit pouvoir être injectée dans un composant. C'est ici qu'on le déclare.
export class MyService extends AbstractService<Entity1>{

  private url = 'api/entity1';  // URL to web API
  private http: Http;
  private Entity1Serializer serializer = new Entity1Serializer();

  constructor (http: Http) {
    this.http = http;
  }

  protected convertToEntity(input: any): Entity1 {
    return serializer.deserialize(input);               // Bonne sérialisation !!
  }

  getAll(): Observable<Entity1[]> {
    const url = `${this.url}/getAll`;
    return this.http.get(url)
            .map(res => super.extractListFromResponse(res))
            .catch(super.handleError);
  }

  create(entity: Entity1): Observable<Entity1> {
    const url = `${this.url}/create`;

    const headers = new Headers({ 'Content-Type': 'application/json' });
    const options = new RequestOptions({ headers: headers });

    return this.http.post(url, entity, options)
            .map(res => super.extractSingleEntityFromResponse(res))
            .catch(super.handleError);
  }
}

entity2Service.ts

import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions } from '@angular/http';

import 'rxjs/add/operator/map'
import 'rxjs/add/operator/catch';
import { Observable } from 'rxjs/Observable';

import { Entity2 } from '../entities/entity2';
import { Entity2Serializer } from '../serializer/entity2Serializer';

@Injectable()     // Cette classe doit pouvoir être injectée dans un composant. C'est ici qu'on le déclare.
export class MyService extends AbstractService<Entity2>{

  private url = 'api/entity2';  // URL to web API
  private http: Http;
  private Entity2Serializer serializer = new Entity2Serializer();

  constructor (http: Http) {
    this.http = http;
  }

  protected convertToEntity(input: any): Entity2 {
    return serializer.deserialize(input);               // Bonne sérialisation !!
  }

  getAll(): Observable<Entity2[]> {
    const url = `${this.url}/getAll`;
    return this.http.get(url)
            .map(res => super.extractListFromResponse(res))
            .catch(super.handleError);
  }

  create(entity: Entity2): Observable<Entity2> {
    const url = `${this.url}/create`;

    const headers = new Headers({ 'Content-Type': 'application/json' });
    const options = new RequestOptions({ headers: headers });

    return this.http.post(url, entity, options)
            .map(res => super.extractSingleEntityFromResponse(res))
            .catch(super.handleError);
  }
}

Cela permet de mutualiser les efforts, homogénéiser les comportements et de simplifier la lecture. Merci Typescript !

Pour en savoir plus sur la classe de serialisation, c'est ici.

Warning ! Attention, en cas d'héritage, l'appel d'une méthode abstraite dans une lambda est toujours un peu complexe. Typescript peut s'emmeler les pinceaux et 'this' risque de ne pas toujours être celui que vous imaginez. Exemple :

protected extractListFromResponse(res: Response): T[] {
    this.manageErrorCodes(res);

    if (res.json() != null) {
      const currentJsonString: Object[] = res.json();
      return currentJsonString.map(jsonObject => {return this.convertToEntity(jsonObject);});
    } else {
      return new Array<T>();
    }
  }

Dans ce cas, lors de l'appel à 'this.convertToEntity', 'this' est bien la classe qui contient la méthode convertToEntity.

Mais dans le cas ou j'écris ma lambda de cette manière :

protected extractListFromResponse(res: Response): T[] {
    this.manageErrorCodes(res);

    if (res.json() != null) {
      const currentJsonString: Object[] = res.json();
      return currentJsonString.map(this.convertToEntity);
    } else {
      return new Array<T>();
    }
  }

Alors, lors de l'appel à 'this.convertToEntity', 'this' représente la lambda initiée par la méthode map.