依赖注入简介
依赖注入是前端开发者也是 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 定义提供者。 比如: 类提供者。
- provide 属性是依赖令牌,它作为一个 key,在定义依赖值和配置注入器时使用,可以是一个 类的类型 、 InjectionToken 、或者字符串,甚至对象,但是不能是一个 Interface、数字和布尔类型
- 第二个属性是一个提供者定义对象,它告诉注入器要如何创建依赖值。 提供者定义对象中的 key 可以是 useClass —— 就像这个例子中一样。 也可以是
useExisting
、useValue
或useFactory
, 每一个 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
属性引用NgModuleType
、root
、platform
或者any
。 - 使用
@NgModule()
的providers
数组。
Tree-shaking and
@Injectable()
- 摇树优化与@Injectable()
使用@Injectable()
的providedIn
属性优于@NgModule()
的providers
数组,因为使用@Injectable()
的providedIn
时,优化工具可以进行摇树优化,从而删除你的应用程序中未使用的服务,以减小捆绑包尺寸。 摇树优化对于库特别有用,因为使用该库的应用程序不需要注入它。在 服务与依赖注入简介了解关于可摇树优化的提供者的更多信息。
需要特别注意:ModuleInjector
由@NgModule.providers
和NgModule.imports
属性配置。ModuleInjector
是可以通过NgModule.imports
递归找到的所有providers
数组的扁平化。子ModuleInjector
是在惰性加载其它@NgModules
时创建的。
root和NullInjector
所有非LazyLoader
模块的 providers
和 @Injectable({providedIn: "root"})
供应商都时在 root
根注入器中提供,那么在 root
之上还有两个注入器,一个是额外的平台 ModuleInjector
,一个是 NullInjector
。
provideIn: "any" | "root" | "platform" | NgModuleType
-
root
表示在根模块注入器(root ModuleInjector )提供依赖 -
platform
表示在平台注入器提供依赖 - 指定模块表示在特定的特性模块提供依赖(注意循环依赖)
-
any
所有急性加载的模块都会共享同一个服务单例,惰性加载模块各自有它们自己独有的单例
ElementInjector
除了模块注入器外, Angular 会为每个 DOM 元素隐式创建 ElementInjector
。
可以用 @Component()
装饰器中的 providers
或 viewProviders
属性来配置 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
,整个解析过程如下所示:
解析修饰符
修饰符可以更改开始(默认是自己)或结束位置,从而达到一些高级的使用场景
@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
都将使用 🌼