Angular9 DI 提供者

2020-07-03 16:26 更新

依赖提供者会使用 DI 令牌来配置注入器,注入器会用它来提供这个依赖值的具体的、运行时版本。 注入器依靠 "提供者配置" 来创建依赖的实例,并把该实例注入到组件、指令、管道和其它服务中。

你必须使用提供者来配置注入器,否则注入器就无法知道如何创建此依赖。 注入器创建服务实例的最简单方法,就是用这个服务类本身来创建它。 如果你把服务类作为此服务的 DI 令牌,注入器的默认行为就是 new 出这个类实例。

在下面这个典型的例子中,Logger 类自身提供了Logger` 的实例。

providers: [Logger]

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

  • 你可以提供一个替代类。

  • 你可以提供一个类似于 Logger 的对象。

  • 你的提供者可以调用一个工厂函数来创建 logger

Provider 对象字面量

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

providers: [Logger]

[{ provide: Logger, useClass: Logger }]

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

  • provide 属性存有令牌,它作为一个 key,在定位依赖值和配置注入器时使用。

  • 第二个属性是一个提供者定义对象,它告诉注入器要如何创建依赖值。 提供者定义对象中的 key 可以是 useClass —— 就像这个例子中一样。 也可以是 useExistinguseValueuseFactory。 每一个 key 都用于提供一种不同类型的依赖,我们稍后会讨论。

替代类提供者

不同的类都可用于提供相同的服务。 比如,下面的代码告诉注入器,当组件使用 Logger 令牌请求日志对象时,给它返回一个 BetterLogger 实例。

[{ provide: Logger, useClass: BetterLogger }]

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

  • provide 属性存有令牌,它作为一个 key,在定位依赖值和配置注入器时使用。

  • 第二个属性是一个提供者定义对象,它告诉注入器要如何创建依赖值。 提供者定义对象中的 key 可以是 useClass —— 就像这个例子中一样。 也可以是 useExistinguseValueuseFactory。 每一个 key 都用于提供一种不同类型的依赖,我们稍后会讨论。

替代类提供者

不同的类都可用于提供相同的服务。 比如,下面的代码告诉注入器,当组件使用 Logger 令牌请求日志对象时,给它返回一个 BetterLogger 实例。

[{ provide: Logger, useClass: BetterLogger }]

带依赖的类提供者

另一个类 EvenBetterLogger 可能要在日志信息里显示用户名。 这个 logger 要从注入的 UserService 实例中来获取该用户。

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


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

注入器需要提供这个新的日志服务以及该服务所依赖的 UserService 对象。 使用 useClass 作为提供者定义对象的 key,来配置一个 logger 的替代品,比如 BetterLogger。 下面的数组同时在父模块和组件的 providers 元数据选项中指定了这些提供者。

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

别名类提供者

假设老的组件依赖于 OldLogger 类。OldLoggerNewLogger 的接口相同,但是由于某种原因,我们没法修改老的组件来使用 NewLogger

当老的组件要使用 OldLogger 记录信息时,你可能希望改用 NewLogger 的单例来处理它。 在这种情况下,无论某个组件请求老的 logger 还是新的 logger,依赖注入器都应该注入这个 NewLogger 的单例。 也就是说 OldLogger 应该是 NewLogger 的别名。

如果你试图用 useClassOldLogger 指定一个别名 NewLogger,就会在应用中得到 NewLogger 的两个不同的实例。

[ NewLogger,
  // Not aliased! Creates two instances of `NewLogger`
  { provide: OldLogger, useClass: NewLogger}]

要确保只有一个 NewLogger 实例,就要用 useExisting 来为 OldLogger 指定别名。

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

值提供者

有时候,提供一个现成的对象会比要求注入器从类去创建更简单一些。 如果要注入一个你已经创建过的对象,请使用 useValue 选项来配置该注入器。

下面的代码定义了一个变量,用来创建这样一个能扮演 logger 角色的对象。

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


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

下面的提供者定义对象使用 useValue 作为 key 来把该变量与 Logger 令牌关联起来。

[{ provide: Logger, useValue: SilentLogger }]

非类依赖

并非所有的依赖都是类。 有时候你会希望注入字符串、函数或对象。

应用通常会用大量的小型参数来定义配置对象,比如应用的标题或 Web API 端点的地址。 这些配置对象不一定总是类的实例。 它们还可能是对象字面量,如下例所示。

Path:"src/app/app.config.ts (excerpt)" 。

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

TypeScript 接口不是有效的令牌

HERO_DI_CONFIG 常量满足 AppConfig 接口的要求。 不幸的是,你不能用 TypeScript 的接口作为令牌。 在 TypeScript 中,接口是一个设计期的概念,无法用作 DI 框架在运行期所需的令牌。

// FAIL! Can't use interface as provider token
[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]

// FAIL! Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }

如果你曾经在强类型语言中使用过依赖注入功能,这一点可能看起来有点奇怪,那些语言都优先使用接口作为查找依赖的 key。 不过,JavaScript 没有接口,所以,当 TypeScript 转译成 JavaScript 时,接口也就消失了。 在运行期间,没有留下任何可供 Angular 进行查找的接口类型信息。

替代方案之一是以类似于 AppModule 的方式,在 NgModule 中提供并注入这个配置对象。

Path:"src/app/app.module.ts (providers)" 。

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

另一个为非类依赖选择提供者令牌的解决方案是定义并使用 InjectionToken 对象。 下面的例子展示了如何定义那样一个令牌。

Path:"src/app/app.config.ts" 。

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


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

虽然类型参数在这里是可选的,不过还是能把此依赖的类型信息传达给开发人员和开发工具。 这个令牌的描述则是开发人员的另一个助力。

使用 InjectionToken 对象注册依赖提供者:

Path:"src/app/app.component.ts" 。

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

虽然 AppConfig 接口在依赖注入时没有任何作用,但它可以为该组件类中的这个配置对象指定类型信息。

工厂提供者

有时候你需要动态创建依赖值,创建时需要的信息你要等运行期间才能拿到。 比如,你可能需要某个在浏览器会话过程中会被反复修改的信息,而且这个可注入服务还不能独立访问这个信息的源头。

这种情况下,你可以使用工厂提供者。 当需要从第三方库创建依赖项实例时,工厂提供者也很有用,因为第三方库不是为 DI 而设计的。

比如,假设 HeroService 必须对普通用户隐藏秘密英雄,只有得到授权的用户才能看到他们。

EvenBetterLogger 一样,HeroService 需要知道该用户是否有权查看秘密英雄。 而认证信息可能会在应用的单个会话中发生变化,比如你改用另一个用户登录。

假设你不希望直接把 UserService 注入到 HeroService 中,因为你不希望把这个服务与那些高度敏感的信息牵扯到一起。 这样 HeroService 就无法直接访问到用户信息,来决定谁有权访问,谁没有。

要解决这个问题,我们给 HeroService 的构造函数一个逻辑型标志,以控制是否显示秘密英雄。

Path:"src/app/heroes/hero.service.ts (excerpt)" 。

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


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

你可以注入 Logger 但是不能注入 isAuthorized 标志。不过你可以改用工厂提供者来为 HeroService 创建一个新的 logger 实例。

工厂提供者需要一个工厂函数。

Path:"src/app/heroes/hero.service.provider.ts (excerpt)" 。

let heroServiceFactory = (logger: Logger, userService: UserService) => {
  return new HeroService(logger, userService.user.isAuthorized);
};

虽然 HeroService 不能访问 UserService,但是工厂函数可以。 你把 LoggerUserService 注入到了工厂提供者中,并让注入器把它们传给这个工厂函数。

Path:"src/app/heroes/hero.service.provider.ts (excerpt)" 。

export let heroServiceProvider =
  { provide: HeroService,
    useFactory: heroServiceFactory,
    deps: [Logger, UserService]
  };

  • useFactory 字段告诉 Angular 该提供者是一个工厂函数,该函数的实现代码是 heroServiceFactory

  • deps 属性是一个提供者令牌数组。 LoggerUserService 类作为它们自己的类提供者令牌使用。 注入器解析这些令牌,并把与之对应的服务注入到相应的工厂函数参数表中。

注意,你把这个工厂提供者保存到了一个导出的变量 heroServiceProvider 中。 这个额外的步骤让工厂提供者可被复用。 你可以在任何需要它的地方用这个变量来配置 HeroService 的提供者。 在这个例子中,你只在 HeroesComponent 中用到了它。你在该组件元数据的 providers 数组中用 heroServiceProvider 替换了 HeroService

下面并列显示了新旧实现。

  1. Path:"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 { }

  1. Path:"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 { }

预定义令牌与多提供者

Angular 提供了一些内置的注入令牌常量,你可以用它们来自定义系统的多种行为。

比如,你可以使用下列内置令牌来切入 Angular 框架的启动和初始化过程。 提供者对象可以把任何一个注入令牌与一个或多个用来执行应用初始化操作的回调函数关联起来。

  • PLATFORM_INITIALIZER:平台初始化之后调用的回调函数。

  • APP_BOOTSTRAP_LISTENER:每个启动组件启动完成之后调用的回调函数。这个处理器函数会收到这个启动组件的 ComponentRef 实例。

  • APP_INITIALIZER:应用初始化之前调用的回调函数。注册的所有初始化器都可以(可选地)返回一个 Promise。所有返回 Promise 的初始化函数都必须在应用启动之前解析完。如果任何一个初始化器失败了,该应用就不会继续启动。

该提供者对象还有第三个选项 multi: true,把它和 APP_INITIALIZER 一起使用可以为特定的事件注册多个处理器。

比如,当启动应用时,你可以使用同一个令牌注册多个初始化器。

export const APP_TOKENS = [
 { provide: PLATFORM_INITIALIZER, useFactory: platformInitialized, multi: true    },
 { provide: APP_INITIALIZER, useFactory: delayBootstrapping, multi: true },
 { provide: APP_BOOTSTRAP_LISTENER, useFactory: appBootstrapped, multi: true },
];

在其它地方,多个提供者也同样可以和单个令牌关联起来。 比如,你可以使用内置的 NG_VALIDATORS 令牌注册自定义表单验证器,还可以在提供者定义对象中使用 multi: true 属性来为指定的验证器令牌提供多个验证器实例。 Angular 会把你的自定义验证器添加到现有验证器的集合中。

路由器也同样用多个提供者关联到了一个令牌。 当你在单个模块中用 RouterModule.forRootRouterModule.forChild 提供了多组路由时,ROUTES 令牌会把这些不同的路由组都合并成一个单一值。

可摇树优化的提供者

摇树优化是指一个编译器选项,意思是把应用中未引用过的代码从最终生成的包中移除。 如果提供者是可摇树优化的,Angular 编译器就会从最终的输出内容中移除应用代码中从未用过的服务。 这会显著减小你的打包体积。

理想情况下,如果应用没有注入服务,它就不应该包含在最终输出中。 不过,Angular 要能在构建期间识别出该服务是否需要。 由于还可能用 injector.get(Service) 的形式直接注入服务,所以 Angular 无法准确识别出代码中可能发生此注入的全部位置,因此为保险起见,只能把服务包含在注入器中。 因此,在 NgModule 或 组件级别提供的服务是无法被摇树优化掉的。

下面这个不可摇树优化的 Angular 提供者的例子为 NgModule 注入器配置了一个服务提供者。

Path:"src/app/tree-shaking/service-and-modules.ts" 。

import { Injectable, NgModule } from '@angular/core';


@Injectable()
export class Service {
  doSomething(): void {
  }
}


@NgModule({
  providers: [Service],
})
export class ServiceModule {
}

你可以把该模块导入到你的应用模块中,以便该服务可注入到你的应用中,例子如下。

Path:"src/app/tree-shaking/app.modules.ts" 。

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot([]),
    ServiceModule,
  ],
})
export class AppModule {
}

当运行 ngc 时,它会把 AppModule 编译到模块工厂中,工厂包含该模块及其导入的所有模块中声明的所有提供者。在运行时,该工厂会变成负责实例化所有这些服务的注入器。

这里摇树优化不起作用,因为 Angular 无法根据是否用到了其它代码块(服务类),来决定是否能排除这块代码(模块工厂中的服务提供者定义)。要让服务可以被摇树优化,关于如何构建该服务实例的信息(即提供者定义),就应该是服务类本身的一部分。

创建可摇树优化的提供者

只要在服务本身的 @Injectable() 装饰器中指定,而不是在依赖该服务的 NgModule 或组件的元数据中指定,你就可以制作一个可摇树优化的提供者。

下面的例子展示了与上面的 ServiceModule 例子等价的可摇树优化的版本。

Path:"src/app/tree-shaking/service.ts" 。

@Injectable({
  providedIn: 'root',
})
export class Service {
}

该服务还可以通过配置工厂函数来实例化,如下例所示。

Path:"src/app/tree-shaking/service.0.ts" 。

@Injectable({
  providedIn: 'root',
  useFactory: () => new Service('dependency'),
})
export class Service {
  constructor(private dep: string) {
  }
}

要想覆盖可摇树优化的提供者,请使用其它提供者来配置指定的 NgModule 或组件的注入器,只要使用 @NgModule()@Component() 装饰器中的 providers: [] 数组就可以了。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号