什么是Angular依赖提供者?

依赖提供者

笔记参考:https://angular.cn/guide/dependency-injection-providers

 

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

 

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

providers: [Logger]

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

  • 你可以提供一个替代类
  • 你可以提供一个类似于Logger的对象
  • 你的提供者可以调用一个工厂函数来创建logger

 

Provider对象字面量

类提供者的语法实际上是一种简写形式,它会扩展成一个由provider接口定义的提供者配置对象。

providers: [Logger]

完整的提供者配置对象

[{ provide: Logger, useClass: Logger }]
  • provide属性存有令牌,它作为一个key,在定位依赖值和配置注入器时使用。
  • 第二个属性是一个提供者定义对象,它告诉注入器要如何创建依赖值。提供者定义对象中的key可以是useClass--就像这个例子中一样。也可以是useExisting useValue或useFactory。每一个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类。OldLogger和NewLogger的接口相同,但是由于某种原因,我们没法修改老的组件来使用NewLogger

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

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

[ NewLogger,
    { provide: OldLogger, useClass: NewLogger }
]

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

[ NewLogger,
   { provide: OldLogger, useExisting: NewLogger}
]

 

值提供者

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

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

function silentLoggerFn() {}

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

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

[{ provide: Logger, useValue: SilentLogger }]

 

非类依赖

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

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

src/app/app.config.ts

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

 

TypeScript接口不是有效的令牌

在TypeScript中,接口是一个设计期的概念,无法用做DI框架在运行期所需的令牌。

// 错误,不能使用接口作为提供者
[{ provide: AppConfig, useValue: HERO_DI_CONFIG }]

// 错误,不能使用接口作为参数类型
constructor(private config: AppConfig) {}

 

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

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

src/app/app.module.ts(providers)

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

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

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

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

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

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

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

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

src/app/app.component.ts

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

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

 

工厂提供者

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

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

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

而认证信息可能会在应用的单个会话中发生变化,比如你改用另一个用户登录。

 

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

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

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实例。

 

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

src/app/heroes/hero.service.provider.ts(excerpt)

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

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

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

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

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

 

posted on 2020-08-27 17:30  白小鸽  阅读(422)  评论(0编辑  收藏  举报