依赖注入

依赖注入 ( Dependency Injection ) 简称 DI,是面向对象编程中的一种设计原则,用来减少代码之间的耦合度

class MailService {
  constructor(APIKEY) {}
}

class EmailSender {
  mailService: MailService
  constructor() {
    this.mailService = new MailService("APIKEY1234567890")
  }

  sendMail(mail) {
    this.mailService.sendMail(mail)
  }
}

const emailSender = new EmailSender()
emailSender.sendMail(mail)

EmailSender 类运行时要依赖 MailService 类。

以上写法的耦合度太高,代码并不健壮。如果 MailService 类改变了参数的传递方式,在 EmailSender 的构造函数中的写法也要跟着改变。

class EmailSender {
  mailService: MailService
  constructor(mailService: MailService) {
    this.mailService = mailService;
  }
}
const mailService = new MailService("APIKEY1234567890")
const emailSender = new EmailSender(mailService)

在实例化 EmailSender 类时将它的依赖项通过 constructor 构造函数参数的形式注入到类的内部,这种写法就是依赖注入

通过依赖注入降了代码之间的耦合度,增加了代码的可维护性。MailService 类中代码的更改再也不会影响 EmailSender 类。

DI 框架

Angular 有自己的 DI 框架,它将实现依赖注入的过程隐藏了,对于开发者来说只需使用很简单的代码就可以使用复杂的依赖注入功能。

在 Angular 的 DI 框架中有四个核心概念:

  1. Dependency:组件要依赖的实例对象,服务实例对象
  2. Token:获取服务实例对象的唯一标识
  3. Injector:注入器,负责创建维护服务类的实例对象并向组件中注入服务实例对象。
  4. Provider:配置注入器的对象,指定创建服务实例对象的服务类和获取实例对象的标识。

注入器 Injectors

注入器负责创建服务类实例对象,并将服务类实例对象注入到需要的组件中。

  1. 创建注入器

    import { ReflectiveInjector } from "@angular/core"
    // 服务类
    class MailService {}
    // 创建注入器并传入服务类
    // 服务实例对象的标识,默认就是类的名字
    const injector = ReflectiveInjector.resolveAndCreate([MailService])
  2. 获取注入器中的服务类实例对象

    const mailService = injector.get(MailService)
  3. 服务实例对象为单例模式,注入器在创建服务实例后会对其进行缓存

    const mailService1 = injector.get(MailService)
    const mailService2 = injector.get(MailService)
    
    console.log(mailService1 === mailService2) // true
    
  4. 不同的注入器返回不同的服务实例对象

    const injector = ReflectiveInjector.resolveAndCreate([MailService])
    // 创建一个子注入器
    const childInjector = injector.resolveAndCreateChild([MailService])
    
    const mailService1 = injector.get(MailService)
    const mailService2 = childInjector.get(MailService)
    
    console.log(mailService1 === mailService2) // false
    
  5. 服务实例的查找类似函数作用域链,当前级别可以找到就使用当前级别,当前级别找不到去父级中查找

    const injector = ReflectiveInjector.resolveAndCreate([MailService])
    // 创建一个子注入器
    const childInjector = injector.resolveAndCreateChild([])
    
    const mailService1 = injector.get(MailService)
    // 由于子级注入器没有 MailService,就会去父级注入器查找,所以这里拿到的 MailService 是属于父级注入器的
    const mailService2 = childInjector.get(MailService)
    
    console.log(mailService1 === mailService2) // true
    

提供者 Provider

  1. 配置注入器的对象,通过 Provider 就可以让注入器知道使用哪个类来创建实例对象,访问这个实例对象的唯一标识是什么。

    const injector = ReflectiveInjector.resolveAndCreate([
      { provide: MailService, useClass: MailService }
    ])
    • useClass:创建实例对象所使用的类。
    • provideToken 访问实例对象的唯一标识
  2. 访问依赖对象的标识也可以是字符串类型

    const injector = ReflectiveInjector.resolveAndCreate([
      { provide: "mail", useClass: MailService }
    ])
    const mailService = injector.get("mail")
  3. useValue 允许将一个静态值与 DI 令牌关联起来。

    const injector = ReflectiveInjector.resolveAndCreate([
      {
        provide: "Config",
        // Object.freeze 冻结对象,不允许外部修改
        useValue: Object.freeze({
          APIKEY: "API1234567890",
          APISCRET: "500-400-300"
        })
      }
    ])
    const Config = injector.get("Config")
  4. useFactory 允许通过调用工厂函数创建一个依赖对象。通过这种方式,你可以基于 DI 和应用中可用的信息创建一个动态值。

    const heroServiceFactory = (logger: Logger, userService: UserService) =>
    new HeroService(logger, userService.user.isAuthorized);
    
    export const heroServiceProvider = {
      provide: HeroService,
      useFactory: heroServiceFactory,
      deps: [Logger, UserService]
    };

    deps 属性是一个提供者令牌数组。 LoggerUserService 类作为它们自身类提供者的令牌。 注入器会根据指定的顺序将相应的服务注入到匹配的 heroServiceFactory 工厂函数参数中。

将实例对象和外部的引用建立了松耦合关系,外部通过标识获取实例对象,只要标识保持不变,内部代码怎么变都不会影响到外部。

InjectionToken

InjectionToken 对象的使用场景:

  • 来为非类的依赖选择一个提供者令牌。
  • 自定义注入行为或提供动态值

语法:

const MY_TOKEN = new InjectionToken<T>('描述信息', {
   factory?: () => T; // 默认值的工厂函数
   providedIn?: 'root' | 'platform' | 'any' | null; // 提供范围
});
  • factory:如果没有在 providers 中显式提供值,则注入时会使用该工厂函数返回的默认值。

定义了一个类型为 InjectionTokenAPP_CONFIG

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

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

参数类型是可选的 <AppConfig>'app.config' 是令牌的描述,指明了此令牌的用途。

接着,在组件中注册这个依赖提供者:

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

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

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

@Inject 装饰器

@Inject 装饰器用于显式地指定要注入的依赖项。虽然 Angular 的依赖注入(DI)通常是通过构造函数注入来自动推断类型的,但是这是基于类的类型进行注入的。 在某些情况下,Angular 无法自动推断要注入的类型或参数(比如非类的依赖项,常量,值,接口等),这时就需要使用 @Inject 装饰器。

注入值:

@NgModule({
  providers: [
    { provide: 'myValue', useValue: 28 }
  ]
})
export class AppModule {}

这里注入的值是 hardcode 的,如果需要动态值可以使用 InjectionToken

@Injectable()
export class MyService {
  constructor(@Inject('myValue') private value: number) {
    console.log(this.value); // 28
  }
}

注入接口:

interface User {
  name: string;
  age: number;
}

@Injectable({
  providedIn: 'root'
})
export class MyService {
  constructor(@Inject('user') private user: User) {
    console.log(this.user.name);
  }
}

提供 user 时可以使用 useValue 或者其他方式提供接口类型:

const user = { name: 'John', age: 30 };

@NgModule({
  providers: [
    { provide: 'user', useValue: user }
  ]
})
export class AppModule {}

inject() 函数

在 Angular 中,依赖注入(DI)有两种常见的方式:

  • 通过构造函数注入依赖
  • 通过 inject() 函数来注入依赖

虽然两者都实现了依赖注入的目标,但它们在使用方式、场景和目的上有所不同。

通过构造函数注入依赖

构造函数注入是 Angular 中最常见的依赖注入方式。在类的构造函数中,Angular 会自动分析参数的类型,并从应用程序的依赖注入容器中查找并注入匹配的依赖项。

  • 更符合 Angular 的编程模型和规范。
  • 这种方式必须在类中使用:构造函数注入只能用于类(比如组件、服务等),不适用于其他类型(如普通函数)。

inject()

inject() 函数是 Angular 9 中引入的一个新的 API,通常用于不依赖类构造函数注入的场景中使用。例如某些功能函数、工厂函数或外部注入的场景。

对于无法直接使用构造函数注入时,它非常有用

最后更新于