web-dev-qa-db-fra.com

Service générique dactylographié

Je suis nouveau sur TypeScript et angular2/4 et je construis une seule application qui a deux entités de base qui est Car et Driver et tout ce que je fais est de les répertorier avec un appel API.

Le problème auquel je suis confronté est que j'ai une redondance de code pour chaque CarService et DriverService, et je pourrais avoir le même code pour le service d'autres entités.

L'implémentation suit jusqu'à présent, en ignorant d'autres méthodes pour l'illustration:

@Injectable()
export class CarService  {

private actionUrl: string;
private headers: Headers;

constructor(private _http: Http, private _configuration: Configuration) {

    // Getting API URL and specify the root
    this.actionUrl = _configuration.serverWithApiUrl + 'Car/';

    this.headers = new Headers();
    this.headers.append('Content-Type', 'application/json');
    this.headers.append('Accept', 'application/json');
}

// Function to get all Cars - API CALL: /
public GetAll = (): Observable<Car[]> => {
    return this._http.get(this.actionUrl)
        .map((response: Response) => <Car[]>response.json())
        .catch(this.handleError);
}

// Function to get a Car by specific id - API CALL: /:id
public GetSingle = (id: number): Observable<Car> => {
    return this._http.get(this.actionUrl + id)
        .map((response: Response) => <Car>response.json())
        .catch(this.handleError);
}

// Function to add a Car - API CALL: /create
public Add = (newCar: Car): Observable<Car> => {
    return this._http.post(this.actionUrl + '/create', JSON.stringify(newCar), { headers: this.headers })
        .catch(this.handleError);
}

// Function to update a Car - API CALL: /
public Update = (id: number, CarToUpdate: Car): Observable<Car> => {
    return this._http.put(this.actionUrl + id, JSON.stringify(CarToUpdate), { headers: this.headers })
        .catch(this.handleError);
}

// Function to delete a Car - API CALL: /:id
public Delete = (id: number): Observable<Response> => {
    return this._http.delete(this.actionUrl + id)
        .catch(this.handleError);
}

// Function to throw errors
private handleError(error: Response) {
    console.error(error);
    return Observable.throw(error.json().error || 'Server error');
}

Ce qui ne change qu'avec DriverService, c'est le Car/ à la fin de l'URL et le type de données dans Observable<Car[]> et la réponse.

Je voudrais savoir quelle est la meilleure façon d'éviter cela avec un service générique et comment le faire en TypeScript.

23
Imad El Hitti

Vous pouvez créer une classe générique abstraite et deux classes enfants qui en héritent:

classe abstraite:

export abstract class AbstractRestService<T> {
  constructor(protected _http: Http, protected actionUrl:string){
  }

  getAll():Observable<T[]> {
    return this._http.get(this.actionUrl).map(resp=>resp.json() as T[]);
  }
  getOne(id:number):Observable<T> {
    return this._http.get(`${this.actionUrl}${id}`).map(resp=>resp.json() as T);
  }
} 

classe de service conducteur

@Injectable()
export class DriverService extends AbstractRestService<Driver> {
  constructor(http:Http,configuration:Configuration){
    super(http,configuration.serverWithApiUrl+"Driver/");
  }
}

classe de service de voiture

@Injectable()
export class CarService extends AbstractRestService<Car> {
  constructor(http:Http,configuration:Configuration) {
    super(http,configuration.serverWithApiUrl+"Car/");
  }
}

Notez que seules les classes concrètes sont marquées comme @Injectable() et doivent être déclarées à l'intérieur d'un module tandis que la abstraite ne doit pas l'être.

mise à jour pour Angular 4 +

Http classe étant dépréciée au profit de HttpClient, vous pouvez changer la classe abstraite en quelque chose comme ça:

export abstract class AbstractRestService<T> {
  constructor(protected _http: HttpClient, protected actionUrl:string){
  }

  getAll():Observable<T[]> {
    return this._http.get(this.actionUrl) as Observable<T[]>;
  }

  getOne(id:number):Observable<T> {
    return this._http.get(`${this.actionUrl}${id}`) as Observable<T>;
  }
} 
45
n00dl3

Ayez un service de base pour votre application.

Avec les méthodes getpost et delete avec votre base URL attaché.

export class HttpServiceBase {

    Host_AND_ENDPOINT_START : string = 'you/rD/efa/ult/Url' ;
    public getWebServiceDataWithPartialEndpoint(remainingEndpoint: string): Observable<Response> {

        if (!remainingEndpoint) {
            console.error('HttpServiceBase::getWebServiceDataWithPartialEndpoint - The supplied remainingEndpoint was invalid');
            console.dir(remainingEndpoint);
        }

        console.log('GET from : ' , this.Host_AND_ENDPOINT_START + remainingEndpoint);
        return this.http.get(
            this.Host_AND_ENDPOINT_START + remainingEndpoint

        );
    }

C'est une implémentation utile car elle vous permet de déboguer facilement les appels WS - tous les appels finissent par provenir de la base.

Host_AND_ENDPOINT_START peut être remplacé par n'importe quel module que vous souhaitez étendre le service de base.

Imaginons que votre point de terminaison ressemble à quelque chose comme: /myapp/rest/

Et vous voulez implémenter un HttpSearchBase vous pouvez simplement étendre HttpServiceBase et remplacer Host_AND_ENDPOINT_START avec quelque chose comme:

/myapp/rest/search

Exemple CarDriverService

@Injectable()
export class CarDriverService extends HttpServiceBase{

    //here we are requesting a different API
    Host_AND_ENDPOINT_START : string = '/myapp/rest/vehicle/;
    getAllCars() : Observable<Car[]>{
    return this.getWebServiceDataWithPartialEndpoint('/Car')
           .map(res => <Car[]>res.json())
    }

    getAllDrivers(){
    return this.getWebServiceDataWithPartialEndpoint('/Driver')
    }

    addNewDriver(driver: Driver){
    return this.postWebServiceDataWithPartialEndpoint('/Driver/',driver)
    }


}
2
Daniel Cooke

Voici un exemple de base basé sur Angular 7 et RxJS 6.

ApiResponse<T> Représente toute réponse du serveur. Le serveur doit avoir la même structure et la renvoyer quoi qu'il arrive:

export class ApiResponse<T> {
  constructor() {
    this.errors = [];
  }
  data: T;
  errors: ApiError[];
  getErrorsText(): string {
    return this.errors.map(e => e.text).join(' ');
  }
  hasErrors(): boolean {
    return this.errors.length > 0;
  }
}

export class ApiError { code: ErrorCode; text: string; }

export enum ErrorCode {
  UnknownError = 1,
  OrderIsOutdated = 2,
  ...
}

Service générique:

export class RestService<T> {
  httpOptions = {
    headers: new HttpHeaders({ 'Content-Type': 'application/json', 
       'Accept': 'application/json'})
  };
  private _apiEndPoint: string = environment.apiEndpoint;
  constructor(private _url: string, private _http: HttpClient) { }

  getAll(): Observable<ApiResponse<T[]>> {
    return this.mapAndCatchError(
      this._http.get<ApiResponse<T[]>>(this._apiEndPoint + this._url
         , this.httpOptions)
    );
  }
  get(id: number): Observable<ApiResponse<T>> {
    return this.mapAndCatchError(
      this._http.get<ApiResponse<T>>(`${this._apiEndPoint + this._url}/${id}`
         , this.httpOptions)
    );
  }
  add(resource: T): Observable<ApiResponse<number>> {
    return this.mapAndCatchError(
      this._http.post<ApiResponse<number>>(
        this._apiEndPoint + this._url,
        resource,
        this.httpOptions)
    );
  }
  // update and remove here...

  // common method
  makeRequest<TData>(method: string, url: string, data: any)
                                    : Observable<ApiResponse<TData>> {
    let finalUrl: string = this._apiEndPoint + url;
    let body: any = null;
    if (method.toUpperCase() == 'GET') {
      finalUrl += '?' + this.objectToQueryString(data);
    }
    else {
      body = data;
    }
    return this.mapAndCatchError<TData>(
      this._http.request<ApiResponse<TData>>(
        method.toUpperCase(),
        finalUrl,
        { body: body, headers: this.httpOptions.headers })
    );
  }

  /////// private methods
  private mapAndCatchError<TData>(response: Observable<ApiResponse<TData>>)
                                         : Observable<ApiResponse<TData>> {
    return response.pipe(
      map((r: ApiResponse<TData>) => {
        var result = new ApiResponse<TData>();
        Object.assign(result, r);
        return result;
      }),
      catchError((err: HttpErrorResponse) => {
        var result = new ApiResponse<TData>();
        Object.assign(result, err.error)
        // if err.error is not ApiResponse<TData> e.g. connection issue
        if (result.errors.length == 0) {
          result.errors.Push({ code: ErrorCode.UnknownError, text: 'Unknown error.' });
        }
        return of(result);
      })
    );
  }

  private objectToQueryString(obj: any): string {
    var str = [];
    for (var p in obj)
      if (obj.hasOwnProperty(p)) {
        str.Push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
      }
    return str.join("&");
  }
}

alors vous pouvez dériver de RestService<T>:

export class OrderService extends RestService<Order> {
  constructor(http: HttpClient) { super('order', http); }
}

et l'utiliser:

this._orderService.getAll().subscribe(res => {
  if (!res.hasErrors()) {
    //deal with res.data : Order[]
  }
  else {
    this._messageService.showError(res.getErrorsText());
  }
});
// or
this._orderService.makeRequest<number>('post', 'order', order).subscribe(r => {
  if (!r.hasErrors()) {
    //deal with r.data: number
  }
  else
    this._messageService.showError(r.getErrorsText());
});

Vous pouvez reconcevoir RestService<T>.ctor Et injecter RestService<Order> Directement au lieu de déclarer et d'injecter OrderService.

Il semble que RxJS 6 Ne permet pas de renvoyer/renvoyer les erreurs tapées. Pour cette raison, RestService<T> Intercepte toutes les erreurs et les renvoie dans ApiResponse<T> Fortement tapé. Le code appelant doit vérifier ApiResponse<T>.hasErrors() au lieu de détecter les erreurs sur Observable<T>

2
AlbertK