首页 > 解决方案 > 通过 Angular 中的工厂打开通用注入

问题描述

我希望能够使用通用工厂注入具有约束到 Angular 中的接口的泛型服务...如果我声明每种注入类型的提供者,我可以做到这一点,但这违背了我为什么要这样做的目的想要这样。

我想要的是这样的:

interface WorkerServiceContract {
  doWork(): void;
}

class MyService<T extends WorkerServiceContract> {
  constructor(private worker: T) {}

  doWorkWrapper() { this.worker.doWork(); }
}

所以在任何时候我都可以做一个:

@Injectable({ providedIn: 'root' })
class FooWorkerService implements WorkerServiceContract {
   doWork() { console.log('foo'); }
}

@Injectable({ providedIn: 'root' })
class BarWorkerService implements WorkerServiceContract {
   doWork() { console.log('bar'); }
}

@Component(/*blabla*/)
class MyComponent {
   constructor(private fooWorker: MyService<FooWorkerService>, private barWorker: MyService<BarWorkerService>) {}
}

我知道我可以为每种可能性使用特定的令牌声明注入WorkerServiceContract,但我想知道是否有办法(我查看了文档但无法弄清楚)让它“打开”.. .所以我可以做类似的事情(这显然行不通):

providers: [
   {
      provide: MyService<T extends ServiceWorker>
      useFactory: (worker: T) => new MyService<T>(worker);
      deps: [T]
   }
] 

我知道这在提供者定义中是不可能的(因为它不知道T),但是是否有任何机制可以允许像这样的“通用”工作?这可能是显而易见的事情,但我似乎可以开始做这件事

我正在使用 Angular 9


真实场景(附录)

我们为什么想要这个(现实生活场景)的全部理由是:

我有一个工具生成的服务类(来自 Swagger/OpenApi)。对于该服务,我创建了一个代理/包装服务,它拦截对 API 的所有 http(不是 http,而是方法,这些方法不仅仅是调用 http 客户端)调用,并通过管道将返回的 observables 处理错误(和成功,实际上)显示 UI 通知(并创建对诊断日志服务器的其他调用等)。

这些处理程序有时是通用的,但每个视图(取决于被调用的 API)可能希望有自己的处理程序(例如,一个可能显示一个 toast,一个可能显示一个弹出窗口,一个可能希望转换对 API 的调用一些方法,或其他)。

我可以在每次调用 API 时都这样做,但在我的团队中,我们发现将这些问题(处理成功调用与从 API 接收的正常数据以及错误处理)分开有助于提高可读性和组件代码的大小(以及每个代码的责任)。我们已经通过对构造函数的“简单”调用解决了这个问题,例如:

constructor(private apiService: MyApiService) {
  this.apiService = apiService.getProxyService<MyErrorHandler>();
}

它返回一个Proxy处理所有这些的。这很好用,但我们正在讨论让它“更干净”的想法,如下所示:

constructor(private apiService: MyApiService<MyErrorHandler>) {}

并让 DI 容器上的工厂为我们创建该代理,这将带来以下好处:a) 不必记住对构造函数进行调用,b) 清楚地了解所有依赖项(包括错误处理程序)直接在构造函数参数上(无需深入研究构造函数代码,根据实际组件可能还有其他内容)

不,HttpClient拦截器在这里不起作用,因为自动生成的服务不仅仅是HttpClient调用(我们想要处理从该服务返回的内容,而不是直接在HttpResponse对象上)

标签: angulartypescript

解决方案


更新

也许尝试使用令牌的注射器

const services = new Map();
const myService = <T>(service: T): InjectionToken<MyService<T>> => {
  if (services.has(service)) {
    return services.get(service);
  }

  const token = new InjectionToken<<T>(t: T) => MyService<T>>(`${Math.random()}`, {
    providedIn: 'root',
    factory: injector => new MyService(injector.get(service)),
    deps: [Injector],
  });

  services.set(service, token);
  return token;
};

class MyComponent {
   constructor(
     @Inject(myService(FooWorkerService)) private foo: MyService <FooWorkerService>,
     @Inject(myService(BarWorkerService)) private bar: MyService <BarWorkerService>,
   ) {
  }
}

原来的

你是对的,泛型在编译后不存在,因此不能用作提供者。

要解决它,您需要在组件中注入工厂本身。因为无论如何您都会指定泛型的类型,所以现在您需要更多代码来实现所需的行为。

您可以使用


const MY_SERVICE_FACTORY = new InjectionToken<<T>(t: T) => MyService<T>>('MY_SERVICE_FACTORY', {
    providedIn: 'root',
    factory: () => worker => new MyService(worker),
});

// just a helper to extract type, can be omitted
export type InjectionTokenType<Type> = Type extends InjectionToken<infer V> ? V : never;

class MyComponent {
   constructor(
     @Inject(MY_SERVICE_FACTORY) private serviceFactory: InjectionTokenType<typeof MY_SERVICE_FACTORY>,
     foo: FooWorkerService,
     bar: BarWorkerService,
   ) {
   this.fooWorker = this.serviceFactory(foo);
   this.barWorker = this.serviceFactory(bar);
  }
}

还为了保持构造函数中的代码更清晰,您可以将其移动到组件的提供者。

@Component({
  // blabla
  providers: [
    {
      provide: 'foo',
      useFactory: t => new MyService(t),
      deps: [FooWorkerService],
    },
    {
      provide: 'bar',
      useFactory: t => new MyService(t),
      deps: [BarWorkerService],
    },
  ],
})
class MyComponent {
   constructor(
     @Inject('foo') private fooWorker: MyService<FooWorkerService>,
     @Inject('bar') private barWorker: MyService<BarWorkerService>,
   ) {}
}

推荐阅读