依赖注入(DI -- Dependency Injection)是一种重要的应用设计模式。Angular里面也有自己的DI框架,在设计应用时经常会用到它,它可以我们的开发效率和模块化程度。
依赖,是当类需要执行其功能时,所需要的服务或对象。DI是一种编码模式,其中的类会从外部源中请求获取依赖,而不需要我们自己创建它们。
Angular系统中通过在类上添加@Injectable装饰器来告诉系统这个类(服务)是可注入的。当然了这仅仅只是告诉Angular系统这个类(服务)类是可注入的。但是这个服务可以在哪里使用,由谁提供,就得靠注入器和提供商来一起确定了。
注入器: 注入器负责服务实例的创建,并把它们注入到你想要注入的类中。从而确定服务的使用范围和服务的生命周期。
提供商: 服务由谁提供。Angular本身没法自动判断你是打算自行创建服务类的实例,还是等注入器来创建它。如果想通过注入器来创建,必须在每个注入器里面为每个服务指定服务提供商。
一 注入器(Injector)
Angular依赖注入中的注入器(Injector),用来管理服务。包括服务的创建,服务的获取。
Angular依赖注入系统中的注入器(Injector)是多级的。实际上,应用程序中有一个与组件树平行的注入器树。你可以在组件树中的任何级别上重新配置注入器,注入提供商。
还有一点要特别注意,Angular注入器是冒泡机制的。当一个组件申请获得一个依赖时,Angular先尝试用该组件自己的注入器来满足它。如果该组件的注入器没有找到对应的提供商,它就把这个申请转给它父组件的注入器来处理。如果当前注入器也无法满足这个申请,它就继续转给它在注入器树中的父注入器。这个申请继续往上冒泡—直到Angular找到一个能处理此申请的注入器或者超出了组件树中的祖先位置为止。如果超出了组件树中的祖先还未找到,Angular就会抛出一个错误。
所有所有的一切最终都是服务组件的。每个组件的注入器其实包含两部分:组件本身注入器的,组件所在NgMoudle对应的注入器。
在我们的Angular系统中我们可以认为NgModule是一个注入器,Component也是一个注入器。
1.1 NgMoudle(模块)级注入器
NgMoudle(模块)级注入器会告诉Angualr系统把服务作用在NgModule上。这个服务器可以在这个NgModule范围下所有的组件上使用。NgModule级注入服务有两种方式:一个是在 @NgModule()的providers元数据中指定、另一种是直接在@Injectable()的providedIn选项中指定模块类。
NgMoudle级注入器两种方式:@NgModule() providers 元数据中指定、或者直接在@Injectable() 的 providedIn 选项中指定某个模块类。
1.1.1 通过@NgModule()的providers将服务注入到NgModule中
通过@NgModule()的providers将服务注入到NgModule中,限制服务只能在当前NgModule里面使用。
export interface NgModule {
...
/**
* 本模块需要创建的服务。这些服务可以在本模块的任何地方使用。
* NgModule我们可以认为是注入器,Provider是提供商。注入器通过提供商的信息来创建服务
*/
providers?: Provider[];
...
}
关于Provider(提供商)更加具体的用户我们会在下文做详细的解释。
比如如下的代码我们先定义一个NgmoduleProvidersService服务类。当前类只是添加了@Injectable()告诉Angular系统这是一个可注入的服务。
import {Injectable} from '@angular/core';
/**
* 我们将在NgmoduleProvidersModule中注入该服务。
* 然后在NgmoduleProvidersComponent里面使用该服务
*/
@Injectable()
export class NgmoduleProvidersService {
constructor() {
}
// TODO:其他逻辑
}
接下来,想在NgmoduleProvidersModule模块里面所有的地方使用该服务。很简单,我们在@NgModule元数据providers里面指定这个类NgmoduleProvidersService就好了。(该服务的TOKEN是NgmoduleProvidersService,提供商也是他自己。关于提供商更加具体的用法,我们会在下文详细讲解)
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {NgmoduleProvidersComponent} from './ngmodule-providers.component';
import {NgmoduleProvidersRoutingModule} from './ngmodule-providers-routing.module';
import {NgmoduleProvidersService} from './ngmodule-providers.service';
@NgModule({
declarations: [
NgmoduleProvidersComponent
],
providers: [
NgmoduleProvidersService,
],
imports: [
CommonModule,
NgmoduleProvidersRoutingModule
]
})
export class NgmoduleProvidersModule {
}
1.1.2 通过@Injectable()的providedIn将服务注入到NgModule中
@Injectable()装饰器里面的元数据providedIn也可以直接指定NgModue。来告知服务可以在哪里使用。providedIn的值可以有三种:一种是Type
export interface InjectableDecorator {
/**
* providedIn有三种值:Type、 ‘root’、 null
* Type指的是NgModule
*/
(): TypeDecorator;
(options?: {
providedIn: Type | 'root' | null;
} & InjectableProvider): TypeDecorator;
new (): Injectable;
new (options?: {
providedIn: Type | 'root' | null;
} & InjectableProvider): Injectable;
}
当providedIn是null的时候。咱们仅仅是告诉了系统这个类是可注入的。在其他的地方还使用不了。如果想使用需要在NgModule装饰器或者Component装饰器里面的元数据providers中指定。
1.1.2.1 providedIn: 'root'
providedIn: 'root'。咱们可以简单的认为root字符串就代表顶级AppModule。表明当前服务可以在整个Angular应用里面使用。而且在整个Angular应用中只有一个服务实例。
比如如下的代码我们定义一个StartupService服务类。 providedIn: 'root'。则
StartupService这个类当前项目的任务地方注入使用。而且都是同一份实例对象。
import {Injectable} from '@angular/core';
/**
* StartupService可以在系统的任务地方使用
*/
@Injectable({
providedIn: 'root'
})
export class StartupService {
constructor() {
}
// TODO: 其他逻辑
}
1.1.2.2 providedIn: NgModule
providedIn: NgModule。通过providedIn直接指定一个NgModule。让当前服务只能在这个指定的NgModule里面使用。
而且providedIn: NgModule这种情况是可以摇树优化。只要在服务本身的 @Injectable() 装饰器中指定提供商,而不是在依赖该服务的NgModule或组件的元数据中指定,你就可以制作一个可摇树优化的提供商。当前前提是这个NgModule是懒加载的。
摇树优化是指一个编译器选项,意思是把应用中未引用过的代码从最终生成的包中移除。如果提供商是可摇树优化的,Angular编译器就会从最终的输出内容中移除应用代码中从未用过的服务。 这会显著减小你的打包体积。
providedIn: NgModule使用的时候有一个特别要特别注意的地方。举个例子比如我们想在NgmoduleProvidersModule模块中使用NgmoduleProviderInModuleService服务。如下的写法是不对的。
import { Injectable } from '@angular/core';
import {NgmoduleProvidersModule} from './ngmodule-providers.module';
@Injectable({
providedIn: NgmoduleProvidersModule
})
export class NgmoduleProviderInModuleService {
constructor() { }
}
编译的时候会抛出一个警告信息,编译不过。
WARNING in Circular dependency detected:
为了解决这个异常信息,让代码能正常编译,我们需要借助一个NgModule(NgmoduleProvidersResolveModule名字你随便来)来过渡下。这个过渡NgModule赋值给providedIn。最后在我们真正想使用该服务的NgModule里面imports这个过渡NgModule。说的有点绕来绕去的。我们直接用代码来说明。
// 需要在模块NgmoduleProvidersModule里面使用的服务NgmoduleProviderInModuleService
import {Injectable} from '@angular/core';
import {NgmoduleProvidersResolveModule} from './ngmodule-providers-resolve.module';
/**
* providedIn中直接指定了当前服务可以在哪个模块使用
* 特别说明:我们想在NgmoduleProvidersModule模块里面使用该服务,
* 如果providedIn直接写NgmoduleProvidersModule,会报编译错误,
* 所以我们定义了一个中间模块NgmoduleProvidersResolveModule,
* 然后在NgmoduleProvidersModule里面引入了NgmoduleProvidersResolveModule。
*
* NgmoduleProvidersResolveModule相当于一个过渡的作用
*/
@Injectable({
providedIn: NgmoduleProvidersResolveModule
})
export class NgmoduleProviderInModuleService {
constructor() {
}
// TODO: 其他逻辑
}
// 过渡NgModule NgmoduleProvidersResolveModule
import {NgModule} from '@angular/core';
/**
* providedIn: NgModule的时候NgModule不能直接写对应的NgModule,
* 需要一个过渡的NgModule。否则编译报错:WARNING in Circular dependency detected
*/
@NgModule({
})
export class NgmoduleProvidersResolveModule {
}
// NgmoduleProvidersModule 服务将在该模块里面使用。
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {NgmoduleProvidersComponent} from './ngmodule-providers.component';
import {NgmoduleProvidersRoutingModule} from './ngmodule-providers-routing.module';
import {NgmoduleProvidersService} from './ngmodule-providers.service';
import {NgmoduleProvidersResolveModule} from './ngmodule-providers-resolve.module';
@NgModule({
declarations: [
NgmoduleProvidersComponent
],
providers: [
NgmoduleProvidersService,
],
imports: [
CommonModule,
/**
* 导入了过渡的NgModule
*/
NgmoduleProvidersResolveModule,
NgmoduleProvidersRoutingModule
]
})
export class NgmoduleProvidersModule {
}
1.2 Component(组件)级注入器
组件级注入器,每个组件也是一个注入器。通过在组件级注入器中注入服务。这样该组件实例或其下级组件实例都可以使用这个服务(当然我们也可以设置只在当前组件使用,子组件不能使用。这个就涉及到viewProviders和providers的区别了)。组件注入器提供的服务具有受限的生命周期。该组件的每个新实例都会获得自己的一份服务实例。当销毁组件实例时,服务实例也会被同时销毁。所以组件级别的服务和组件是绑定在一起的。一起创建一起消失。
我们通过一个简单的实例来看看组件级注入器的使用。
首先定义一个ComponentInjectService服务。
import { Injectable } from '@angular/core';
/**
* 当前服务在组件里面使用,会在需要使用的组件里面注入
*/
@Injectable()
export class ComponentInjectService {
constructor() { }
}
然后在组件里面注入
import {ComponentInjectService} from './component-inject.service';
@Component({
selector: 'app-ngmodule-providers',
templateUrl: './ngmodule-providers.component.html',
styleUrls: ['./ngmodule-providers.component.less'],
providers: [ComponentInjectService], // providers提供的服务在当前组件和子组件都可以使用
// viewProviders: [ComponentInjectService], // viewProviders提供的服务在当前组件使用
})
export class NgmoduleProvidersComponent implements OnInit {
constructor(private service: ComponentInjectService) {
}
ngOnInit() {
}
}
二,提供商(Provider)
上面所有的实例代码,咱们往注入器里面注入服务的时候,使用的是最简单的一种方式TypeProvider,也是咱们用的最多的一种方式。不管是@NgModule装饰器里面还是@Component装饰器里面。providers元数据里面都是直接写了服务类。类似如下的代码。
@NgModule({
...
providers: [
NgmoduleProvidersService,
],
...
})
上面代码中的providers对象是一个Provider(提供商)数组(当前注入器需要注入的依赖对象),在注入器中注入服务时咱们还必须指定这些提供商,否则注入器就不知道怎么来创建此服务。Angular系统中我们通过Provider来描述与Token相关联的依赖对象的创建方式。
简而言之Provider是用来描述与Token关联的依赖对象的创建方式。当我们使用Token向DI系统获取与之相关连的依赖对象时,DI 会根据已设置的创建方式,自动的创建依赖对象并返回给使用者。中间过程我们不需要过。我们只需要知道哪个Token对应哪个(或者哪些)服务就好了。通过Token来获取到对应的服务。所以关于Povider我们重点需要知道以下两个东西:Token,Token对应对象的创建方式。
2.1 Povider Token
Token的作用是用来标识依赖对象的,Token值可以是Type、InjectionToken、OpaqueToken类的实例或字符串。通常不推荐使用字符串,因为如果使用字符串存在命名冲突的可能性比较高。
你可以简单的认为Token是依赖对象的key。在我们需要使用依赖对象的时候我们可以通过这个key找到依赖对象。
2.2 对象的创建方式
给出了依赖对象的创建方式,注入器才能知道怎么去创建对象。Provider有如下几种方式:TypeProvider ,ValueProvider,ClassProvider,ConstructorProvider, ExistingProvider,FactoryProvider,any[]。
export declare type Provider = TypeProvider | ValueProvider | ClassProvider | ConstructorProvider | ExistingProvider | FactoryProvider | any[];
ConstructorProvider这种方式,咱们就不考虑了,我是在是没找到这种方式的使用场景。
2.2.1 TypeProvider
export interface TypeProvider extends Type {
}
TypeProvider用于告诉Injector(注入器),使用给定的Type创建对象,并且Token也是给定的Type。这也是我们用的最多的一种方式。比如如下。就是采用的TypeProvider方式。
@NgModule({
...
providers: [NgmoduleProvidersService], // NgmoduleProvidersService是我们定义的服务,TypeProvider方式
})
2.2.2 ClassProvider
ClassProvider用于告诉Injector(注入器),useClass指定的Type创建的对应对象就是Token对应的对象。
export interface ClassSansProvider {
/**
* token生成对象对应的class.
* 用该class生成服务对象
*/
useClass: Type;
}
export interface ClassProvider extends ClassSansProvider {
/**
* 用于设置与依赖对象关联的Token值,Token值可能是Type、InjectionToken、OpaqueToken的实例或字符串
*/
provide: any;
/**
* 用于标识是否multiple providers,若是multiple类型,则返回与Token关联的依赖对象列表
* 简单来说如果multi是true的话,通过provide(Token)获取的依赖对象是一个列表。
* 同一个Token可以注入多个服务
*/
multi?: boolean;
}
简单使用
export const TOKEN_MODULE_CLASS_PROVIDER = new InjectionToken('TOKEN_MODULE_CLASS_PROVIDER');
// ModuleClassProviderService类是我们依赖对象
@NgModule({
...
providers: [
{
provide: TOKEN_MODULE_CLASS_PROVIDER, useClass: ModuleClassProviderService
}
],
...
})
export class ClassProviderModule {
}
2.2.3 ValueProvider
ValueProvider用于告诉Injector(注入器),useValue指定的值(可以是具体的对象也可以是string ,number等等之类的值)就是Token依赖的对象。
export interface ValueSansProvider {
/**
* 需要注入的值
*/
useValue: any;
}
export interface ValueProvider extends ValueSansProvider {
/**
* 用于设置与依赖对象关联的Token值,Token值可能是Type、InjectionToken、OpaqueToken的实例或字符串
*/
provide: any;
/**
* 用于标识是否multiple providers,若是multiple类型,则返回与Token关联的依赖对象列表
* 简单来说如果multi是true的话,通过provide(Token)获取的依赖对象是一个列表。
* 同一个Token可以注入多个服务
*/
multi?: boolean;
}
简单实例。
export const TOKEN_MODULE_CONFIG = new InjectionToken('TOKEN_MODULE_CONFIG');
/**
* Config是我们自定义的一个配置对象
*/
const config = new Config();
config.version = '1.1.2';
@NgModule({
...
providers: [
{provide: TOKEN_MODULE_CONFIG, useValue: config},
],
...
})
export class ValueProviderModule {
}
2.2.4 FactoryProvider
FactoryProvider 用于告诉 Injector (注入器),通过调用 useFactory对应的函数,返回Token对应的依赖对象。
export interface FactorySansProvider {
/**
* 用于创建对象的工厂函数
*/
useFactory: Function;
/**
* 依赖对象列表(你也可以简单的认为是创建对象构造函数里面需要的依赖对象)
*/
deps?: any[];
}
export interface FactoryProvider extends FactorySansProvider {
/**
* 用于设置与依赖对象关联的Token值,Token值可能是Type、InjectionToken、OpaqueToken的实例或字符串
*/
provide: any;
/**
* 用于标识是否multiple providers,若是multiple类型,则返回与Token关联的依赖对象列表
* 简单来说如果multi是true的话,通过provide(Token)获取的依赖对象是一个列表。
* 同一个Token可以注入多个服务
*/
multi?: boolean;
}
useFactory对应一个函数,该函数需要的对象通过deps提供,deps是一个Token数组。
// TOKEN
export const TOKEN_FACTORY_MODULE_DEPS = new InjectionToken('TOKEN_FACTORY_MODULE_DEPS');
export const TOKEN_FACTORY_MODULE = new InjectionToken('TOKEN_FACTORY_MODULE');
/**
* 创建ModuleFactoryProviderService对象,
* 该对象依赖另一个服务,通过deps提供
*/
function moduleServiceFactory(initValue) {
return new ModuleFactoryProviderService(initValue);
}
@NgModule({
...
providers: [
{ // 创建TOKEN_FACTORY_MODULE对应的服务时候,需要依赖的值
provide: TOKEN_FACTORY_MODULE_DEPS,
useValue: 'initValue'
},
{
provide: TOKEN_FACTORY_MODULE,
useFactory: moduleServiceFactory,
deps: [TOKEN_FACTORY_MODULE_DEPS]
}
],
...
})
export class FactoryProviderModule {
}
2.2.5 ExistingProvider
ExistingProvider用于告诉Injector(注入器),想获取Token(provide)对应的对象的时候,使用useExisting(Token)对应的对象。
一定要记住useExisting对应的值也是一个Token。
export interface ExistingSansProvider {
/**
* 已经存在的 `token` (等价于 `injector.get(useExisting)`)
*/
useExisting: any;
}
export interface ExistingProvider extends ExistingSansProvider {
/**
* 用于设置与依赖对象关联的Token值,Token值可能是Type、InjectionToken、OpaqueToken的实例或字符串
*/
provide: Type;
/**
* 用于标识是否multiple providers,若是multiple类型,则返回与Token关联的依赖对象列表
* 简单来说如果multi是true的话,通过provide(Token)获取的依赖对象是一个列表。
* 同一个Token可以注入多个服务
*/
multi?: boolean;
}
实例代码。
@NgModule({
...
providers: [
ModuleExistingProviderServiceExtended, // 我们先通过TypeProvider的方式注入了ModuleExistingProviderServiceExtended
{provide: ModuleExistingProviderService, useExisting: ModuleExistingProviderServiceExtended}
],
...
})
export class ExistingProviderModule {
}
三,获取依赖对象
通过上面的讲解,我们已经知道怎么的在指定的注入器里面通过提供商注入相应的依赖对象。如果我们想在指定的地方(一般是组件里面)使用依赖对象,就得先拿到对象。接下来我们就得叨叨怎么拿到这个对象了。
通过提供者(providers)注入服务的时候,每个服务我们都给定了Token(Provider里面的provide对象对应的值)。TypeProvider例外,其实TypeProvider虽然没有明确的指出Token。其实内部的处理,Token就是TypeProvider设置的Type。
我们总结出获取依赖对象有三种方式:
3.1 构造函数中通过@Inject获取
借助@Inject装饰器获取到指定的依赖对象。@Inject的参数就是需要获取的依赖对象对应的Token。
/**
* 通过@Inject装饰器获取Token对应依赖的对象
*/
constructor(@Inject(TOKEN_MODULE_CLASS_PROVIDER) private service: ModuleClassProviderService) {
}
3.2 通过Injector.get(Token)获取
先在构造函数中把Injector对象注入进来,然后在通过Injector.get(Token)获取对象。同样参数也是依赖对象对应的Token。
service: ModuleClassProviderService;
/**
* 借助Injector服务来获取Token对应的服务
*/
constructor(private injector: Injector) {
this.service = injector.get(TOKEN_MODULE_CLASS_PROVIDER);
}
3.3 构造函数中通过Type获取
直接在构造函数中通过Type来获取,这种获取方式有个前提。必须是TypeProvider方式提供的服务。
constructor(private service: ModuleClassProviderService) {
}
四,Provider中的multi
上面讲提供商(Provider)的时候多次出现了multi。multi表示同一个Token对应的服务可以是多个。当使用multi的时候。通过Token获取依赖服务的时候是一个服务数组。其实也很好理解。比如网络拦截器。是允许同一个Token有多个服务。每个拦截器做不同的逻辑处理。
文章最后给出文章涉及到的实例代码下载地址https://github.com/tuacy/angular-inject,同时我们对Angular依赖注入的使用做一个简单的总结。Angular里面使用依赖注入步骤:
- 1.定义依赖对象的业务逻辑。
就是定义依赖对象服务类,确定服务类需要干哪些事情。
- 2.明确我们依赖对象的作用范围。
确定注入器,是用NgModule注入器呢,实时Component注入器。
- 3.依赖对象的Token确定。
依赖对象的获取都是通过Token去获取的。
- 4.依赖对象提供商的确定。
Provider用那种方式,TypeProvider呢,还是ValueProvider呢等等。
- 5.在需要使用依赖对象的地方获取到依赖对象。