依赖注入(DI)是现在软件开发中常用的一个设计模式,它能显著的增强软件系统的灵活性,可靠性和可测试性。非常多的软件框架使用了DI设计模式,包括大家熟知的Spring (Java平台)等。Angular也使用了DI。
基本语法
举一个场景来帮助说明问题,现在有一个TodoList的组件,依赖于一个BackendService的服务类来提供数据。那么,按照Angular中经典的写法就是:
在服务类的定义中,用Injectable来装饰服务类,声明其是可以被用来注入的
1 | import { Injectable } from '@angular/core'; |
在组件类的定义中,使用构造函数的参数来接收注入的依赖类实例
1 | export class TodoList { |
这两个类的关系如图所示:
在上图中,大家可以看到:生成BackendService的实例并注入到TodoList中这个动作实际上是由注入器(Injector)和构造器(Provider)共同来完成的,Injector主要完成注入,Provider用来产生对象的实例,就好像在别的框架中的工厂一样(Factory)。注入器(Injector)是有Angular框架来维护的,每个注入器中可以包括零个或多个构造器(Provider)
注入器(Injector)的分类
在Angular中,有三类或者叫三个层级的注入器(Injector)可供使用:
- 组件级 - Component
- 模块级 - NgModule
- 全局 - root
这些注入器都可以包含零个或多个构造器,当类需要注入依赖的时候,系统在当前层级进行查找,看看是否有合适的构造器,如果没有,则交给上一级的注入器进行查找。
再次重申,注册器(Injector)是由Angular框架维护的,我们提供的只是构造器
依赖类的查找顺序
在应用运行时,依赖查找的顺序是:首先在组件级注册器中查找是否有对应依赖类的Provider,如果找不到,则到上一级组件的注入器中查找,如果上一级逐渐找不到,就再上一级,组件查找完后,就到组件所属的模块级注册器进行查找,如果仍然找不到,就到全局进行查找。如果最后在全局也没找到,则报错。
以本文开头的代码例子来说,就是Angular框架知道TodoList需要一个BackendService的依赖,它首先查找TodoList自身的注入器(注意: 注入器不是我们维护的,是框架自身维护的,所以每个组件其实都有一个注入器)看看是否有BackendService的构造器,当然在本例中是找不到(因为没声明),然后就在TodoList所属的模块的注入器中查找,看看是否有BackendService的构造器,当然还是找不到, 最后只能到全局注入器中去查找,发现在全局注入器中含有BackendService的构造器。这样就通过全局的注入器完成了依赖注入。
从以上的查找路径可以看到,依赖查找的顺序是从底往上的,也就是说如果有相同的依赖类,底层定义的会覆盖上层定义的。
Provider如何声明
- 全局: 在@Injectable()中定义 - providedIn属性
- 模块级: 在@NgModule()中定义 - providers属性
- 组件级: 在@Component()中定义 - providers属性
定义使用全局Provider
全局的Provider, 也叫根(root)注入器,定义的方式就是在Injectable中声明providedIn属性为root,就像我们上面的例子一样,
1 | ({ |
在整个应用中,只有一个root注入器(Injector), 声明为root,或者在AppModule中使用providers进行注册,效果是一样的。而且在root注册器中,每个类只会产生一个实例(单例模式)。
定义使用模块级Provider
1 | { |
在模块级中声明的Provider,只对本模块中的组件产生作用。
定义使用组件级Provider
1 | { |
在组件级别中声明的Provider, 只对当前组件或是组件树下的子组件其作用。