目录
前言
Angular模块是带有**@NgModule装饰器函数的类。@NgModule接收一个元数据对象,该对象告诉 Angular 如何编译和运行模块代码。 它标记出该模块拥有的组件、指令和管道, 并把它们的一部分公开出去,以便外部组件使用它们。 它可以向应用的依赖注入器中添加服务提供商。参考例子代码:https://angular.cn/docs/ts/latest/guide/ngmodule.html。
Angular模块化
Angular模块把组件、指令和管道打包成内聚的功能块,每个模块聚焦于一个特性区域、业务领域、工作流或通用工具。
Angular模块是一个由@NgModule装饰器提供元数据的类,元数据包括:
- 声明哪些组件、指令、管道属于该模块;
- 公开某些类,以便其它的组件模板可以使用它们;
- 导入其它模块,从其它模块中获得本模块所需的组件、指令和管道;
- 在应用程序级提供服务,以便应用中的任何组件都能使用它。
每个Angular应用至少有一个模块类——根模块,我们将通过引导根模块来启动应用。
对于组件很少的简单应用来说,只用一个根模块就足够了。随着应用规模的增长,我们逐步从根模块中重构出一些特性模块,来代表一组相关功能的集合。然后,我们在根模块中导入它们。
AppModule - 应用的根模块
每个Angular应用都有一个根模块类。
@NgModule装饰器用来为模块定义元数据。
在main.ts中引导
在main.ts文件中,我们通过引导AppModule来启动应用。
针对不同的平台,Angular提供了很多引导选项。
- 通过即时(JiT)编译器动态引导
- 使用预编译器(AoT - Ahead-Of-Time)进行静态引导
声明指令和组件
服务提供商
模块可以往应用的“根依赖入器”中添加提供商,让那些服务在应用中到处可用。
导入支持性模块
Angular能识别NgIf、NgFor等指令的原因,是因为在AppModule中我们导入了BrowserModule模块。
导入BrowserModule会让该模块公开的所有组件、指令和管道在AppModule下的任何组件模板中直接可用,而不需要额外的繁琐步骤。
更准确地说,NgIf
是来自@angular/common
的CommonModule
中声明的。
CommonModule
提供了很多应用程序中常用的指令,包括NgIf和NgFor等。
BrowserModule
导入了CommonModule
并且重新导出了它。最终的效果是:只要导入BrowserModule就自动获得了CommonModule
中的指令。
有些熟悉的Angular指令并不属于CommonModule。例如,NgModel
和RouterLink
分别属于Angular的FormsModule
模块和RouterModule
模块。在使用那些指令之前,我们也必须导入那些模块。
注:永远不要再次声明属于其它模块的类。
解决指令冲突
指令冲突表现在指令同名但功能不同时导入根模块时的情况。即这些指令是不同的,只是恰好指令名称相同而已。
我们可以通过创建特性模块来消除组件与指令的冲突。特性模块可以把来自一个模块中的声明和来自另一个的区隔开。
特性模块
特性模块是带有@NgModule
装饰器及其元数据的类,就像根模块一样。特性模块的元数据和根模块的元数据的属性是一样的。
根模块和特性模块还共享着相同的执行环境。它们共享着同一个依赖注入器,这意味着某个模块中定义的服务在所有模块中也都能用。
它们在技术上有两个显著的不同点:
1. 我们引导根模块来启动应用,但导入特性模块来扩展应用。
2. 特性模块可以对其它模块暴露或隐藏自己的实现。
特性模块用来提供了内聚的功能集合。聚焦于应用的某个业务邻域、用户工作流、某个基础设施(表单、HTTP、路由),或一组相关的工具集合。
虽然这些都能在根模块中做,但特性模块可以帮助我们把应用切分成具有特定关注点和目标的不同区域。
特性模块通过自己提供的服务和它决定对外共享的那些组件、指令、管道来与根模块等其它模块协同工作。
BrowserModule
提供了启动和运行浏览器应用的那些基本的服务提供商。
BrowserModule
还从@angular/common
中重新导出了CommonModule
,这意味着AppModule
中的组件也同样可以访问那些每个应用都需要的Angular指令,如NgIf
和NgFor
。
注:在其它任何模块中都不要导入BrowserModule
。特性模块和惰性加载模块应该改成导入CommonModule
。它们不需要重新初始化全应用级的提供商。
如果你在惰性加载模块中导入BrowserModule
,Angular就会抛出一个错误。
特性模块中导入CommonModule
可以让它用在任何目标平台,不仅是浏览器。那些跨平台库的作者应该喜欢这种方式的。
通过路由器惰性加载模块
应用路由
app/app-routing.module.ts
COPY CODE
import { NgModule } from '@angular/core';
import { Routes,RouterModule } from '@angular/router';
export const routes: Routes = [
{ path: '',redirectTo: 'contact',pathMatch: 'full'},{ path: 'crisis',loadChildren: 'app/crisis/crisis.module#CrisisModule' },{ path: 'heroes',loadChildren: 'app/hero/hero.module#HeroModule' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],exports: [RouterModule]
})
export class AppRoutingModule {}
其中,contact路由并不是在这里定义的。对于带有路由组件的特性模块,其标准做法就是让它们定义自己的路由。如,ContactModule特性模块,在其特性区中定义自己的路由文件contact.routing.ts。
另外两个路由使用惰性加载语法来告诉路由器要到哪里去找这些模块。
{ path: 'crisis',loadChildren: 'app/crisis/crisis.module#CrisisModule' },{ path: 'heroes',loadChildren: 'app/hero/hero.module#HeroModule' }
惰性加载模块的位置是字符串而不是类型。在本应用中该字符串同时标记出了模块文件和模块类,两者用#
分隔开。
对于这些惰性模块(这里指CrisisModule和HeroModule模块),并不需要在根模块AppModule中导入。它们将在用户导航到其中的某个路由时,被异步获取并加载。在AppModule根模块中需要导入ContactModule模块,以便在应用启动时加载它的路由和组件。
RouterModule.forRoot
RouterModule
类的forRoot
静态方法和提供的配置,被添加到imports
数组中,提供该模块的路由信息。
@NgModule({
imports: [RouterModule.forRoot(routes)],exports: [RouterModule]
})
export class AppRoutingModule {}
该方法返回的AppRoutingModule类是一个路由模块,它同时包含了RouterModule指令和用来生成配置好的Router的依赖注入提供商。
这个AppRoutingModule仅用于应用的根模块。
注:永远不要在特性路由模块中调用RouterModule.forRoot!
只能在应用的根模块AppModule中调用并导入.forRoot的结果。 在其它模块中导入它,特别是惰性加载模块中,是违反设计目标的并会导致一个运行时错误。
路由到特性模块
app/contact目录中也有一个新文件contact-routing.module.ts。 它定义了我们前面提到过的联系人路由,并提供了ContactRoutingModule,就像这样:
@NgModule({
imports: [RouterModule.forChild([
{ path: 'contact',component: ContactComponent }
])],exports: [RouterModule]
})
export class ContactRoutingModule {}
这次我们要把路由列表传给RouterModule的forChild方法。 该方法会为特性模块生成另一种对象。
注:总是在特性路由模块中调用RouterModule.forChild。
当我们只需要从路由器导航到某个特性模块中的某个组件时,我们就不需要公开它了。如,通过路由器导航到ContactComponent组件,contact.module.ts定义如下:
@NgModule({
imports: [ CommonModule,FormsModule,ContactRoutingModule ],declarations: [ ContactComponent,HighlightDirective,AwesomePipe ],providers: [ ContactService ]
})
export class ContactModule { }
现在我们通过路由器导航到ContactComponent,所以也就没有理由公开它了。它也不再需要选择器 (selector)。 也没有模板会再引用ContactComponent。它从 AppComponent 模板中彻底消失了。
路由到惰性加载的模块
惰性加载的HeroModule和CrisisModule与其它特性模块遵循同样的规则。它们和主动加载的ContactModule看上去没有任何区别。
如HeroModule:
@NgModule({
imports: [ CommonModule,HeroRoutingModule ],declarations: [
HeroComponent,HeroDetailComponent,HeroListComponent,HighlightDirective
]
})
export class HeroModule { }
共享模块
在多个特性模块中,可能存在公共的组件、指令和管道。我们可以添加SharedModule
来存放这些公共组件、指令和管道,并且共享给那些需要它们的模块。
如,项目中公共的有AwesomePipe管道、HighlightDirective指令、CommonModule和FormsModule模块。可以定义SharedModule如下:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AwesomePipe } from './awesome.pipe';
import { HighlightDirective } from './highlight.directive';
@NgModule({
imports: [ CommonModule ],declarations: [ AwesomePipe,HighlightDirective ],exports: [ AwesomePipe,CommonModule,FormsModule ]
})
export class SharedModule { }
值得注意的有:
- 它导入了CommonModule,这是因为它的组件需要这些公共指令。
- 正如我们所期待的,它声明并导出了工具性的管道、指令和组件类。
- 它重新导出了CommonModule和FormsModule
重新导出其它模块
当回顾应用程序时,我们注意到很多需要SharedModule的组件也同时用到了来自CommonModule
的NgIf和NgFor指令,并且还通过来自FormsModule的[(ngModel)]指令绑定到了组件的属性。那些声明这些组件的模块将不得不同时导入CommonModule
、FormsModule
和SharedModule
。
通过让SharedModule
重新导出CommonModule和FormsModule模块,我们可以消除这种重复。于是导入SharedModule
的模块也同时免费获得了CommonModule
和FormsModule
。
如,导入SharedModule的AppModule根模块,在该根模块中声明的组件,如果使用到NgIf或NgFor指令就不需要在AppModule中再导入CommonModule模块,同理,如果在该根模块中声明的组件,如果使用到NgModel指令同样也不要在AppModule中导入FormsModule模块了。
实际上,SharedModule本身所声明的组件没绑定过[(ngModel)],那么,严格来说SharedModule并不需要导入FormsModule。
这时SharedModule仍然可以导出FormsModule,而不需要先把它列在imports中。
注:不要在共享模块中把应用级单例添加到providers中。 否则如果一个惰性加载模块导入了此共享模块,就会导致它自己也生成一份此服务的实例。
对于如只在AppComponent中使用一次的组件,我们没必要共享它。如:TitleComponent只被AppComponent用一次,我们就没必要共享它,即不要在SharedModule中导出这样的组件。
全应用级的单例服务,不应在共享模块的providers中。如:UserService是全应用级单例。 我们不希望每个模块都各自有它的实例。
核心模块
现在,我们的根目录下只剩下UserService和TitleComponent这两个被根组件AppComponent用到的类没有清理了。 但正如我们已经解释过的,它们无法被包含在SharedModule中。
不过,我们可以把它们收集到单独的CoreModule中,并且只在应用启动时导入它一次,而不会在其它地方导入它。
如CoreModule:
import {
ModuleWithProviders,NgModule,Optional,SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TitleComponent } from './title.component';
import { UserService } from './user.service';
@NgModule({
imports: [ CommonModule ],declarations: [ TitleComponent ],exports: [ TitleComponent ],providers: [ UserService ]
})
export class CoreModule {
}
因此我们建议把这些一次性的类收集到CoreModule中,并且隐藏它们的实现细节。 简化之后的根模块AppModule导入CoreModule来获取其能力。记住,根模块是整个应用的总指挥,不应该插手过多细节。
用forRoot配置核心服务
为应用添加服务提供商的模块也可以同时提供配置那些提供商的功能。
用CoreModule.forRoot配置核心服务
按照约定,模块的静态方法forRoot可以同时提供并配置服务。 它接收一个服务配置对象,并返回一个ModuleWithProviders。
根模块AppModule会导入CoreModule类并把它的providers添加到AppModule的服务提供商中。
更精确的说法是,Angular 会先累加所有导入的提供商,然后才把它们追加到@NgModule.providers中。 这样可以确保我们显式添加到AppModule中的那些提供商总是优先于从其它模块中导入的提供商。
现在添加CoreModule.forRoot方法,以便配置核心中的UserService。
我们曾经用一个可选的、被注入的UserServiceConfig服务扩展过核心的UserService服务。 如果有UserServiceConfig,UserService就会据此设置用户名。
app/core/user.service.ts(constructor)
constructor(@Optional() config: UserServiceConfig) { if (config) { this._userName = config.userName; }
}
这里的CoreModule.forRoot接收UserServiceConfig对象:
app/core/core.module.ts (forRoot)
static forRoot(config: UserServiceConfig): ModuleWithProviders { return { ngModule: CoreModule,providers: [ {provide: UserServiceConfig,useValue: config } ] };
}
最后,我们在AppModule的imports列表中调用它。
app/app.module.ts (imports)
imports: [
BrowserModule,ContactModule,CoreModule.forRoot({userName: 'Miss Marple'}),AppRoutingModule
],
注:只在应用的根模块AppModule中调用forRoot。 如果在其它模块(特别是惰性加载模块)中调用它则违反了设计意图,并会导致运行时错误。
禁止重复导入CoreModule
只有根模块AppModule才能导入CoreModule。 如果惰性加载模块导入了它,就会出问题。
我们可以祈祷任何开发人员都不会犯错。 但是最好还是对它进行一些保护,以便让它“尽快出错”。只要把下列代码添加到CoreModule的构造函数中就可以了。
constructor (@Optional() @SkipSelf() parentModule: CoreModule) { if (parentModule) { throw new Error( 'CoreModule is already loaded. Import it in the AppModule only'); }
}
参考
参考最终版本的全部源码:https://angular.cn/docs/ts/latest/guide/ngmodule.html。