依赖注入简介

依赖注入是前端开发者也是 Angular 开发者一道很难迈过去的坎。软件只有到达了一定的复杂度才会需要各种设计原则和模式,那么依赖倒置原则(Dependency Inversion Principle )就是为了解决软件模块之间的耦合性提出的一种思想,让大型软件变的可维护,高层模块不应该依赖低层模块,两者都应该依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象。那么控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计思路,具体采用的方法就是所谓的依赖注入(Dependency Injection),通过依赖注入实现控制权的反转,除了依赖注入外,还有可以通过模板方法模式实现控制反转,那么所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。

依赖注入的基本元素:

  • @Injectable() 装饰器来提供元数据,表示一个服务可以被注入的(在之前的版本中不加也是可以被注入的,后来5.0版本改成静态注入器后必须要标识一下才可以被注入,否则会报错)
  • 注入器(Injector) 会创建依赖、维护一个容器来管理这些依赖,并尽可能复用它们,Angular 默认会创建各种注入器,甚至感觉不到他的存在,但是理解注入器的底层逻辑后再看依赖注入就更简单了
  • @Inject() 装饰器表示要在组件或者服务中注入一个服务
  • 提供者(Provider 是一个对象,用来告诉注入器应该如何获取或创建依赖值。

基本使用

在 Angular 中,通过 @angular/cli 提供的命令 ng generate service heroes/hero (简写 ng g s heroes/hero ) 可以快速的创建一个服务,创建后的服务代码如下:

// src/app/heroes/hero.service.ts
import { Injectable } from '@angular/core';

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

HeroService 通过 @Injectable() 装饰器标记为可以被注入的服务, providedIn: 'root'表示当前服务在 Root 注入器中提供,简单理解就是这个服务在整个应用所有地方都可以注入,并全局唯一实例。
添加完服务后,我们就可以在任何组件中通过构造函数注入 HeroService, 通过 TS 的构造函数赋值属性的特性设置为公开,这样组件内和模板中都可以使用该服务端的函数和方法。

简化版的代码:

import { Hero } from '../hero';
import { HeroService } from '../hero.service';

@Component({
    selector: 'app-hero-list',
    template: 'Heroes: {{heroes | json}}'
})
export class HeroListComponent implements OnInit {
    heroes!: Hero[];
    
    constructor(public heroService: HeroService) {}

    ngOnInit(): void {
        this.heroes = this.heroService.getHeroes();
    }
}

依赖注入中的Provider

Provider的作用:

  • 告诉注入器如何提供依赖值
  • 限制服务可使用的范围

在组件或者模块中通过装饰器元数据 providers 定义提供者。 比如: 类提供者。

android 依赖注入的原理 依赖注入 angular_ide

  • provide 属性是依赖令牌,它作为一个 key,在定义依赖值和配置注入器时使用,可以是一个 类的类型 、 InjectionToken 、或者字符串,甚至对象,但是不能是一个 Interface、数字和布尔类型
  • 第二个属性是一个提供者定义对象,它告诉注入器要如何创建依赖值。 提供者定义对象中的 key 可以是 useClass —— 就像这个例子中一样。 也可以是 useExistinguseValueuseFactory , 每一个 key 都用于提供一种不同类型的依赖。

TypeProvider & ClassProvider

类提供者应该是最常用的一种,文章开始中的示例就是,简写和全写的配置如下:

provides: [ Logger ]   // 简写
provides: [{ provide: Logger, useClass: Logger }]  // 全写

所有 class 定义的服务默认都是用 类提供者
指定替代性的类提供者,替换原有服务的行为实现可扩展性,这样我在使用的时候还是注入 Logger ,但是实际返回的对象是 BetterLogger
示例:

@Injectable()
export class LoggerService {
  constructor() {
  }

  logMessage(msg: string): void {
    console.log(`[LOG] ${msg}`);
  }
}

@Injectable()
export class BetterLoggerService extends LoggerService{
  constructor() {
    super();
  }

  override logMessage(msg: string): void {
    const date = new Date().toLocaleString();
    console.log(`[${date}] [LOG] ${msg}`);
  }
}

使用:

import {Component, Inject, InjectionToken} from '@angular/core';
import {BETTER_LOGGER_TOKEN, BetterLoggerService, LoggerService, silentLogger} from "./LoggerService";

@Component({
  selector: 'app-root',
  providers: [{provide: LoggerService, useClass: BetterLoggerService}],
})
export class AppComponent {

  constructor(
    private _logger: BetterLoggerService,
  ) {
    this._logger.logMessage('welcome');
  }
}

ExistingProvider

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

[ 
  NewLogger,
  // Alias OldLogger reference to NewLogger
  { provide: OldLogger, useExisting: NewLogger}
]
  • useExisting 值是一个 DI Token ,provide 也是一个 DI Token, 2个 Token 指向同一个实例
  • useClass 值是一个可以实例化的类,也就是可以 new 出来的类,这个类可以是任何类

示例:

export abstract class MinimalLogger {
  abstract logs: string[];
  abstract logInfo: (msg: string) => void;
}

{ provide: MinimalLogger, useExisting: LoggerService },
// parent.ts
class abstract Parent {
     ...
}

// alex.component.ts
providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }]
class AlexComponent {
   // ChildComponent
}

// child.component.ts
class ChildComponent {
    constructor(parent: Parent)
}

ValueProvider

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

示例:

@Injectable()
export class LoggerService {
  constructor() {
  }

  timezone: string = '?'

  logMessage(msg: string): void {
    console.log(`[LOG] ${msg}`);
  }
}

export const silentLogger  = {
  timezone: 'ust-8',
  logMessage: (msg: string) => {},
};
@Component({
  selector: 'app-root',
  providers: [{provide: BetterLoggerService, useValue: silentLogger}],
})
export class AppComponent {
  title = 'graphql-ang-demo';

  constructor(
    private _logger: BetterLoggerService,
  ) {
    this._logger.logMessage(this._logger.timezone);
    this._logger.logMessage('welcome');
  }
}

FactoryProvider

Factory需要提供一个方法,通过调用方法构建出Service。
示例:

export const FactoryLoggerService = (loggerService: LoggerService) => {
  console.log('Use a factory');
  return new BetterLoggerService();
}
@Component({
  selector: 'app-root',
  providers: [{provide: BetterLoggerService, useFactory: FactoryLoggerService}],
})
export class AppComponent {
  title = 'graphql-ang-demo';

  constructor(
    private _logger: BetterLoggerService,
  ) {
    this._logger.logMessage(this._logger.timezone);
    this._logger.logMessage('welcome');
  }

InjectionToken

每当你要注入的类型无法确定(没有运行时表示形式)时,比如在注入接口、可调用类型、数组或参数化类型时,都应使用 InjectionToken

InjectionToken 在 T 上的参数化版本,T 是 Injector 返回的对象的类型。这提供了更高级别的类型安全性。

当创建 InjectionToken 时,可以选择指定一个factory函数,该函数返回(可能通过创建)参数化类型 T 的默认值。这将使用工厂型提供者设置 InjectionToken,就像它是在应用程序的根注入器中显式定义的一样。如果使用需要注入依赖项的零参数工厂函数,则可以使用 inject 函数来这样做。参见以下示例。

export const BETTER_LOGGER_TOKEN = new InjectionToken<BetterLoggerService>('app.betterLogger',
  {
    providedIn: 'root',
    factory: () => {
      console.log('created a logger by InjectionToken')
      return new BetterLoggerService()
    },
  },
);
@Component({
  selector: 'app-root',
  // 这里不写ProvidedIn
})
export class AppComponent {
  title = 'graphql-ang-demo';

  constructor(
    @Inject(BETTER_LOGGER_TOKEN) private _logger: BetterLoggerService
  ) {
    console.log(this._logger.timezone);
    this._logger.logMessage(this._logger.timezone);
    this._logger.logMessage('welcome');
  }
}

多极注入器(MultiInjector)

通过依赖注入的概念我们知道,创建实例的工作都交给 Ioc 容器(也就是注入器)了,通过构造函数参数装饰器 @Inject(DIToken) 告诉注入器我们需要注入 DIToken 对应的依赖,注入器就会帮我们查找依赖并返回值,Angular 应用启动会默认创建相关的注入器,而且 Angular 的注入器是有层级的,类似于一个 Dom 树。

Angular 中有两个注入器层次结构:

  • ModuleInjector —— 使用 @NgModule()@Injectable() 装饰器在此层次结构中配置 ModuleInjector
  • ElementInjector —— 在每个 DOM 元素上隐式创建。除非你在 @Directive()@Component() 的 providers 属性中进行配置,否则默认情况下, ElementInjector 为空

ModuleInjector

可以通过以下两种方式之一配置 ModuleInjector :

  • 使用 @Injectable()providedIn 属性引用 NgModuleTyperootplatform 或者 any
  • 使用 @NgModule()providers 数组。

Tree-shaking and @Injectable() - 摇树优化与 @Injectable() 使用 @Injectable()providedIn 属性优于 @NgModule()providers 数组,因为使用 @Injectable()providedIn 时,优化工具可以进行摇树优化,从而删除你的应用程序中未使用的服务,以减小捆绑包尺寸。 摇树优化对于库特别有用,因为使用该库的应用程序不需要注入它。在 服务与依赖注入简介了解关于可摇树优化的提供者的更多信息。
需要特别注意:ModuleInjector@NgModule.providersNgModule.imports 属性配置。 ModuleInjector 是可以通过 NgModule.imports 递归找到的所有 providers 数组的扁平化。子 ModuleInjector 是在惰性加载其它 @NgModules 时创建的。

root和NullInjector

所有非LazyLoader模块的 providers@Injectable({providedIn: "root"}) 供应商都时在 root 根注入器中提供,那么在 root 之上还有两个注入器,一个是额外的平台 ModuleInjector ,一个是 NullInjector

android 依赖注入的原理 依赖注入 angular_Angular_02

provideIn: "any" | "root" | "platform" | NgModuleType

  • root 表示在根模块注入器(root ModuleInjector )提供依赖
  • platform 表示在平台注入器提供依赖
  • 指定模块表示在特定的特性模块提供依赖(注意循环依赖)
  • any 所有急性加载的模块都会共享同一个服务单例,惰性加载模块各自有它们自己独有的单例

ElementInjector

除了模块注入器外, Angular 会为每个 DOM 元素隐式创建 ElementInjector
可以用 @Component() 装饰器中的 providersviewProviders 属性来配置 ElementInjector 以提供服务。

@Component({
  ...
  providers: [{ provide: ItemService, useValue: { name: 'lamp' } }]
})
export class TestComponent

注意事项:

  • 在组件中提供服务时,可以通过 ElementInjector 在该组件以及子组件/指令处通过注入令牌使用该服务
  • 当组件实例被销毁时,该服务实例也将被销毁
  • Component是一种特殊类型的Directive,这意味着 @Directive()@Component() 都具有 providers 属性

解析规则

前面已经介绍了 Angular 会有2个层级的注入器,那么当组件/指令解析令牌时,Angular 分为两个阶段来解析它:

  • 针对 ElementInjector 层次结构(其父级)
  • 针对 ModuleInjector 层次结构(其父级)

Angular 会先从当前组件/指令的 ElementInjector 查找令牌,找不到会去父组件中查找,直到根组件,如果根还找不到,就去当前组件所在的模块注入器中查找,如果不是懒加载那么就是根注入器,一步一步到最顶层的 NullInjector,整个解析过程如下所示:

android 依赖注入的原理 依赖注入 angular_依赖注入_03

解析修饰符

修饰符可以更改开始(默认是自己)或结束位置,从而达到一些高级的使用场景

@Optional

@Optional() 允许 Angular 将你注入的服务视为可选服务。这样,如果无法在运行时解析它,Angular 只会将服务解析为 null,而不会抛出错误。

export class OptionalComponent {
  constructor(@Optional() public optional?: OptionalService) {}
}

@Self

@Self()让 Angular 仅查看当前组件或指令的 ElementInjector 。可以和@Optional组合使用

@Component({
  selector: 'app-self-no-data',
  templateUrl: './self-no-data.component.html',
  styleUrls: ['./self-no-data.component.css']
})
export class SelfNoDataComponent {
  constructor(@Self() @Optional() public flower?: FlowerService) { }
}

@SkipSelf

@SkipSelf()@Self() 相反,使用 @SkipSelf() ,Angular 在父 ElementInjector 中开始搜索服务,而不是从当前 ElementInjector 中开始搜索服务。

@Injectable({
    providedIn: 'root'
})
export class FlowerService {
    emoji = '🌿';
    constructor() {}
}
import { Component, OnInit, SkipSelf } from '@angular/core';
import { FlowerService } from '../flower.service';

@Component({
    selector: 'app-skipself',
    templateUrl: './skipself.component.html',
    styleUrls: ['./skipself.component.scss'],
    providers: [{ provide: FlowerService, useValue: { emoji: '🍁' } }]
})
export class SkipselfComponent implements OnInit {
    constructor(@SkipSelf() public flower: FlowerService) {}

    ngOnInit(): void {}
}

上面的示例会得到 root 注入器中的 🌿,而不是组件所在的 ElementInjector 中提供的 🍁。 如果值为 null 可以同时使用 @SkipSelf()@Optional() 来防止错误。

@Host

@Host() 使你可以在搜索提供者时将当前组件指定为注入器树的最后一站,即使树的更上级有一个服务实例,Angular 也不会继续寻找。

@Component({
  selector: 'app-host',
  templateUrl: './host.component.html',
  styleUrls: ['./host.component.css'],
  //  provide the service
  providers: [{ provide: FlowerService, useValue: { emoji: '🌼' } }]
})
export class HostComponent {
  // use @Host() in the constructor when injecting the service
  constructor(@Host() @Optional() public flower?: FlowerService) { }
}

由于 HostComponent 在其构造函数中具有 @Host() ,因此,无论 HostComponent 的父级是否可能有 flower.emoji 值,该 HostComponent 都将使用 🌼