原文链接:https://angular-2-training-book.rangle.io/handout/modules/feature-modules.html
当我们的根模块开始增长时,最开始显而易见的是,某些元素(组件,指令等)在某方面相关联,几乎感觉他们属于可以被“插入”的库。
在我们前面的例子中,我们开始看到了。 我们的根模块具有组件,管道和服务,其唯一目的是处理信用卡。 如果我们将这三个元素提取到自己的特征模块 ,然后将其导入到我们的根模块会怎么样?
我们要做到这一点。 第一步是创建两个文件夹,以将属于根模块的元素与属于功能模块的元素区分开来。
.
├── app
│ ├── app.component.ts
│ └── app.module.ts
├── credit-card
│ ├── credit-card-mask.pipe.ts
│ ├── credit-card.component.ts
│ ├── credit-card.module.ts
│ └── credit-card.service.ts
├── index.html
└── main.ts
请注意每个文件夹如何拥有自己的模块文件: app.module.ts和credit-card.module.ts 。 我们首先关注后者。
credit-card/credit-card.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CreditCardMaskPipe } from './credit-card-mask.pipe';
import { CreditCardService } from './credit-card.service';
import { CreditCardComponent } from './credit-card.component';
@NgModule({
imports: [CommonModule],
declarations: [
CreditCardMaskPipe,
CreditCardComponent
],
providers: [CreditCardService],
exports: [CreditCardComponent]
})
export class CreditCardModule {}
我们的功能CreditCardModule它非常类似于根模块AppModule,但还是有一些重要的区别:
*我们没有导入BrowserModule而是CommonModule 。 如果我们在这里可以看到BrowserModule的文档,我们可以看到它是通过许多其他服务重新导出CommonModule ,有助于在浏览器中渲染Angular应用程序。 这些服务将我们的根模块与特定平台(浏览器)相结合,但我们希望我们的功能模块是独立于平台的。 这就是为什么我们只在那里导入CommonModule ,它只导出通用的指令和管道。
当涉及组件,管道和指令时,如果在根模块或任何其他功能模块中导入相同的依赖项,则每个模块都应导入自己的依赖项。 简而言之,即使有多个功能模块,每个功能模块都需要导入CommonModule 。
我们正在使用一种名为exports的新属性。 默认情况下 , declarations数组中定义的每个元素都是私有的 。 我们应该只输出我们应用程序中执行其他任务的其他模块。 在我们的例子中,我们只需要使CreditCardComponent可见,因为它被用在AppComponent的模板中。
app/app.component.ts
...
@Component({
...
template: `
...
<app-credit-card></app-credit-card>
`
})
export class AppComponent {}
我们正在将CreditCardMaskPipe保留为私有,因为它仅在CreditCardModule中使用 ,没有被其他模块直接使用。
我们现在可以将此功能模块导入到我们简化的根模块中。
app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CreditCardModule } from '../credit-card/credit-card.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule,
CreditCardModule
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
到这一步,我们完成了,我们的应用程序如预期的一样。
服务和延迟加载模块
这是Angular模块的棘手部分。 除非明确导出组件,管道和指令的范围,否则服务是全局可用的,除非模块被懒加载。
很难理解,我们首先让我们尝试在我们的示例中看到CreditCardService发生了什么。 请注意,该服务不在exports数组中,而是在providers数组中。 通过这种配置,即使在另一个模块中的AppComponent中,我们的服务也将随处可见。 所以,即使使用模块,也没有办法拥有一个“私有”服务,除非这个模块被懒加载。
当模块被懒惰加载时,Angular将创建一个子注入器(它是根模块的根注射器的子代),并且将在那里创建我们的服务实例。
想象一下,我们的CreditCardModule被配置为懒惰加载。 使用我们当前的配置,当应用程序被引导并且我们的根模块加载到内存中时,将会将CreditCardService (单例)的实例添加到根注入器中。 但是,当CreditCardModule在将来某个时候被懒惰加载时,将为该模块创建一个新的CreditCardService 实例的子注入器。 在这一点上,我们有一个分层注入器,具有相同服务的两个实例 ,这通常不是我们想要的。
想想一个执行身份验证的服务的示例。 我们希望在整个应用程序中只有一个单例,不管我们的模块是在bootstrap还是lazy加载的时候加载。 因此,为了使我们的功能模块的服务只添加到根注射器,我们需要使用不同的方法。
credit-card/credit-card.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
/* ...other imports... */
@NgModule({
imports: [CommonModule],
declarations: [
CreditCardMaskPipe,
CreditCardComponent
],
exports: [CreditCardComponent]
})
export class CreditCardModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: CreditCardModule,
providers: [CreditCardService]
}
}
}
不同于以前,我们并没有把我们的服务直接放在NgModule装饰器的providers属性中 。 这一次我们定义了一个名为forRoot的静态方法,通过它定义要导出的模块和服务。
使用这种新语法,我们的根模块略有不同。
app/app.module.ts
/* ...imports... */
@NgModule({
imports: [
BrowserModule,
CreditCardModule.forRoot()
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
你能发现差异吗? 我们不直接导入CreditCardModule ,而是导入从forRoot方法返回的对象,其中包括CreditCardService 。 虽然这种语法比原始语法更复杂一些,但它将保证我们只有一个CreditCardService实例被添加到根模块中。 当加载CreditCardModule (甚至延迟加载)时,不会将该服务的新实例添加到子注入器。
作为经验法则, 在从功能模块导出服务时始终使用forRoot语法 ,除非您有非常特殊的需求,需要在依赖注入树的不同级别进行多个实例。