Angular模块划分和依赖注入的思想可以说是Angular架构核心中的核心,最近通读了一遍Angular官网上的模块和依赖注入两章,也领会到了其中的一些精髓。
Angular的模块NgModule
,其作用其实有点类似于Jave的package
和C#的namespace
,是代码组织和代码分割的一种形式。下面是Angular官网上的一段经典的NgModule
的写法:
@NgModule({
declarations: [
AppComponent,
ItemDirective
],
imports: [
BrowserModule,
FormsModule,
HttpModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
可以看出,NgModule实际上是在一个普通的class上加上一些描述性的元数据,这些元数据的意义是:
- declarations: 声明某些组件、指令和管道属于这个模块。
- exports: 公开其中的部分组件、指令和管道,以便其它模块中的组件模板中可以使用它们。
- imports: 导入其它带有组件、指令和管道的模块,这些模块中的元件都是本模块所需的。
- providers: 提供一些供应用中的其它组件使用的服务。
1. 模块间分享组件
NgModule
通过declares
来申明了一些组件(component
)和类组件(如directive
和pipe
,下文统称为组件)是隶属于自己模块的,而且这些组件和类组件必须且仅仅只能属于一个NgModule
,就好像每个人都必须是属于某一个国家。
当你要用到其他NgModule
的组件,就需要import
其他的模块了。但是这里注意,import
的模块不是所有的组件都是可以使用的,import
模块必须明确表明了它的哪些组件是可以外用的,这由exports
数组来定义。三者的关系见下图:
2. 模块间分享服务
在NgModule
的元数据中还有一个属性叫providers
,这个属性里一般是声明了一些service
。一般情况下我们是希望这些service
在模块内的组件中共享的,Angular确实也是如此设计的,因为providers
中申明的services
都是单例模式的。
但是如果我们要用到其他模块的服务,是否也是像模块间共享组件一样,通过import
把模块引入进来就可以呢?确实如此,但是我们还需要理解得更深刻一些。
在Angular的设计思想里,组件是私有化的,服务是公有化的。你可以这么来理解,把组件当作“人才”,把服务当作“知识”,知识是无国界的,可以共享,但是人才是有国界的,不能随便共享(只有exports
出去的才可以共享)。当外来模块被import
进来后,它的服务就被共享到你的模块中了。
我们知道,每个NgModule
都有injector
,里面存放着通过providers
声明过的service
。如果通过imports
导入了外来模块,那么外来模块的服务就都注入到了你所在模块的injectors
中,如下图所示。
3. 懒加载下的服务共享
如果你的app中应用到了懒加载,那么情况就会更加复杂了。因为懒加载模块是在特定的情况下app需要用到的时候才会被加载进来,所以一般情况下懒加载模块下的serivce
不会被imports
到主模块中的,也就不会注入到root injector
的,而是在root injector
下重新开辟了一个child injector
。如果你在主模块和懒加载模块都provide
了同样的service
,那么就会在两个模块中分别拥有不同的实例。而这往往是开发者不想看到的,因为服务的作用就是共享数据,而此时不知不觉有两个实例存在,每个实例单独维护一份数据,那么就会造成逻辑上的错误,更可怕的是,很多开发者并不知道这一点。所以记得在懒加载模块中不要注入跟主模块相同的服务,用主模块中的就好了。但是有时候你因为要用到某些特殊指令,又不得不导入相同的模块,比如路由模块,在主模块和懒加载模块甚至是一些特征模块中都需要导入,这个时候,就需要在主模块中用到forRoot
了。那么forRoot
到底起到一个什么作用呢?其实他们只是一个Angular模块中约定俗成的写法,主要是它们有一个返回类型ModuleWithProviders
,其实就是想把module
和providers
给区分开来,它的意思就是告诉导入模块可以共享被导入模块中的组件(被exports出的组件),但是服务注入一次就好了,以后要是再有同样的服务需要注入就忽略之,不要创建两个不同实例的服务。
懒加载模块代码如下:
import { NgModule, ModuleWithProviders } from '@angular/core';
import { MyDirective } from './my.directive';
import { FunPipe } from './fun.pipe';
import { SomeService } from './some.service';
@NgModule({
declarations: [
FunPipe,
MyDirective
],
exports: [
FunPipe,
MyDirective
]
})
export class SharedModule {
static forRoot(): ModuleWithProviders {
return {
ngModule:SharedModule,
providers:[ SomeService ]
};
}
}
我们在NgModule
的元数据中像往常一样声明和导出我们的管道和指令,但是我们不提供服务,而是在模块的类中定义一个静态方法forRoot,该方法返回一个实现Angular的 ModuleWithProviders
接口的对象。
在我们的应用模块中,导入懒加载模块并调用forRoot静态方法来提供我们的服务:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { SharedModule } from './shared/shared.module';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
SharedModule.forRoot()
],
bootstrap: [
AppComponent
]
})
export class AppModule {}
这样,在导入模块和被导入的懒加载模块中仅仅维护了一个service
实例。
4. 服务在组件中私有化
在Angular的@Component
里也有个providers
,但是如果你在组件级别注入了service
,那么这个service
就只能在该组件和它的子组件中使用了,别的组件即使在同一个模块中也不能使用这个service
。组件这个时候类似于一个家庭,这个私有的服务其实类似于传家宝,这个传家宝只能在你们这个家庭里传下去,它有很强的排他性,而且每个组件实例都会独享一份service
,这可以保证这个service
只服务于你这个组件实例,是专属于你自己的“私人银行”。比如有很多个申请表,虽然申请表都是一样的,每个服务都必须服务于自己的申请表,这种情况下就适合于用到component
级别的provide。
总结
如果想系统的学习Angular的模块和依赖注入知识,建议去Angular官网好好研读。我这里只是学习过后加入了自己的理解,又由于我最近在做关于分包加载涉及到Angular Module和DI比较多,也比较有体会,遂成此拙文。