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.
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>;
}
}
Ayez un service de base pour votre application.
Avec les méthodes get
post
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)
}
}
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>