Angular依赖注入理解

527 阅读7分钟

参考

定义

  • 依赖,是当类需要执行特定功能时,所需要的服务或对象。DI是一种编码模式,其中的类会从外部源中请求获取依赖,而不需要我们自己创建它们。

  • 依赖注入就是不通过 new 这种方式来在类(NotificationComponent)的内部创建所依赖类(MessageService)的对象,而是在外部创建好需要依赖的类对象之后通过构造函数等方式注入进来就可以了。

好处

  • 代码的行数变少了
  • 耦合性降低了 我们的 MessageService 做了版本升级,这时我们想要将其替换为 NewMessageService,很简单,根本不需要对 NotificationComponent 做修改

image.png

可注入的

Angular系统中通过在类上添加@Injectable装饰器来告诉系统这个类(服务)是可注入的。当然了这仅仅只是告诉Angular系统这个类(服务)类是可注入的。但是这个服务可以在哪里使用,由谁提供,就得靠注入器和提供商来一起确定了。

  • 注入器: 注入器负责服务实例的创建,并把它们注入到你想要注入的类中。从而确定服务的使用范围和服务的生命周期。

  • 提供商: 服务由谁提供。Angular本身没法自动判断你是打算自行创建服务类的实例,还是等注入器来创建它。如果想通过注入器来创建,必须在每个注入器里面为每个服务指定服务提供商。

注入器(Injector)

用来管理服务。包括服务的创建,服务的获取

Angular依赖注入系统中的注入器(Injector)是多级的。实际上,应用程序中有一个与组件树平行的注入器树。你可以在组件树中的任何级别上重新配置注入器,注入提供商。

还有一点要特别注意,Angular注入器是冒泡机制的。当一个组件申请获得一个依赖时,Angular先尝试用该组件自己的注入器来满足它。如果该组件的注入器没有找到对应的提供商,它就把这个申请转给它父组件的注入器来处理。如果当前注入器也无法满足这个申请,它就继续转给它在注入器树中的父注入器。这个申请继续往上冒泡—直到Angular找到一个能处理此申请的注入器或者超出了组件树中的祖先位置为止。如果超出了组件树中的祖先还未找到,Angular就会抛出一个错误。

在我们的Angular系统中我们可以认为NgModule是一个注入器,Component也是一个注入器。

通过@NgModule()的providers将服务注入到NgModule中

NgmoduleProvidersModule模块里面所有的地方使用NgmoduleProvidersService TOKEN是NgmoduleProvidersService,提供商也是他自己

@NgModule({
    declarations: [
        NgmoduleProvidersComponent
    ],
    providers: [
        NgmoduleProvidersService,
    ],
    imports: [
        CommonModule,
        NgmoduleProvidersRoutingModule
    ]
})
export class NgmoduleProvidersModule {
}

通过@Injectable()的providedIn将服务注入到NgModule中

@Injectable()装饰器里面的元数据providedIn也可以直接指定NgModue。来告知服务可以在哪里使用。providedIn的值可以有三种:一种是Type也是NgModule、一种是字符串'root'、一种是null

providedIn: null

当providedIn是null的时候。咱们仅仅是告诉了系统这个类是可注入的。在其他的地方还使用不了。如果想使用需要在NgModule装饰器或者Component装饰器里面的元数据providers中指定。

providedIn: 'root'

root字符串就代表顶级AppModule。表明当前服务可以在整个Angular应用里面使用。而且在整个Angular应用中只有一个服务实例

@Injectable({
    providedIn: 'root'
})
export class StartupService {

    constructor() {
    }

    // TODO: 其他逻辑
}

 providedIn: NgModule

通过providedIn直接指定一个NgModule。让当前服务只能在这个指定的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 {
}

Component(组件)级注入器

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)

@NgModule({
    ...
    providers: [
        NgmoduleProvidersService,
    ],
    ...
})

上面代码中的providers对象是一个Provider(提供商)数组(当前注入器需要注入的依赖对象),在注入器中注入服务时咱们还必须指定这些提供商,否则注入器就不知道怎么来创建此服务。Angular系统中我们通过Provider来描述与Token相关联的依赖对象的创建方式。

Provider是用来描述与Token关联的依赖对象的创建方式。当我们使用Token向DI系统获取与之相关连的依赖对象时,DI 会根据已设置的创建方式,自动的创建依赖对象并返回给使用者。中间过程我们不需要过。我们只需要知道哪个Token对应哪个(或者哪些)服务就好了。通过Token来获取到对应的服务

TypeProvider

最常用,token就是服务名

@NgModule({
  ...
  providers: [MessageService], // MessageService是我们定义的服务,TypeProvider方式
})

其实是这类提供商的简写形式

ClassProvider

{
  // ...
  providers: [
    {
      provide: MessageService,
      useClass: MessageService
    }	
  ]
  // ...
}

ValueProvider

export const TOKEN_MODULE_CONFIG = new InjectionToken<Config>('TOKEN_MODULE_CONFIG');

/**
 * Config是我们自定义的一个配置对象
 */
const config = new Config();
config.version = '1.1.2';

@NgModule({
    ...
    providers: [
        {provide: TOKEN_MODULE_CONFIG, useValue: config},
    ],
    ...
})
export class ValueProviderModule {
}

工厂提供商 FactoryProvider

const MessageServiceFactory = () => {
  if (environment.production) {
    return new MessageService();
  } else {
    return new NewMessageService();
  }
};

{
  // ...
  providers: [
    {
      provide: MessageService,
      useFactory: MessageServiceFactory
    }
  ]
  // ...
}

ExistingProvider

@NgModule({
    ...
    providers: [
        ModuleExistingProviderServiceExtended, // 我们先通过TypeProvider的方式注入了ModuleExistingProviderServiceExtended
        {provide: ModuleExistingProviderService, useExisting: ModuleExistingProviderServiceExtended}
    ],
    ...
})
export class ExistingProviderModule {
}

获取依赖对象

构造函数中通过@Inject获取

   /**
     * 通过@Inject装饰器获取Token对应依赖的对象
     */
    constructor(@Inject(TOKEN_MODULE_CLASS_PROVIDER) private service: ModuleClassProviderService) {
    }

通过Injector.get(Token)获取

service: ModuleClassProviderService;

    /**
     * 借助Injector服务来获取Token对应的服务
     */
    constructor(private injector: Injector) {
       this.service = injector.get(TOKEN_MODULE_CLASS_PROVIDER);
    }

构造函数中通过Type获取

 constructor(private service: ModuleClassProviderService) {
    }

Multi Providers

让我们可以使用相同的 Token 去注册多个 Provider

const SOME_TOKEN: OpaqueToken = new OpaqueToken('SomeToken');

var injector = ReflectiveInjector.resolveAndCreate([
  provide(SOME_TOKEN, {useValue: 'dependency one', multi: true}),
  provide(SOME_TOKEN, {useValue: 'dependency two', multi: true})
]);

// dependencies == ['dependency one', 'dependency two']
var dependencies = injector.get(SOME_TOKEN);

multi: true 告诉 Angular 2的依赖注入系统,我们设置的 provider 是 multi provider。正如之前所说,我们可以使用相同的 token 值,注册不同的 provide。当我们使用对应的 token 去获取依赖项时,我们获取的是已注册的依赖对象列表。

为什么 Angular 2 中会引入 multi provider ?

  • 如果使用同一个 token 注册 provider,后面注册的 provider 将会覆盖前面已注册的 provider

  • 提供可插拔的钩子

预定义的 token

NG_VALIDATORS

自定义验证器

@Directive({
  selector: '[customValidator][ngModel]',
  providers: [
  {
    provide: NG_VALIDATORS,
    useValue: (formControl) => {
      // validation happens here
    },
    multi: true
  }
 ]
})
class CustomValidator {}

APP_INITIALIZER

该 Token 用于配置系统初始化相关的 Provider

// exe-app-v2/src/core/core_module.ts
export function configFactory(config: AppConfig) {
    return function () { config.load(); }
}

@NgModule({
      ...,
    providers: [
        // 系统启动时,加载项目的配置文件,如系统登录、首页模块的ApiUrl等信息
      { provide: APP_INITIALIZER, useFactory: configFactory, 
          deps: [AppConfig], multi: true }
    ]
})
export class CoreModule { }
APP_BOOTSTRAP_LISTENER

每个启动组件启动完成后的回调函数 执行自定义的初始化逻辑,比如 Router 模块监听启动过程

修饰符

Angular 文档中帮助我们对解析修饰符做了分类:

  • 如果 Angular 找不到想要的东西怎么办:@Optional()
  • 到哪里开始寻找,用@SkipSelf()
  • 到哪里停止寻找,用 @Host() 和 @Self()

@Optional() 表示该服务是可选的,有时候我们引入的服务是不一定存在的,或者说用户不一定会在提供商中配置注入器

export class NotificationComponent implements OnInit {
  constructor(@Optional() private msg: MessageService) {}

  ngOnInit() {
    this.msg.send();
  }
}

@Self 仅在当前组件或指令中查找