Angular中的依赖注入

前言

相信angular中的「依赖注入」对于大部分的angular开发者都是一个不容易跨过去的坎,当然我也不例外,这篇文章构思了很久,大概从一年前就想对此来个总结,奈何不知道从何写起,主要原因是在平常的实际开发中用到的依赖注入的知识太浅,所以并不能很好的读懂文档的意思,如今有机会在实际的项目中接触到了更多依赖注入的用法,故此总结一下。

本文内容大部分来源于官方文档,因为文档中的知识点过于零散,东一锤子西一棒子的,摸不着头脑,所以我按照自己的理解,加上实际的demo来梳理一下其中的知识点。

什么是「依赖注入(dependency injection)」?

依赖注入既是设计模式,同时又是一种机制:当应用程序的一些部件(即一些依赖)需要另一些部件时, 利用依赖注入来创建被请求的部件,并将它们注入到需要它们的部件中。
在 Angular 中,依赖通常是服务,但是也可以是值,比如字符串或函数。应用的注入器(它是在启动期间自动创建的)会使用该服务或值的配置好的提供者来按需实例化这些依赖。各个不同的提供者可以为同一个服务提供不同的实现。

上面这段话用我自己的理解,通俗的来讲就是,之前我们如果想要实例化类(class)的一个对象,就要用「new」运算符来实现,现在有了依赖注入这个东西以后就不需要我们自己亲自动手去new一个对象了,我们只需要在用到的地方使用angular的「依赖注入」的语法,就可以拿到我们想要的对象了,而帮助我们创建对象的这个东西叫做「注入器」,注入器这个东西,很抽象,我们看不见摸不着,这是angular在运行过程中帮助我们自动创建的。

先把几个名词搞清楚,「服务」、「注入器」、「DI令牌」、「提供者」。

服务

在 Angular 中,服务就是一个带有 @Injectable 装饰器的类,它封装了可以在应用程序中复用的非 UI 逻辑和代码。 Angular 把组件和服务分开,是为了增进模块化程度和可复用性。

@Injectable 元数据让服务类能用于依赖注入机制中。可注入的类是用提供者进行实例化的。 各个注入器会维护一个提供者的列表,并根据组件或其它服务的需要,用它们来提供服务的实例。

DI令牌

一种用来查阅的令牌,它关联到一个依赖提供者,用于依赖注入系统中,DI令牌和注入的依赖项相映射,可以把DI令牌作为key来找到依赖项。

令牌通常是 Type 或 InjectionToken 的实例,但也可以是 any 实例。

提供者

一个实现了 Provider 接口的对象。一个提供者对象定义了如何获取与 DI 令牌(token) 相关联的可注入依赖。 注入器会使用这个提供者来创建它所依赖的那些类的实例。

Angular 会为每个注入器注册一些 Angular 自己的服务。你也可以注册应用自己所需的服务提供者。

依赖提供者会使用 DI 令牌来配置注入器,注入器会用它来提供这个依赖值的具体的、运行时版本。

1
type Provider = TypeProvider | ValueProvider | ClassProvider | ConstructorProvider | ExistingProvider | FactoryProvider | any[];

ClassProvider

1
2
3
4
5
6
7
8
interface ClassProvider extends ClassSansProvider {
// DI 令牌
provide: any
multi?: boolean

// 继承自 core/ClassSansProvider
useClass: Type<any>
}

注入器

Angular 依赖注入系统中可以在缓存中根据名字查找依赖,也可以通过配置过的「提供者」来创建依赖。 启动过程中会自动为每个模块创建一个注入器,并被组件树继承。

  • 注入器会提供依赖的一个单例,并把这个单例对象注入到多个组件中。

  • 模块和组件级别的注入器树可以为它们拥有的组件及其子组件提供同一个依赖的不同实例。

  • 你可以为同一个依赖使用不同的提供者来配置这些注入器,这些提供者可以为同一个依赖提供不同的实现。

怎么注入一个依赖?

以最常用的服务为例,在组件中注入一个服务:

上面👆介绍了,在 Angular 中服务就是一个带有 @Injectable() 装饰器的类,可以把它作为依赖,注入到组件中。同样,也要使用 @Injectable() 装饰器来表明一个组件或其它类(比如另一个服务、管道或 NgModule)拥有一个依赖,我们实际项目中绝大部分注入的依赖都是服务。

  • 注入器是主要的机制。Angular 会在启动过程中为你创建全应用级注入器以及所需的其它注入器。你不用自己创建「注入器」。

  • 该注入器会创建依赖、维护一个容器来管理这些依赖,并尽可能复用它们。

  • 提供者是一个对象,用来告诉注入器应该如何获取或创建依赖。

你的应用中所需的任何依赖,都必须使用该应用的注入器来注册一个提供者,以便注入器可以使用这个提供者来创建新实例。对于服务,该提供者通常就是服务类本身。

依赖不一定是服务 —— 它还可能是函数或值。

当 Angular 创建组件类的新实例时,它会通过查看该组件类的构造函数,来决定该组件依赖哪些服务或其它依赖项。如下:HeroListComponent 的构造函数中需要 HeroService

1
constructor(private service: HeroService) { }

当 Angular 发现某个组件依赖某个服务时,它会首先检查是否该注入器中已经有了那个服务的任何现有实例。如果所请求的服务尚不存在实例,注入器就会使用以前注册的服务提供者来制作一个,并把它加入注入器中,然后把该服务返回给 Angular。

当所有请求的服务已解析并返回时,Angular 可以用这些服务实例为参数,调用该组件的构造函数。

注入过程可以用下方非常形象和经典的一张图来概括:

个人理解:图中的Injector就是注入器,它管理着很多的依赖,Angular中「依赖项」和「DI令牌」相映射,「DI令牌」作为映射的key,「依赖项」作为映射的value,当发现构造函数中依赖HeroService时,注入器就会把HeroService当作「DI令牌」去注入器中去查找相应的「依赖项」,找到依赖项以后,然后再去查找该依赖有没有已经实例化好的对象,如果有就直接返回该对象,如果没有,注入器就会根据该依赖的「提供者」去创建对象,所以说对象是由注入器根据提供者创建的。「服务」的「提供者定义对象」默认就是 useClass,也就是说注入器会通过 new 的方式创建一个对象,此外「提供者定义对象」还可以是 useValue 、useExisting、useFactory等。

提供服务的三种方式

对于要用到的任何服务,你必须至少注册一个提供者。服务可以在自己的元数据中把自己注册为提供者,这样可以让自己随处可用。或者,你也可以为特定的模块或组件注册提供者。要注册提供者,就要在服务的 @Injectable() 装饰器中提供它的元数据,或者在 @NgModule() 或 @Component() 的元数据中。

  1. 默认情况下,Angular CLI 的 ng generate service 命令会在 @Injectable() 装饰器中提供元数据来把它注册到根注入器中。本教程就用这种方法注册了 HeroService 的提供者:
1
2
3
@Injectable({
providedIn: 'root',
})

当你在根一级提供服务时,Angular 会为 HeroService 创建一个单一的共享实例,并且把它注入到任何想要它的类中。这种在 @Injectable 元数据中注册提供者的方式还让 Angular 能够通过移除那些从未被用过的服务来优化大小。

  1. 当你使用特定的 NgModule 注册提供者时,该服务的同一个实例将会对该 NgModule 中的所有组件可用。要想在这一层注册,请用 @NgModule() 装饰器中的 providers 属性:
1
2
3
4
5
6
7
@NgModule({
providers: [
BackendService,
Logger
],

})
  1. 当你在组件级注册提供者时,你会为该组件的每一个新实例提供该服务的一个新实例。要在组件级注册,就要在 @Component() 元数据的 providers 属性中注册服务提供者。
1
2
3
4
5
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
providers: [ HeroService ]
})

定义提供者

类提供者

类提供者的语法实际上是一种简写形式,它会扩展成一个由 Provider 接口定义的提供者配置对象。下面的代码片段展示了 providers 中给出的类会如何扩展成完整的提供者配置对象。

1
providers: [Logger]

Angular 把这个 providers 值扩展为一个完整的提供者对象,如下所示。

1
[{ provide: Logger, useClass: Logger }]

扩展的提供者配置是一个具有两个属性的对象字面量:

  • provide 属性存有令牌,它作为一个 key,在定位依赖值和配置注入器时使用。

  • 第二个属性是一个提供者定义对象,它告诉注入器要如何创建依赖值。提供者定义对象中的 key 可以是 useClass —— 就像这个例子中一样。也可以是 useExisting、useValue 或 useFactory。每一个 key 都用于提供一种不同类型的依赖,我们稍后会讨论。

别名类提供者

要为类提供者设置别名,请在 providers 数组中使用 useExisting 属性指定别名和类提供者。

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

1
2
3
[ NewLogger,
// Alias OldLogger w/ reference to NewLogger
{ provide: OldLogger, useExisting: NewLogger}]

请确保你没有使用 useClass 来把 OldLogger 设为 NewLogger 的别名,因为如果这样做它就会创建两个不同的 NewLogger 实例。

注入一个配置对象

常用的对象字面量是配置对象。下列配置对象包括应用的标题和 Web API 的端点地址。

1
2
3
4
export const HERO_DI_CONFIG: AppConfig = {
apiEndpoint: 'api.heroes.com',
title: 'Dependency Injection'
};

定义和使用一个 InjectionToken 对象来为非类的依赖选择一个提供者令牌。

1
2
3
import { InjectionToken } from '@angular/core';

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

接着,用 APP_CONFIG 这个 InjectionToken 对象在组件中注册依赖提供者。

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

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

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

接口和依赖注入

虽然 TypeScript 的 AppConfig 接口可以在类中提供类型支持,但它在依赖注入时却没有任何作用。在 TypeScript 中,接口是一项设计期工件,它没有可供 DI 框架使用的运行时表示形式或令牌。

当转译器把 TypeScript 转换成 JavaScript 时,接口就会消失,因为 JavaScript 没有接口。

由于 Angular 在运行期没有接口,所以该接口不能作为令牌,也不能注入它。

使用工厂提供者

要想根据运行前尚不可用的信息创建可变的依赖值,可以使用工厂提供者。

在下面的例子中,只有授权用户才能看到 HeroService 中的秘密英雄。授权可能在单个应用会话期间发生变化,比如改用其他用户登录。

要想在 UserService 和 HeroService 中保存敏感信息,就要给 HeroService 的构造函数传一个逻辑标志来控制秘密英雄的显示。

1
2
3
4
5
6
7
8
9
constructor(
private logger: Logger,
private isAuthorized: boolean) { }

getHeroes() {
const auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
this.logger.log(`Getting heroes for ${auth} user.`);
return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

要实现 isAuthorized 标志,可以用工厂提供者来为 HeroService 创建一个新的 logger 实例。

1
2
const heroServiceFactory = (logger: Logger, userService: UserService) =>
new HeroService(logger, userService.user.isAuthorized);

这个工厂函数可以访问 UserService。你可以同时把 Logger 和 UserService 注入到工厂提供者中,这样注入器就可以把它们传给工厂函数了。

1
2
3
4
5
export const heroServiceProvider =
{ provide: HeroService,
useFactory: heroServiceFactory,
deps: [Logger, UserService]
};
  • useFactory 字段指定该提供者是一个工厂函数,其实现代码是 heroServiceFactory

  • deps 属性是一个提供者令牌数组。Logger 和 UserService 类都是自己类提供者的令牌。该注入器解析了这些令牌,并把相应的服务注入到 heroServiceFactory 工厂函数的参数中。

通过把工厂提供者导出为变量 heroServiceProvider,就能让工厂提供者变得可复用。

1
2
3
4
5
6
7
8
9
10
11
12
import { Component } from '@angular/core';
import { heroServiceProvider } from './hero.service.provider';

@Component({
selector: 'app-heroes',
providers: [ heroServiceProvider ],
template: `
<h2>Heroes</h2>
<app-hero-list></app-hero-list>
`
})
export class HeroesComponent { }

多级注入器

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

  • ModuleInjector 层次结构:使用 @NgModule() 或 @Injectable() 注解在此层次结构中配置 ModuleInjector。

  • ElementInjector 层次结构:在每个 DOM 元素上隐式创建。默认情况下,ElementInjector 是空的,除非你在 @Directive() 或 @Component() 的 providers 属性中配置它。

ModuleInjector

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

  • 使用 @Injectable() 的 providedIn 属性引用 @NgModule() 或 root

  • 使用 @NgModule() 的 providers 数组

摇树优化与 @Injectable()

使用 @Injectable() 的 providedIn 属性优于 @NgModule() 的 providers 数组,因为使用 @Injectable() 的 providedIn 时,优化工具可以进行摇树优化,从而删除你的应用程序中未使用的服务,以减小捆绑包尺寸。摇树优化对于库特别有用,因为使用该库的应用程序不需要注入它。

ModuleInjector 由 @NgModule.providers 和 NgModule.imports 属性配置。ModuleInjector 是可以通过 NgModule.imports 递归找到的所有 providers 数组的扁平化。

子 ModuleInjector 是在惰性加载其它 @NgModules 时创建的。

平台注入器

在 root 之上还有两个注入器,一个是额外的 ModuleInjector,一个是 NullInjector()。

另一个额外的ModuleInjector这里不作赘述,感兴趣的可以去官网了解。

层次结构中的下一个父注入器是 NullInjector(),它是树的顶部。如果你在树中向上走了很远,以至于要在 NullInjector() 中寻找服务,那么除非使用 @Optional(),否则将收到错误消息,因为最终所有东西都将以 NullInjector() 结束并返回错误,或者对于 @Optional(),返回 null。

下图展示了前面各段落描述的 root ModuleInjector 及其父注入器之间的关系。

ElementInjector

Angular 会为每个 DOM 元素隐式创建 ElementInjector。

可以用 @Component() 装饰器中的 providers 或 viewProviders 属性来配置 ElementInjector 以提供服务。

1
2
3
4
5
@Component({

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

解析规则

当为组件/指令解析令牌时,Angular 分为两个阶段来解析它:

  1. 针对 ElementInjector 层次结构(其父级)。

  2. 针对 ModuleInjector 层次结构(其父级)。

当组件声明依赖项时,Angular 会尝试使用它自己的 ElementInjector 来满足该依赖。 如果组件的注入器缺少提供者,它将把请求传给其父组件的 ElementInjector。

这些请求将继续转发,直到 Angular 找到可以处理该请求的注入器或用完祖先 ElementInjector。

如果 Angular 在任何 ElementInjector 中都找不到提供者,它将返回到发起请求的元素,并在 ModuleInjector 层次结构中进行查找。如果 Angular 仍然找不到提供者,它将引发错误。

如果你已在不同级别注册了相同 DI 令牌的提供者,则 Angular 会用遇到的第一个来解析该依赖。比如,如果提供者已经在需要此服务的组件中本地注册了,则 Angular 不会再寻找同一服务的其它提供者。

解析修饰符

可以使用 @Optional(),@Self(),@SkipSelf() 和 @Host() 来修饰 Angular 的解析行为。从 @angular/core 导入它们,并在注入服务时在组件类构造函数中使用它们。

修饰符的类型

解析修饰符分为三类:

  • 如果 Angular 找不到你要的东西该怎么办,用 @Optional()

  • 从哪里开始寻找,用 @SkipSelf()

  • 到哪里停止寻找,用 @Host() 和 @Self()

默认情况下,Angular 始终从当前的 Injector 开始,并一直向上搜索。修饰符使你可以更改开始(默认是自己)或结束位置。

另外,你可以组合除 @Host() 和 @Self() 之外的所有修饰符,当然还有 @SkipSelf() 和 @Self()。

@Optional()

@Optional() 允许 Angular 将你注入的服务视为可选服务。这样,如果无法在运行时解析它,Angular 只会将服务解析为 null,而不会抛出错误。在下面的范例中,服务 OptionalService 没有在 @NgModule() 或组件类中提供,所以它没有在应用中的任何地方。

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

@Self()

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

@Self() 的一个好例子是要注入某个服务,但只有当该服务在当前宿主元素上可用时才行。为了避免这种情况下出错,请将 @Self() 与 @Optional() 结合使用。

@SkipSelf()

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

@Host()

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

最后

由于angular的依赖注入知识体系实在是过于庞大,本文也仅仅是了解了下它的基础用法,但是掌握了这些也足够我们在平常的开发中使用了,当然只学习用法和真正实践还是有很大区别的,有机会还是要多实践,除此之外angular还有很多依赖注入的高级实践,等以后有机会学习实践以后会再来补充文章。最后,angular官网文档yyds!