[Angular] Day19. Dependency providers

在上一篇中提到了如何建立与使用一个 Service,也大概介绍了什麽是 Dependency Injection,在介绍使用 @Injectable() 装饰 service 的 class 的时候提到了要在他的 metadata 中设定 providedIn,如果将它设定为 root 的话代表这个 service 在整个专案中都是可被使用的,但如果没有设定这个 property,则要在 component 中注入这个 service 时需要在 @Component() 的 metadata 中设定 providers ,将要注入的 service 放进去,在上一章中的用法是 providers: [ProductService],这其实是一个缩写,本篇就是要来详细的介绍他,那我们就继续往下看吧!

https://ithelp.ithome.com.tw/upload/images/20210821/20124767ZpsjOVE7SU.jpg


Dependency injection tokens

在上一章中提到了什麽是 Dependency injection 不过这边再来复习一下,这有利於我们接下来要讲的内容,简单来说 Dependency injection 又称 DI 它是一种设计模式和机制,他用於将某些部分与依赖相分离,以 Angular 的例子来说,他就是用於将复杂的逻辑与 Component 中负责处理画面或事件的逻辑分离,而这些被分离出来的部分也可以给其他 Component 使用,使它变成可重复使用且可测试的一个部分。

当整个应用程序启动时,会创建一个名为 injector 的东西,可以将它看成会检查客户订单且满足客户要求的服务生,举例来说他可以制作一些咖啡(实例化 CoffeeService)将它提供给客户,而如果有其他的客户想要喝茶,那麽这个聪明的服务生就会实例化 MintTeaService 和 CamomileTeaService 用於满足不同客户的需求。

然而要让我们的服务生(injector)知道该出什麽餐点则需要客户点单(component),他可以在 component 中的 constructor 中要求他要吃什麽(dependency)

constructor(private coffee: CoffeeService){};

coffee: CoffeeService 就是一个 Dependency injection tokens 它是一种 inject class 的快捷方式,现在我们的服务生从客户那边收集了所有的订单( injector 向 component 收集需求 ),接着 服务生(injector) 会使用 CoffeeService 来找咖啡的标记,将客户点的咖啡记在他的笔记本上。

记录完客户的点单後,接着就需要知道如何制作指定的咖啡,这时就需要配方(provider)了,这个配方是一个 object 他会定义如何获取与 Dependency injection tokens 有关连的可注入依赖项,简单来说他会告诉服务生(injector)该如何制作这杯咖啡( 将 service 实例化成一个 object 并将它注入给 component )

providers: [CoffeeService, BurgerService, PizzaService]

上面的例子来说服务生将知道如何做出咖啡、汉堡、Pizza 并提供给客户,以程序的角度来看可以看成 injector 知道 CoffeeService 要使用 CoffeeService 作为模板将它实例化成一个 object 并注入给 component 让他使用,这边可能看不出来可以换另一种更详细的表达方式

providers: [
	{ provide: CoffeeService, useClass: CoffeeService },
    { provide: BurgerService, useClass: BurgerService },
	{ provide: PizzaService, useClass: PizzaService }
]

这样应该更可以了解,provide property 作爲 locating a dependency value 和 configuring the injector 的 Token,可以把它理解为他是一个 ID,以服务生的例子来说他就等於是客户点的餐点名称,以程序的观点来看就等於他是 component 需要使用的 service name。

而第二个 property 是一个提供 provider 定义的一个 object,他会告诉 injector 该如何建立 dependency 的值,以上面的服务生例子来说他就是菜单,他会告诉服务生该怎麽制作客户点的咖啡,而这个定义 provider 的 key 可以是 useClass 就像上面使用的,也可以是 useExistinguseValueuseFactory 他们每一个都提供了不同类型的 dependency,下面将仔细介绍他们的区别。

https://ithelp.ithome.com.tw/upload/images/20210815/20124767PRR6NqC8Uy.png


Specifying an alternative class provider

首先介绍的是 useClass ,不同的 class 可以提供给相同的 service,比如说

[{ provide: Logger, useClass: BetterLogger }]

上面的例子中代表 component 向 injector 説他需要使用 Logger,而 provider 向他提供了 BetterLogger 这个 class 让 injector 将它实例化後注入给 component 使用。

Configuring class providers with dependencies

如果使用 useClass 的这个 class 有自己的 dependencies,则要在父层 module 或 component 的 metadata 中的 providers 提供他自己於他的 dependencies,举个例子

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

  log(message: string) {
    const name = this.userService.user.name;
    super.log(`Message to ${name}: ${message}`);
  }
}
@Component({
	providers: [UserService, { provide: Logger, useClass: EvenBetterLogger }]]
})

在 component 中使用 useClass 的 class 有自己的 dependencies (UserService),所以在 component 的 metadata 中也需要提供 UserService。


Aliasing class providers

在我们使用 useClass 注入指定 class 内容时可能会遇到一个情况,一开始我们建立了两个 service 分别是 oldServicenewService,这两个 service 一开始负责不同的服务但是到後来的增加跟删除下,发现只要使用 newService 就好,当遇到这种情况可能有些人会到每个有使用到 oldService 的 component 或 module 去把它改掉,但其实 Angular 他提供了另一个方法那就是别名。

在提到这个方法之前先来厘清一下 useClass 的用法

providers: [{provide: Class1, useClass: Class1}]

当使用上面这个方法可以等价为将 Class1 注入到 component 或 module 中

providers: [{provide: Class1, useClass: Class3}]

而上面这个方法可以看成在 Component 或 module 中有一个名叫 Class1 的 Token 要用 Class3 创建并注入,那麽问题来了下面这样该怎麽解释?

providers: [
	Class3,
	{provide: Class1, useClass: Class3}
]

其实很简单,你可以想像在这个 component 或 module 中有一个 Class3 的 Token 利用 Class3 创建并注入,还有第二个 Token 名叫 Class1 但是也是用 Class3 创建并注入,等於说这个 component 或 module 有两个 class3 的实例

有上面的概念後就可以来看 useExisting ,在一开始提到的我们希望 oldService 都替换成 newService 这时有人会下意识的觉得

providers: [
	newService,
	{provide: oldService, useClass: newService}
]

上面的这种改法可以看成在这个 component 或 module 中注入一个 newService 的实例并且在将一个名为 oldService 的 Token 也使用 newService 创建,等於说会有两个 newService 这是我们不希望看见的,这时就可以用 useExisting 改写

providers: [
	newService,
	{provide: oldService, useExisting: newService}
]

上面的写法改成使用 useExisting 就可以避免建立两个 newService 的情况了,可以想像是 newService 这个实例但是用了 oldService 这个别名。


Injecting an object

除了使用 useClass 直接提供一个 class 之外,也可以使用 useValue 将一个 object 提供给 injector,举例来说

export const silentLogger = {
	logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
	log: () => {
		console.log('You are log in');
	}
}
@Component({
	providers: [{ provide: Logger, useValue: silentLogger }]
})

上面的例子中可以看到,在别的地方建立一个 Object 并将它提供给 injector 让他注入给 component 中让 component 中可以使用到这个 Object 中的 property 与 method。

Using an InjectionToken object

在上面可以看到我们在填入 property provide 的时候都是预定他是一个 service,那们可不可以填入某个 object 或一个 function 呢?答案是可以的,不过你得使用 InjectionToken 强制将你想填入的内容产生一个 token,举个例子吧

import { Component, Inject, OnInit, InjectionToken } from '@angular/core';

export const APP_CONFIG = new InjectionToken<{                          // (1)
  apiEndpoint: string;
  title: string;
}>('app.config');

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  providers: [                                                            // (2)
    {
      provide: APP_CONFIG,
      useValue: {
        apiEndpoint: 'api.heroes.com',
        title: 'Dependency Injection',
      },
    },
  ],
})
export class AppComponent implements OnInit {
  title!: string;
  apiEndpoint!: string;
  constructor(
    @Inject(APP_CONFIG) config: { apiEndpoint: string; title: string }    // (3)
  ) {
    this.title = config.title;
    this.apiEndpoint = config.apiEndpoint;
  }

  ngOnInit() {
    console.log(this.title);
    console.log(this.apiEndpoint);
  }
}
  • (1): 替 APP_CONFIG 产生一个 token,参数的字串('app.config')只是对他的描述
  • (2): 将 APP_CONFIG 使用 useValue 提供模板给 injector
  • (3): 使用 @Inject() 装饰器告知使用哪一个 token 注入

https://ithelp.ithome.com.tw/upload/images/20210815/20124767fXP00j0QCs.png


Using factory providers

在介绍完 useValue 後可以看到就算是普通的 object 也可以使用 InjectionToken 强制产生一个 token,不过要这样做的前提是要事先建立好要替代的 token 实体,但这种事先建立的情况在实际中不太常发生,比较多都是需要动态产生的,这时就可以使用 useFactory 把复杂的动态计算放在 factory 里面,动态的建立需要的 token 实体,举个例子

  1. 建立 service1 与 service2

    ng generate service service1
    ng generate service service1
    
  2. 在两个 service 中添加一个 getName() method 但是回传不同的字串

    import { Injectable } from '@angular/core';
    
    @Injectable({ providedIn: 'root' })
    export class Service1Service {
      constructor() { }
      getName() {
        return 'This is Service-1 method'
      }
    }
    
    import { Injectable } from '@angular/core';
    
    @Injectable({ providedIn: 'root' })
    export class Service2Service {
      constructor() { }
      getName() {
        return 'This is Service-2 method'
      }
    }
    
  3. 在 app.component.ts 中使用 useFactory

    import { Component, InjectionToken, OnInit  } from '@angular/core';
    import { Service1Service } from './service1.service';
    import { Service2Service } from './service2.service';
    import { AppService } from './app.service';
    
    export enum ServiceList {                                                 // (1)
      SERVICE_1 = 0,
      SERVICE_2 = 1
    }
    
    export const SelectService = new InjectionToken<number>('selectService'); // (2)
    
    export const serviceFactory = (selectService: number) => {                // (3)
      if (selectService === ServiceList.SERVICE_1) {
        return new Service1Service();
      } else {
        return new Service2Service();
      }
    }
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      providers: [
        {
          provide: SelectService,                                             // (4)
          useValue: ServiceList.SERVICE_1
        },
        {
          provide: AppService,                                                // (5)
          useFactory: serviceFactory,
          deps: [SelectService]
        }
      ],
    })
    export class AppComponent implements OnInit {
    
      constructor(private appService: AppService) {}                          // (6)
    
      ngOnInit() {
        console.log('Useing Service: ', this.appService.getName());
      }
    }
    
    • (1): 定义一个选择使用哪一个 service 的 enum
    • (2): 使用刚刚介绍的 Using an InjectionToken object 为 SelectService 这个 number 产生一个 Token
    • (3): 建立一个 Factory function,传入 select number 用来决定要使用哪一个 service
    • (4): 使用刚刚介绍的 useValue 将 SelectService Token 赋予 ServiceList.SERVICE_1 的值并将他创建出来注入给 app.component
    • (5): 使用 useFactory 将 Factory function 的结果提供给 AppService Token 并将他创建出来注入到 app.component 中
    • (6): 在 component 的 constructor 中注入 service

这边特别介绍一下 deps 这个 property,他是 injector 要解析的 Token list,然後他列表中的值会作为 useFactory 的参数传入 factory function 中,这就是为什麽上面的例子中要先用 InjectionToken 产生一个 Token,这样 injector 才能将 SelectService 解析并将其中的值,也就是使用 useValue 赋予的值传递近 factory function 里面。

如果使用 useValue 设定值为 ServiceList.SERVICE_1 那们在 console 中会看到
https://ithelp.ithome.com.tw/upload/images/20210815/20124767nVTPjutgPn.png
app.component 中注入的 service 是 service1 的内容,而 useValue 设定值为 ServiceList.SERVICE_2
https://ithelp.ithome.com.tw/upload/images/20210815/20124767AUS3HBvDqV.png
app.component 中注入的 service 变为 service2 的内容,这就是 useFactory 的概念,不过实际上 useFactory 用法会远比上面的例子还要复杂得多,不过这边介绍的是一个概念。


结论

本章中介绍了什麽是 injector 与 provider 和他们之间的关系,用了一个客户与服务生的例子,不过一样实际上背後的原理比这个复杂得多,不过这边就是介绍他的概念不会太过钻研他背後的原理。

也介绍了 provider 可以透过使用 useClassuseValueuseFactoryuseExisting 将 component 或 module 需要的 Token 利用不同的来源建立出来并注入回去,基本上大多都使用场景都是使用 useClass 跟 useValue,不过在面对比较复杂的专案时也会需要用到另外两个,这样才能使你的专案更加的灵活。

下一篇将会介绍 Angular 的 Router,他在 Angular 中也是占有非常重要的地位,现代的网页中没有人只做一页的,所以就需要 Router 来控制不同的 url 会进到那一个页面,详细的内容就留到明天再讲解吧,那们一样明天见罗!


Reference


<<:  [拯救上班族的 Chrome 扩充套件] Chrome Extention 的讯息传接球

>>:  [Day 8] 第一主餐-django说明及环境安装

实施零信任架构以防止横向移动,XACML最不可能进行身份验证

-示例 XACML 实现 XACML 旨在支持授权,而不是身份验证。 XACML 代表“可扩展访问...

从零开始学3D游戏开发:程序基础 Part.1 变数

终於看不见起点了,四周看起来还是有很多的参赛者在努力奔跑着,欢迎你一起陪着我们学习 Roblox 游...

入场後,重要性排序

投资人入场後,一定会面临到以下三种情况,依照重要性排序,我会一一说明,到达这个阶段你所要做的事。 将...

【从零开始的 C 语言笔记】第二篇-大家的开始 - Hello World & 档案创建介绍

不怎麽重要的前言 上一篇我们成功的安装好一个程序码编辑器了,接下来我们要来学习怎麽使用它了! 写程序...

从零开始学3D游戏设计:零件介面 Part.2 完成介面

这是 Roblox 从零开始系列,使用者介面章节的第六个单元,今天我们就要来完成上个单元所制作的零件...