【问题标题】:Typescript generic service打字稿通用服务
【发布时间】:2017-05-23 08:38:19
【问题描述】:

我是 typescript 和 angular2/4 的新手,我正在构建一个具有两个基本实体的应用程序,即 Car 和 Driver,我所做的只是通过 API 调用列出它们。

我面临的问题是每个 CarService 和 DriverService 都有代码冗余,而其他实体服务的代码可能相同。

目前的实现如下,跳过其他方法进行说明:

@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');
}

DriverService 唯一改变的是 url 末尾的 Car/Observable&lt;Car[]&gt; 中的数据类型和响应。

我想知道使用通用服务避免这种情况的最佳方法是什么,以及如何在 Typescript 中做到这一点。

【问题讨论】:

  • 为什么还要为此提供服务?为什么不直接使用 http 服务作为通用服务呢?
  • 或者您可能想查看 ngx-resource 库。 github.com/troyanskiy/ngx-resource
  • 我的服务中有多个函数,但为简单起见,我隐藏了其他调用。
  • 不要,它们可能是相关的。 :)
  • 我用完整的服务更新了问题,驱动程序和其他实体服务是一样的,只有url和数据类型会改变,所以我想到了一个通用服务但我不知道如何实现它,我找不到一个完整且解释清楚的方法来使用服务来实现它

标签: angular generics typescript service


【解决方案1】:

您可以创建一个抽象泛型类和两个继承自它的子类:

抽象类:

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);
  }
} 

驱动服务类

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

汽车服务等级

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

请注意,只有具体类被标记为@Injectable(),并且应该在模块内声明,而抽象类不应该。

Angular 4+ 更新

Http 类被弃用以支持HttpClient,您可以将抽象类更改为类似的内容:

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>;
  }
} 

【讨论】:

  • 是的,这更准确地说是我所期待的实现。然后我的问题是如何在其中一个子类中调用getAll()
  • 我不明白这个问题,对不起...你想从哪里打电话给getAll()
  • 我的意思是如果我在一个组件中调用 CarService 的getAll(),它会是这样的:this._carService.GetAll().subscribe((data: Car[])... 对吗?我不会有数据类型的问题吧?
  • @danger89 *Angular 4+
  • 你能用配置扩展答案吗?
【解决方案2】:

以下是基于 Angular 7RxJS 6 构建的基本示例。

ApiResponse&lt;T&gt; 代表任何服务器响应。服务器必须具有相同的结构,并且无论发生什么都返回它:

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,
  ...
}

通用服务:

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>();
        // if err.error is not ApiResponse<TData> e.g. connection issue
        if (err.error instanceof ErrorEvent || err.error instanceof ProgressEvent) {
          result.errors.push({ code: ErrorCode.UnknownError, text: 'Unknown error.' });
        }
        else {
          Object.assign(result, err.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("&");
  }
}

那么你可以从RestService&lt;T&gt;派生:

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

并使用它:

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());
});

您可以重新设计RestService&lt;T&gt;.ctor并直接注入RestService&lt;Order&gt;,而不是声明和注入OrderService

看起来RxJS 6 不允许重新抛出/返回键入的错误。因此RestService&lt;T&gt; 捕获所有错误并在强类型ApiResponse&lt;T&gt; 中返回它们。调用代码应该检查ApiResponse&lt;T&gt;.hasErrors() 而不是在Observable&lt;T&gt; 上捕获错误

【讨论】:

    【解决方案3】:

    为您的应用提供基础服务。

    使用 get postdelete 方法并附加您的 base URL

    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
    
            );
        }
    

    这是一个有用的实现,因为它允许您轻松调试 WS 调用 - 所有调用最终都来自基础。

    HOST_AND_ENDPOINT_START 可以被您想要扩展基础服务的任何模块覆盖。

    让我们假设你的端点是这样的: /myapp/rest/

    如果你想实现一个HttpSearchBase,你可以简单地扩展HttpServiceBase并用类似的东西覆盖HOST_AND_ENDPOINT_START

    /myapp/rest/search

    例如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)
        }
    
    
    }
    

    【讨论】:

    • 我看到了背后的逻辑和我想要的那种。但在这种情况下,例如,在 CarDriverService 中获取 AllCars() 并将其映射到我的数据类型,我所要做的就是 return this.getWebServiceDataWithPartialEndpoint('/Car').map((response: Response) =&gt; &lt;Car[]&gt;response.json()) ?
    • 是的。我忘了包括映射 - 我已经更新了 getAllCars 方法。
    • 感谢您的回答@DanielCooke
    猜你喜欢
    • 1970-01-01
    • 2023-03-29
    • 2015-08-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-15
    • 2019-04-18
    • 2020-07-29
    相关资源
    最近更新 更多