阅读(1104) (13)

Angular DI提供者

2022-06-28 10:46:18 更新

依赖提供者

通过配置提供者,你可以把服务提供给那些需要它们的应用部件。

依赖提供者会使用 DI 令牌来配置注入器,注入器会用它来提供这个依赖值的具体的、运行时版本。

指定提供者令牌

如果你把服务类指定为提供者令牌,那么注入器的默认行为是用 ​new ​来实例化那个类。

在下面这个例子中,​Logger ​类提供了 ​Logger ​的实例。

providers: [Logger]

不过,你也可以用一个替代提供者来配置注入器,这样就可以指定另一些同样能提供日志功能的对象。

可以使用服务类来配置注入器,也可以提供一个替代类、一个对象或一个工厂函数。

依赖注入令牌

当使用提供者配置注入器时,会将该提供者与依赖项注入令牌(或叫 DI 令牌)关联起来。注入器允许 Angular 创建任何内部依赖项的映射。DI 令牌会充当该映射的键名。

依赖项值是一个实例,而这个类的类型用作查找键。在这里,注入器使用 ​HeroService ​类型作为令牌来查找 ​heroService​。

heroService: HeroService;

当你使用 ​HeroService ​类的类型来定义构造函数参数时,Angular 会注入与这个 ​HeroService ​类令牌相关联的服务:

constructor(heroService: HeroService)

尽管许多依赖项的值是通过类提供的,但扩展的 ​provide ​对象使你可以将不同种类的提供者与 DI 令牌相关联。

定义提供者

类提供者的语法实际上是一种简写形式,它会扩展成一个由 ​Provider ​接口定义的提供者配置对象。 下面的代码片段展示了 ​providers ​中给出的类会如何扩展成完整的提供者配置对象。

providers: [Logger]

Angular 把这个 ​providers ​值扩展为一个完整的提供者对象,如下所示。

[{ provide: Logger, useClass: Logger }]

扩展的提供者配置是一个具有两个属性的对象字面量:

  • provide ​属性存有令牌,它作为一个 key,在定位依赖值和配置注入器时使用。
  • 第二个属性是一个提供者定义对象,它告诉注入器要如何创建依赖值。 提供者定义对象中的 key 可以是 ​useClass ​—— 就像这个例子中一样。 也可以是 ​useExisting​、​useValue ​或 ​useFactory​。 每一个 key 都用于提供一种不同类型的依赖,我们稍后会讨论。

指定替代性的类提供者

不同的类可以提供相同的服务。例如,以下代码告诉注入器,当组件使用 ​Logger ​令牌请求一个 logger 时,给它返回一个 ​BetterLogger​。

[{ provide: Logger, useClass: BetterLogger }]

配置带依赖的类提供者

如果替代类提供者有自己的依赖,那就在父模块或组件的元数据属性 ​providers ​中指定那些依赖。

[ UserService,
  { provide: Logger, useClass: EvenBetterLogger }]

在这个例子中,​EvenBetterLogger ​会在日志信息里显示用户名。 这个 logger 要从注入的 ​UserService ​实例中来获取该用户。

@Injectable()
export class EvenBetterLogger extends Logger {
  constructor(private userService: UserService) { super(); }

  override log(message: string) {
    const name = this.userService.user.name;
    super.log(`Message to ${name}: ${message}`);
  }
}

注入器需要提供这个新的日志服务以及该服务所依赖的 ​UserService ​对象。

别名类提供者

要为类提供者设置别名,请在 ​providers ​数组中使用 ​useExisting ​属性指定别名和类提供者。

在下面的例子中,当组件请求新的或旧的记录器时,注入器都会注入一个 ​NewLogger ​的实例。 通过这种方式,​OldLogger ​就成了 ​NewLogger ​的别名。

[ NewLogger,
  // Alias OldLogger w/ reference to NewLogger
  { provide: OldLogger, useExisting: NewLogger}]

请确保你没有使用 ​useClass ​来把 ​OldLogger ​设为 ​NewLogger ​的别名,因为如果这样做它就会创建两个不同的 ​NewLogger ​实例。

为类接口指定别名

通常,编写同一个父组件别名提供者的变体时会使用forwardRef,如下所示。

providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],

为简化你的代码,可以使用辅助函数 ​provideParent()​ 来把这个逻辑提取到一个辅助函数中。

// Helper method to provide the current component instance in the name of a `parentType`.
export function provideParent
  (component: any) {
    return { provide: Parent, useExisting: forwardRef(() => component) };
  }

现在,你可以为组件添加一个更容易阅读和理解的父提供者。

providers:  [ provideParent(AliceComponent) ]

为多个类接口指定别名

要为多个父类型指定别名(每个类型都有自己的类接口令牌),请配置 ​provideParent()​ 以接受更多的参数。

这是一个修订版本,默认值为 ​parent ​但同时也接受另一个父类接口作为可选的第二参数。

// Helper method to provide the current component instance in the name of a `parentType`.
// The `parentType` defaults to `Parent` when omitting the second parameter.
export function provideParent
  (component: any, parentType?: any) {
    return { provide: parentType || Parent, useExisting: forwardRef(() => component) };
  }

接下来,要使用 ​provideParent()​,请传入第二参数,这里是 ​DifferentParent​。

providers:  [ provideParent(BethComponent, DifferentParent) ]

注入一个对象

要注入一个对象,可以用 ​useValue ​选项来配置注入器。 下面的提供者定义对象使用 ​useValue ​作为 key 来把该变量与 ​Logger ​令牌关联起来。

[{ provide: Logger, useValue: SilentLogger }]

在这个例子中,​SilentLogger ​是一个充当记录器角色的对象。

// An object in the shape of the logger service
function silentLoggerFn() {}

export const SilentLogger = {
  logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
  log: silentLoggerFn
};

注入一个配置对象

常用的对象字面量是配置对象。下列配置对象包括应用的标题和 Web API 的端点地址。

export const HERO_DI_CONFIG: AppConfig = {
  apiEndpoint: 'api.heroes.com',
  title: 'Dependency Injection'
};

要提供并注入配置对象,请在 ​@NgModule()​ 的 ​providers ​数组中指定该对象。

providers: [
  UserService,
  { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],

使用 InjectionToken 对象

可以定义和使用一个 ​InjectionToken ​对象来为非类的依赖选择一个提供者令牌。下列例子定义了一个类型为 ​InjectionToken ​的 ​APP_CONFIG ​。

import { InjectionToken } from '@angular/core';

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

可选的参数 ​<AppConfig>​ 和令牌描述 ​app.config​ 指明了此令牌的用途。

接着,用 ​APP_CONFIG ​这个 ​InjectionToken ​对象在组件中注册依赖提供者。

providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]

现在,借助参数装饰器 ​@Inject()​,你可以把这个配置对象注入到构造函数中。

constructor(@Inject(APP_CONFIG) config: AppConfig) {
  this.title = config.title;
}

接口和依赖注入

虽然 TypeScript 的 ​AppConfig ​接口可以在类中提供类型支持,但它在依赖注入时却没有任何作用。在 TypeScript 中,接口是一项设计期工件,它没有可供 DI 框架使用的运行时表示形式或令牌。

当转译器把 TypeScript 转换成 JavaScript 时,接口就会消失,因为 JavaScript 没有接口。

由于 Angular 在运行期没有接口,所以该接口不能作为令牌,也不能注入它。

// Can't use interface as provider token
[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]
// Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }

使用工厂提供者

要想根据运行前尚不可用的信息创建可变的依赖值,可以使用工厂提供者。

在下面的例子中,只有授权用户才能看到 ​HeroService ​中的秘密英雄。授权可能在单个应用会话期间发生变化,比如改用其他用户登录。

要想在 ​UserService ​和 ​HeroService ​中保存敏感信息,就要给 ​HeroService ​的构造函数传一个逻辑标志来控制秘密英雄的显示。

constructor(
  private logger: Logger,
  private isAuthorized: boolean) { }

getHeroes() {
  const auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
  this.logger.log(`Getting heroes for ${auth} user.`);
  return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

要实现 ​isAuthorized ​标志,可以用工厂提供者来为 ​HeroService ​创建一个新的 logger 实例。

const heroServiceFactory = (logger: Logger, userService: UserService) =>
  new HeroService(logger, userService.user.isAuthorized);

这个工厂函数可以访问 ​UserService​。你可以同时把 ​Logger ​和 ​UserService ​注入到工厂提供者中,这样注入器就可以把它们传给工厂函数了。

export const heroServiceProvider =
  { provide: HeroService,
    useFactory: heroServiceFactory,
    deps: [Logger, UserService]
  };
  • useFactory ​字段指定该提供者是一个工厂函数,其实现代码是 ​heroServiceFactory​。
  • deps ​属性是一个提供者令牌数组。 ​Logger ​和 ​UserService ​类都是自己类提供者的令牌。该注入器解析了这些令牌,并把相应的服务注入到 ​heroServiceFactory ​工厂函数的参数中。

通过把工厂提供者导出为变量 ​heroServiceProvider​,就能让工厂提供者变得可复用。

下面这两个并排的例子展示了在 ​providers ​数组中,如何用 ​heroServiceProvider ​替换 ​HeroService

  • src/app/heroes/heroes.component (v3)
  • import { Component } from '@angular/core';
    import { heroServiceProvider } from './hero.service.provider';
    
    @Component({
      selector: 'app-heroes',
      providers: [ heroServiceProvider ],
      template: `
        <h2>Heroes</h2>
        <app-hero-list></app-hero-list>
      `
    })
    export class HeroesComponent { }
  • src/app/heroes/heroes.component (v2)
  • import { Component } from '@angular/core';
    
    import { HeroService } from './hero.service';
    
    @Component({
      selector: 'app-heroes',
      providers: [ HeroService ],
      template: `
        <h2>Heroes</h2>
        <app-hero-list></app-hero-list>
      `
    })
    export class HeroesComponent { }