Angular模块 (NgModule)

Angular 模块能帮你把应用组织成多个内聚的功能块。

Angular 模块是带有 @NgModule 装饰器函数的@NgModule接收一个元数据对象,该对象告诉 Angular 如何编译和运行模块代码。 它标记出该模块拥有的组件、指令和管道, 并把它们的一部分公开出去,以便外部组件使用它们。 它可以向应用的依赖注入器中添加服务提供商。 本章还会涉及到更多选项。

请先阅读根模块一章,那里介绍过 Angular 模块,以及如何为整个应用创建和维护单一的AppModule类。

本章的解释更加详尽,正如下面的目录所示。

目录

在线例子

本章通过一个基于《英雄指南》的渐进式例子解释了 Angular 的模块。这里是例子演化过程中一些关键节点的在线例子。

常见问题

本章涵盖了英雄指南下的 Angular 模块概念。

烹饪宝典中的 Angular 模块常见问题为一些与设计和实现有关的问题提供了答案。 不过在阅读常见问题之前,要先阅读本章。

Angular 模块化

模块是组织应用和使用外部库扩展应用的最佳途径。

很多 Angular 库都是模块,例如:FormsModuleHttpModuleRouterModule。 很多第三方库也封装成了 Angular 模块,例如:Material DesignIonicAngularFire2

Angular 模块把组件、指令和管道打包成内聚的功能块,每个模块聚焦于一个特性区域、业务领域、工作流或通用工具。

模块还能用来把服务加到应用程序中。这些服务可能是内部研发的,例如应用日志服务; 也可能是外部资源,例如 Angular 路由和 Http 客户端。

模块可能在应用启动时主动加载,也可能由路由器进行异步惰性加载

Angular 模块是一个由@NgModule装饰器提供元数据的类,元数据包括:

每个 Angular 应用至少有一个模块类 —— 根模块,我们将通过引导根模块来启动应用。

对于组件很少的简单应用来说,只用一个根模块就足够了。 随着应用规模的增长,我们逐步从根模块中重构出一些特性模块,来代表一组相关功能的集合。 然后,我们在根模块中导入它们。

稍后我们就会看到怎么做。不过还是先从根模块开始吧!

AppModule - 应用的根模块

每个 Angular 应用都有一个根模块类。 按照约定,它的类名叫做AppModule,被放在app.module.ts文件中。

快速起步种子库中的AppModule是能找到的最小版本:

src/app/app.module.ts (minimal)

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }  from './app.component';

@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

@NgModule装饰器用来为模块定义元数据。 我们先凭直觉来理解一下元数据,接下来再逐步深入细节。

这个元数据只导入了一个辅助模块,BrowserModule,每个运行在浏览器中的应用都必须导入它。

BrowserModule注册了一些关键的应用服务提供商。 它还包括了一些通用的指令,例如NgIfNgFor,所以这些指令在该模块的任何组件模板中都是可用的。

declarations列出了该应用程序中唯一的组件(根组件),它是应用的光秃秃的组件树的根。

下面范例AppComponent显示被绑定的标题:

src/app/app.component.ts (minimal)

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: '<h1>{{title}}</h1>',
})
export class AppComponent {
  title = 'Minimal NgModule';
}

最后,@NgModule.bootstrap属性把这个AppComponent标记为引导 (bootstrap) 组件。 当 Angular 引导应用时,它会在 DOM 中渲染AppComponent,并把结果放进index.html<my-app>元素标记内部。

main.ts 中引导

main.ts文件中,我们通过引导AppModule来启动应用。

针对不同的平台,Angular 提供了很多引导选项。 本章我们只讲两个选项,都是针对浏览器平台的。

通过即时 (JiT) 编译器动态引导

先看看动态选项,Angular 编译器在浏览器中编译并引导该应用。

src/main.ts (dynamic)

// The browser platform with a compiler
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

// The app module
import { AppModule } from './app/app.module';

// Compile and launch the module
platformBrowserDynamic().bootstrapModule(AppModule);

这里的例子演示进行动态引导的方法。

plunker

你还可以下载这个例子

You can also download this example.

使用预编译器 (AoT - Ahead-Of-Time) 进行静态引导

静态方案可以生成更小、启动更快的应用,建议优先使用它,特别是在移动设备或高延迟网络下。

使用静态选项,Angular 编译器作为构建流程的一部分提前运行,生成一组类工厂。它们的核心就是AppModuleNgFactory

引导预编译的AppModuleNgFactory的语法和动态引导AppModule类的方式很相似。

src/main.ts (static)

// The browser platform without a compiler
import { platformBrowser } from '@angular/platform-browser';

// The app module factory produced by the static offline compiler
import { AppModuleNgFactory } from './app/app.module.ngfactory';

// Launch with the app module factory.
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

由于整个应用都是预编译的,所以我们不用把 Angular 编译器一起发到浏览器中,也不用在浏览器中进行编译。

下载到浏览器中的应用代码比动态版本要小得多,并且能立即执行。引导的性能可以得到显著提升。

无论是 JiT 还是 AoT 编译器都会从同一份AppModule源码中生成一个AppModuleNgFactory类。 JiT 编译器动态地在浏览器的内存中创建这个工厂类。 AoT 编译器把工厂输出成一个物理文件,也就是我们在静态版本main.ts中导入的那个。

通常,AppModule不必关心它是如何被引导的。

虽然AppModule会随着应用而演化,但是main.ts中的引导代码不会变。 这将是我们最后一次关注main.ts了。

声明指令和组件

应用继续演进。 首先加入的是HighlightDirective,一个属性型指令,它会设置所在元素的背景色。

src/app/highlight.directive.ts

import { Directive, ElementRef } from '@angular/core';

@Directive({ selector: '[highlight]' })
/** Highlight the attached element in gold */
export class HighlightDirective {
  constructor(el: ElementRef) {
    el.nativeElement.style.backgroundColor = 'gold';
    console.log(
      `* AppRoot highlight called for ${el.nativeElement.tagName}`);
  }
}

我们更新AppComponent的模板,来把该指令附加到标题上:

template: '<h1 highlight>{{title}}</h1>'

如果我们现在就运行该应用,Angular 将无法识别highlight属性,并且忽略它。 我们必须在AppModule中声明该指令。

导入HighlightDirective类,并把它加入该模块的declarations数组中,就像这样:

declarations: [
  AppComponent,
  HighlightDirective,
],

添加组件

接着把标题重构到独立的TitleComponent中。 该组件的模板绑定到了组件的titlesubtitle属性中,就像这样:

src/app/title.component.html

<h1 highlight>{{title}} {{subtitle}}</h1>

src/app/title.component.ts

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-title',
  templateUrl: './title.component.html',
})
export class TitleComponent {
  @Input() subtitle = '';
  title = 'Angular Modules';
}

我们重写了AppComponent来把这个新的TitleComponent显示到<app-title>元素中,并使用一个输入型绑定来设置subtitle

src/app/app.component.ts (v1)

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: '<app-title [subtitle]="subtitle"></app-title>'
})
export class AppComponent {
  subtitle = '(v1)';
}

除非我们在AppModule中声明过,否则 Angular 无法识别<app-title>标签。 导入TitleComponent类,并把它加到模块的declarations中:

  declarations: [
    AppComponent,
    HighlightDirective,
    TitleComponent,
  ],

服务提供商

模块是为模块中的所有组件提供服务的最佳途径。

依赖注入一章中讲过 Angular 的层次化依赖注入系统, 以及如何在组件树的不同层次上通过提供商配置该系统。

模块可以往应用的“根依赖注入器”中添加提供商,让那些服务在应用中到处可用。

很多应用都需要获取当前登录的用户的信息,并且通过一个用户服务来访问它们。 该范例中有一个UserService的伪实现。

src/app/user.service.ts

import { Injectable } from '@angular/core';

@Injectable()
/** Dummy version of an authenticated user service */
export class UserService {
  userName = 'Sherlock Holmes';
}

该范例应用会在标题下方为已登录用户显示一条欢迎信息。 更新TitleComponent的模板来显示它。

src/app/title.component.html

<h1 highlight>{{title}} {{subtitle}}</h1>
<p *ngIf="user">
  <i>Welcome, {{user}}</i>
<p>

更新TitleComponent,为它加入一个构造函数,注入UserService类,并把组件的user属性设置为它的实例。

src/app/title.component.ts

import { Component, Input } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-title',
  templateUrl: './title.component.html',
})
export class TitleComponent {
  @Input() subtitle = '';
  title = 'Angular Modules';
  user = '';

  constructor(userService: UserService) {
    this.user = userService.userName;
  }
}

我们已经定义使用了该服务。现在,我们通过把它加入AppModule元数据的providers属性中,来把它提供给所有组件使用。

src/app/app.module.ts (providers)

providers: [ UserService ],

导入支持性模块

注意,在修改过的TitleComponent中,有一个*ngIf指令在“守卫着”该消息。如果没有当前用户,就没有任何消息。

src/app/title.component.html (ngIf)

<p *ngIf="user">
  <i>Welcome, {{user}}</i>
<p>

虽然AppModule没有声明过NgIf指令,但该应用仍然能正常编译和运行。为什么这样没问题呢?Angular 的编译器遇到不认识的 HTML 时应该不是忽略就是报错才对。

Angular 能识别NgIf指令,是因为我们以前导入过它。最初版本的AppModule就导入了BrowserModule

src/app/app.module.ts (imports)

imports: [ BrowserModule ],

导入BrowserModule会让该模块公开的所有组件、指令和管道在AppModule下的任何组件模板中可用。

更准确的说,NgIf是在来自@angular/commonCommonModule中声明的。

CommonModule提供了很多应用程序中常用的指令,包括NgIfNgFor等。

BrowserModule导入了CommonModule并且重新导出了它。 最终的效果是:只要导入BrowserModule就自动获得了CommonModule中的指令。

很多熟悉的 Angular 指令并不属于CommonModule。 例如,NgModelRouterLink分别属于 Angular 的FormsModule模块和RouterModule模块。 在使用那些指令之前,我们也必须导入那些模块。

要解释这一点,我们可以再加入ContactComponent组件,它是一个表单组件,从 Angular 的FormsModule中导入了表单支持。

添加 ContactComponent

Angular 表单是用来管理用户数据输入的最佳方式之一。

ContactComponnet组件展现“联系人编辑器”,它是用模板驱动式表单实现的。

Angular 表单的风格

我们写 Angular 表单组件时,可以使用模板驱动式表单, 也可以使用响应式表单

该例子中从@angular/forms中导入了FormsModule,这是因为ContactComponent组件用的是模板驱动式表单。 那些带有响应式表单组件的模块,应该转而导入ReactiveFormsModule

ContactComponent的选择器会去匹配名叫<app-contact>的元素。 在AppComponent模板中<app-title>的下方添加一个具有此名字的元素:

src/app/app.component.ts (template)

template: `
  <app-title [subtitle]="subtitle"></app-title>
  <app-contact></app-contact>
`

ContactComponent还有很多事要做。 表单组件通常都是很复杂的。本组件具有它自己的ContactService自定义管道 Awesome, 以及HighlightDirective的另一个版本。

为了方便管理,我们把所有与联系人相关的编程元素都放进src/app/contact目录, 并把该组件分解成三个基本成分:HTML、TypeScript 和 CSS 文件:

  1. <h2>Contact of {{userName}}</h2>
  2. <div *ngIf="msg" class="msg">{{msg}}</div>
  3. <form *ngIf="contacts" (ngSubmit)="onSubmit()" #contactForm="ngForm">
  4. <h3 highlight>{{ contact.name | awesome }}</h3>
  5. <div class="form-group">
  6. <label for="name">Name</label>
  7. <input type="text" class="form-control" required
  8. [(ngModel)]="contact.name"
  9. name="name" #name="ngModel" >
  10. <div [hidden]="name.valid" class="alert alert-danger">
  11. Name is required
  12. </div>
  13. </div>
  14. <br>
  15. <button type="submit" class="btn btn-default" [disabled]="!contactForm.form.valid">Save</button>
  16. <button type="button" class="btn" (click)="next()" [disabled]="!contactForm.form.valid">Next Contact</button>
  17. <button type="button" class="btn" (click)="newContact()">New Contact</button>
  18. </form>

先来看组件模板。 注意模板中部的双向数据绑定[(ngModel)]ngModelNgModel指令的选择器。

虽然NgModel是 Angular 指令,但 Angular 编译器并不会识别它,因为:

退一步说,即使 Angular 有办法识别ngModelContactComponent也不会表现的像 Angular 表单, 因为本组件表单的表单相关的特性(例如有效性验证)还不可用。

导入FormsModule

FormsModule加到AppModule元数据中的imports列表中:

imports: [ BrowserModule, FormsModule ],

一旦我们声明了这些新组件、管道和指令,[(ngModel)]绑定就会正常工作,用户的输入也能被 Angular 表单验证了。

不要NgModel(或FORMS_DIRECTIVES)加到AppModule元数据的declarations数据中!这些指令属于FormsModule`。

组件、指令和管道只能属于一个模块。

永远不要再次声明属于其它模块的类。

声明联系人的组件、指令和管道

如果我们没有声明该联系人模块的组件、指令和管道,该应用就会失败。 更新AppModule中的declarations元数据,就像这样:

src/app/app.module.ts (declarations)

  declarations: [
    AppComponent,
    HighlightDirective,
    TitleComponent,

    AwesomePipe,
    ContactComponent,
    ContactHighlightDirective
  ],

如果有两个同名指令,都叫做HighlightDirective,该怎么办呢?

我们只要在 import 时使用as关键字来为第二个指令创建个别名就可以了。

import {
  HighlightDirective as ContactHighlightDirective
} from './contact/highlight.directive';

这解决了在文件中使用指令类型时的冲突问题,但是还有另一个问题没有解决,我们将在后面讨论它。

提供 ContactService

ContactComponent显示从ContactService服务中获取的联系人信息,该服务是被 Angular 注入到组件的构造函数中的。

我们必须在某个地方提供该服务。 在ContactComponent可以提供它。 但是那样一来,它的作用范围就会局限于该组件及其子组件。 而我们希望让该服务与其它和联系人有关的组件中共享,稍后我们就会添加那些组件。

在此应用中,我们选择把ContactSerivce添加到AppModule元数据的providers列表中:

src/app/app.module.ts (providers)

providers: [ ContactService, UserService ],

现在,ContactService服务就能被注入进该应用中的任何组件了,就像UserService一样。

全应用范围的提供商

ContactService的提供商是全应用范围的,这是因为 Angular 使用该应用的根注入器注册模块的providers

从架构上看,ContactService属于“联系人”这个业务领域。 其它领域中的类并不需要知道ContactService,也不会要求注入它。

我们可能会期待 Angular 提供一种模块范围内的机制来保障此设计。 但它没有。与组件不同,Angular的 模块实例并没有它们自己的注入器,所以它们也没有自己的供应商范围。

Angular是故意这么设计的。 Angular的模块设计,主要目的是扩展应用程序,丰富其模块化能力。

在实践中,服务的范围很少会成为问题。 联系人之外的组件不会意外注入ContactService服务。 要想注入ContactService,你得先导入它的类型。 而只有联系人组件才会导入ContactService类型

NgModule常见问题页的如何把服务的范围限制在一个模块中一节中可以了解更多。

运行该应用

一切就绪,可以运行该应用及其联系人编辑器了。

应用的文件结构是这样的:

app
app.component.ts
app.module.ts
highlight.directive.ts
title.component.(html|ts)
user.service.ts
contact
awesome.pipe.ts
contact.component.(css|html|ts)
contact.service.ts
highlight.directive.ts

试试这个例子:

plunker

You can also download this example.

解决指令冲突

以前我们在声明联系人的HighlightDirective指令时遇到了问题,因为在应用程序一级已经有了一个HighlightDirective类。

在查找它们的选择器时,它们都试图用不同的颜色来高亮所依附的元素。

  1. import { Directive, ElementRef } from '@angular/core';
  2. @Directive({ selector: '[highlight]' })
  3. /** Highlight the attached element in gold */
  4. export class HighlightDirective {
  5. constructor(el: ElementRef) {
  6. el.nativeElement.style.backgroundColor = 'gold';
  7. console.log(
  8. `* AppRoot highlight called for ${el.nativeElement.tagName}`);
  9. }
  10. }

Angular 会只用它们中的一个吗?不会。 所有指令都声明在该模块中,所以这两个指令都会被激活

当两个指令在同一个元素上争相设置颜色时,后声明的那个会胜出,因为它对 DOM 的修改覆盖了前一个。 在该例子中,联系人的HighlightDirective把应用标题的文本染成了蓝色,而我们原本期望它保持金色。

真正的问题在于,有两个不同的类试图做同一件事。

多次导入同一个指令是没问题的,Angular 会移除重复的类,而只注册一次。

从 Angular 的角度看,两个类并没有重复。Angular 会同时保留这两个指令,并让它们依次修改同一个 HTML 元素。

至少,应用仍然编译通过了。 如果我们使用相同的选择器定义了两个不同的组件类,并指定了同一个元素标记,编译器就会报错说它无法在同一个 DOM 位置插入两个不同的组件。

我们可以通过创建特性模块来消除组件与指令的冲突。 特性模块可以把来自一个模块中的声明和来自另一个的区隔开。

特性模块

该应用还不大,但是已经在受结构方面的问题困扰了。

我们用特性模块技术来缓解此问题。

特性模块是带有@NgModule装饰器及其元数据的类,就像根模块一样。 特性模块的元数据和根模块的元数据的属性是一样的。

根模块和特性模块还共享着相同的执行环境。 它们共享着同一个依赖注入器,这意味着某个模块中定义的服务在所有模块中也都能用。

它们在技术上有两个显著的不同点:

此外,特性模块主要还是从它的设计意图上来区分。

特性模块用来提供了内聚的功能集合。 聚焦于应用的某个业务领域、用户工作流、某个基础设施(表单、HTTP、路由),或一组相关的工具集合。

虽然这些都能在根模块中做,但特性模块可以帮助我们把应用切分成具有特定关注点和目标的不同区域。

特性模块通过自己提供的服务和它决定对外共享的那些组件、指令、管道来与根模块等其它模块协同工作。

下一节,我们从根模块中把与联系人有关的功能切分到专门的特性模块中。

联系人做成特性模块

把与联系人有关的这些元素重构到“联系人”特性模块中很简单。

  1. src/app/contact目录下创建ContactModule

  2. 把联系人相关的元素从AppModule移到ContactModule中。

  3. 把导入BrowserModule改为导入CommonModule

  4. AppModule中导入ContactModule

AppModule是唯一有改变的已经存在的类,不过我们还会添加一个新文件。

添加 ContactModule

下面是新的ContactModule

src/app/contact/contact.module.ts

  1. import { NgModule } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { AwesomePipe } from './awesome.pipe';
  5. import
  6. { ContactComponent } from './contact.component';
  7. import { ContactService } from './contact.service';
  8. import { HighlightDirective } from './highlight.directive';
  9. @NgModule({
  10. imports: [ CommonModule, FormsModule ],
  11. declarations: [ ContactComponent, HighlightDirective, AwesomePipe ],
  12. exports: [ ContactComponent ],
  13. providers: [ ContactService ]
  14. })
  15. export class ContactModule { }

AppModule中的相关联系人的 import 语句和@NgModule的相关属性复制到ContactModule

导入FormsModule,因为联系人组件需要它。

当前模块不会继承其它模块中对组件、指令或管道的访问权。 AppModule中的 imports 与ContatModule的 imports 互不相干。 如果ContactComponent要绑定到[(ngModel)],它所在的ContactModule必需导入FormsModule

我们还用CommonModule替换了BrowserModule,其中缘由参见这条常见问题

我们在该模块的declarations声明了联系人组件、指令和管道。

我们导出ContactComponent,这样其它模块只要导入了ContactModule,就可以在它们的组件模板中使用ContactComponent了。

声明的所有其它联系人类默认都是私有的。 AwesomePipeHighlightDirective对应用的其它部分是不可见的。 所以HighlightDirective不能把AppComponent的标题文字染色。

重构 AppModule

返回AppModule并移除专属于联系人特性下的任何东西。

只保留本应用的根一级需要的那些类。

然后,导入ContactModule,以便应用能够继续显示导出的ContactComponent

下面是AppModule重构完的版本与之前版本的对比。

  1. import { NgModule } from '@angular/core';
  2. import { BrowserModule } from '@angular/platform-browser';
  3. /* App Root */
  4. import
  5. { AppComponent } from './app.component';
  6. import { HighlightDirective } from './highlight.directive';
  7. import { TitleComponent } from './title.component';
  8. import { UserService } from './user.service';
  9. /* Contact Imports */
  10. import
  11. { ContactModule } from './contact/contact.module';
  12. @NgModule({
  13. imports: [ BrowserModule, ContactModule ],
  14. declarations: [ AppComponent, HighlightDirective, TitleComponent ],
  15. providers: [ UserService ],
  16. bootstrap: [ AppComponent ],
  17. })
  18. export class AppModule { }

改进之处

修改后的AppModule有一些很棒的特性。

试试范例的ContactModule版。

plunker

你还可以下载这个例子

You can also download this example.

用路由器实现惰性 (lazy) 加载

英雄职介所这个例子应用继续成长。 它又增加了两个模块,一个用来管理雇佣的英雄,另一个用来匹配英雄与危机。 这两个模块都还处于前期开发阶段。 它们对于整个故事来说无关紧要,这里我们就不逐行讨论了。

Examine and download the complete source for this version from the

在线例子 / 可下载的例子 试用并下载当前版本的完整代码。

当前应用中还有一些方面值得深入探讨。

我们从这个AppComponent新模板的顶部看起:标题、三个链接和<router-outlet>

src/app/app.component.ts (v3 - Template)

template: `
  <app-title [subtitle]="subtitle"></app-title>
  <nav>
    <a routerLink="contact" routerLinkActive="active">Contact</a>
    <a routerLink="crisis"  routerLinkActive="active">Crisis Center</a>
    <a routerLink="heroes"  routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`

<app-contact>元素不见了,改成了路由到联系人页。

AppModule进行适度的修改:

src/app/app.module.ts (v3)

  1. import { NgModule } from '@angular/core';
  2. import { BrowserModule } from '@angular/platform-browser';
  3. /* App Root */
  4. import { AppComponent } from './app.component.3';
  5. import { HighlightDirective } from './highlight.directive';
  6. import { TitleComponent } from './title.component';
  7. import { UserService } from './user.service';
  8. /* Feature Modules */
  9. import { ContactModule } from './contact/contact.module.3';
  10. /* Routing Module */
  11. import { AppRoutingModule } from './app-routing.module.3';
  12. @NgModule({
  13. imports: [
  14. BrowserModule,
  15. ContactModule,
  16. AppRoutingModule
  17. ],
  18. providers: [ UserService ],
  19. declarations: [ AppComponent, HighlightDirective, TitleComponent ],
  20. bootstrap: [ AppComponent ]
  21. })
  22. export class AppModule { }

有些文件名带有.3扩展名,用来和以前/以后的版本区分开。 我们会在适当的时机解释它们的差异。

该模块仍然要导入ContactModule模块,以便在应用启动时加载它的路由和组件。

该模块导入HeroModuleCrisisModule。 它们将在用户导航到其中的某个路由时,被异步获取并加载。

与第二版相比,最值得注意的修改是imports中那个额外的AppRoutingModule模块。 AppRoutingModule是一个路由模块 用来处理应用的路由。

应用路由

src/app/app-routing.module.ts

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 {}

路由器有专门的章节做深入讲解,所以这里我们跳过细节,而是专注于它和 Angular 模块的协作。

app-routing.module.ts文件定义了三个路由。

第一个路由把空白 URL(例如http://host.com/)重定向到了另一个路径为contact的路由(例如http://host.com/contact)。

contact路由并不是在这里定义的,而是定义在联系人特性区自己的路由文件contact.routing.ts中。 对于带有路由组件的特性模块,其标准做法就是让它们定义自己的路由。 稍后我们就会看到这些。

另外两个路由使用惰性加载语法来告诉路由器要到哪里去找这些模块。

{ path: 'crisis', loadChildren: 'app/crisis/crisis.module#CrisisModule' },
{ path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule' }

惰性加载模块的位置是字符串而不是类型。 在本应用中,该字符串同时标记出了模块文件和模块,两者用#分隔开。

RouterModule.forRoot

RouterModule类的forRoot静态方法和提供的配置,被添加到imports数组中,提供该模块的路由信息。

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

该方法返回的AppRoutingModule类是一个路由模块,它同时包含了RouterModule指令和用来生成配置好的Router的依赖注入提供商。

这个AppRoutingModule用于应用的模块。

永远不要在特性路由模块中调用RouterModule.forRoot

回到根模块AppModule,把这个AppRoutingModule添加到根模块的imports列表中,该应用就可以正常导航了。

src/app/app.module.ts (imports)

imports:      [
  BrowserModule,
  ContactModule,
  AppRoutingModule
],

路由到特性模块

src/app/contact目录中也有一个新文件contact-routing.module.ts。 它定义了我们前面提到过的联系人路由,并提供了ContactRoutingModule,就像这样:

src/app/contact/contact-routing.module.ts (routing)

@NgModule({
  imports: [RouterModule.forChild([
    { path: 'contact', component: ContactComponent }
  ])],
  exports: [RouterModule]
})
export class ContactRoutingModule {}

这次我们要把路由列表传给RouterModuleforChild方法。 该方法会为特性模块生成另一种对象。

总是在特性路由模块中调用RouterModule.forChild

当需要为根模块和特性模块分别提供不同的导入值时,forRootforChild是约定俗成的方法名。 虽然 Angular 无法识别它们,但是 Angular 开发人员可以。

当你要写类似的模块,来为根模块和特性模块分别导出一些声明和服务时,请遵循这个约定

ContactModule已经做了两个微小但重要的细节改动:

  1. @NgModule({
  2. imports: [ CommonModule, FormsModule, ContactRoutingModule ],
  3. declarations: [ ContactComponent, HighlightDirective, AwesomePipe ],
  4. providers: [ ContactService ]
  5. })
  6. export class ContactModule { }

现在我们通过路由器导航到ContactComponent,所以也就没有理由公开它了。它也不再需要选择器 (selector)。 也没有模板会再引用ContactComponent。它从 AppComponent 模板中彻底消失了。

路由到惰性加载的模块

惰性加载的HeroModuleCrisisModule与其它特性模块遵循同样的规则。它们和主动加载的ContactModule看上去没有任何区别。

HeroModuleCrisisModule略复杂一些,因此更适合用作范例。它的文件结构如下:

hero
hero-detail.component.ts
hero-list.component.ts
hero.component.ts
hero.module.ts
hero-routing.module.ts
hero.service.ts
highlight.directive.ts

如果你读过路由那章,那么对这个子路由的场景应该觉得很熟悉。 HeroComponent是本特性区的顶级组件和路由宿主。 模板带有<router-outlet>指令,它或者显示英雄列表(HeroList)或者显示所选英雄的编辑器(HeroDetail)。 这两个组件都把获取和保存数据的任务委托给HeroService执行。

还有另一个HighlightDirective指令,它用另一种方式为元素染色。 在下一节的共享模块中,我们会解决这种不必要的重复和不一致性。

HeroModule是特性模块,与其它的没什么不同。

src/app/hero/hero.module.ts (class)

@NgModule({
  imports: [ CommonModule, FormsModule, HeroRoutingModule ],
  declarations: [
    HeroComponent, HeroDetailComponent, HeroListComponent,
    HighlightDirective
  ]
})
export class HeroModule { }

它导入了FormsModule,因为HeroDetailComponent的模板中绑定到了[(ngModel)]。 像ContactModuleCrisisModule中一样,它还从hero-routing.module.ts中导入了HeroRoutingModule

CrisisModule和本模块非常像,我们不再赘述。

plunker

You can also download this example.

共享模块

本应用在继续演进中。 让我们感到不爽的是:这里有HighlightDirective的三个不同版本。 还有一大堆其它乱七八糟的东西堆在 app 目录这一级,我们得把它们清出去。

我们添加SharedModule来存放这些公共组件、指令和管道,并且共享给那些需要它们的模块。

  1. 创建src/app/shared目录

  2. AwesomePipeHighlightDirectivesrc/app/contact移到src/app/shared中。

  3. src/app/src/app/hero目录中删除HighlightDirective

  4. 创建SharedModule类来管理这些共享的素材

  5. 更新其它特性模块,导入SharedModule

下面就是这个SharedModule

src/app/src/app/shared/shared.module.ts

  1. import { NgModule } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { AwesomePipe } from './awesome.pipe';
  5. import { HighlightDirective } from './highlight.directive';
  6. @NgModule({
  7. imports: [ CommonModule ],
  8. declarations: [ AwesomePipe, HighlightDirective ],
  9. exports: [ AwesomePipe, HighlightDirective,
  10. CommonModule, FormsModule ]
  11. })
  12. export class SharedModule { }

值得注意的有:

重新导出其它模块

当回顾应用程序时,我们注意到很多需要SharedModule的组件也同时用到了来自CommonModuleNgIfNgFor指令, 并且还通过来自FormsModule[(ngModel)]指令绑定到了组件的属性。 那些声明这些组件的模块将不得不同时导入CommonModuleFormsModuleSharedModule

通过让SharedModule重新导出CommonModuleFormsModule模块,我们可以消除这种重复。 于是导入SharedModule的模块也同时免费获得了CommonModuleFormsModule

实际上,SharedModule本身所声明的组件没绑定过[(ngModel)],那么,严格来说SharedModule并不需要导入FormsModule

这时SharedModule仍然可以导出FormsModule,而不需要先把它列在imports中。

为什么 TitleComponent 没有被共享

设计SharedModule的目的在于让常用的组件、指令和管道可以被用在很多其它模块的组件模板中。

TitleComponent只被AppComponent用了一次,因此没必要共享它。

为什么 UserService 没有被共享

虽然很多组件都共享着同一个服务实例,但它们是靠 Angular 的依赖注入体系实现的,而不是模块体系。

例子中的很多组件都注入了UserService。 在整个应用程序中,只应该有一个UserService的实例,并且它只应该有一个提供商。

UserService是全应用级单例。 我们不希望每个模块都各自有它的实例。 而如果由SharedModule提供UserService,就会导致铁板钉钉的危险

不要在共享模块中把应用级单例添加到providers中。 否则如果一个惰性加载模块导入了此共享模块,就会导致它自己也生成一份此服务的实例。

核心 (Core) 模块

现在,我们的根目录下只剩下UserServiceTitleComponent这两个被根组件AppComponent用到的类没有清理了。 但正如我们已经解释过的,它们无法被包含在SharedModule中。

不过,我们可以把它们收集到单独的CoreModule中,并且只在应用启动时导入它一次而不会在其它地方导入它

执行下列步骤:

  1. 创建src/app/core文件夹

  2. UserServiceTitleComponentsrc/app移到src/app/core

  3. 创建CoreModule类来管理这些核心素材

  4. 修改AppRoot模块,使其导入CoreModule模块

这些都是一些熟悉的普通任务。令人感兴趣的是CoreModule

src/app/src/app/core/core.module.ts

  1. import {
  2. ModuleWithProviders, NgModule,
  3. Optional, SkipSelf } from '@angular/core';
  4. import { CommonModule } from '@angular/common';
  5. import { TitleComponent } from './title.component';
  6. import { UserService } from './user.service';
  7. @NgModule({
  8. imports: [ CommonModule ],
  9. declarations: [ TitleComponent ],
  10. exports: [ TitleComponent ],
  11. providers: [ UserService ]
  12. })
  13. export class CoreModule {
  14. }

我们正在从 Angular 核心库中导入一些从未用到的符号,稍后我们会接触它们。

我们对@NgModule的元数据应该很熟悉。 由于该模块拥有TitleComponent,所以我们声明了它。由于AppComponent(位于AppModule模块)在模板中显示了这个标题,所以我们导出了它。 由于TitleComponent需要用到 Angular 的NgIf指令,所以我们导入了CommonModule

CoreModule提供UserService。Angular 在该应用的根注入器中注册了它的提供商, 导致这份UserService的实例在每个需要它的组件中都是可用的,无论那个组件时主动加载的还是惰性加载的。

没必要?

这个场景设计的是有点生硬。 该应用太小了,所以其实并不需要拆分出单独的服务文件和小型的、一次性的组件。

TitleComponent放在根目录中其实也无所谓。 即使我们决定把UserService文件挪到app/core目录中,根AppModule也仍然可以自己注册UserService(就像现在这样)。

但真实的应用要考虑很多。 它们有一些只用于AppComponent的模板的一次性的组件(例如:加载动画、消息浮层和模态对话框等)。 我们不用在其它地方导入它们,因此没必要共享它们。 然而如果把它们留在根目录,还是显得太大、太乱了。

应用通常还有很多像这里的UserService这样的单例服务。 当程序启动时,每个服务都只能在应用的“根注入器”中注册一次

当很多组件在它们的构造函数中注入这些服务时 — 因此也需要用 JavaScript 的import语句来导入它们的符号 — 任何组件或模块自身都不应该定义或重新创建这些服务。 因为它们的提供商不是共享的。

因此我们建议把这些一次性的类收集到CoreModule中,并且隐藏它们的实现细节。 简化之后的根模块AppModule导入CoreModule来获取其能力。记住,根模块是整个应用的总指挥,不应该插手过多细节。

清理

我们已经重构完CoreModuleSharedModule,现在开始清理其它模块。

清理 AppModule

这里是更新后的AppModule与其第三版本的对比:

  1. import { NgModule } from '@angular/core';
  2. import { BrowserModule } from '@angular/platform-browser';
  3. /* App Root */
  4. import { AppComponent } from './app.component';
  5. /* Feature Modules */
  6. import { ContactModule } from './contact/contact.module';
  7. import { CoreModule } from './core/core.module';
  8. /* Routing Module */
  9. import { AppRoutingModule } from './app-routing.module';
  10. @NgModule({
  11. imports: [
  12. BrowserModule,
  13. ContactModule,
  14. CoreModule,
  15. AppRoutingModule
  16. ],
  17. declarations: [ AppComponent ],
  18. bootstrap: [ AppComponent ]
  19. })
  20. export class AppModule { }

AppModule现在变得:

清理ContactModule

这里是新的ContactModule与以前版本的对比:

  1. import { NgModule } from '@angular/core';
  2. import { SharedModule } from '../shared/shared.module';
  3. import { ContactComponent } from './contact.component';
  4. import { ContactService } from './contact.service';
  5. import { ContactRoutingModule } from './contact-routing.module';
  6. @NgModule({
  7. imports: [ SharedModule, ContactRoutingModule ],
  8. declarations: [ ContactComponent ],
  9. providers: [ ContactService ]
  10. })
  11. export class ContactModule { }

注意:

CoreModule.forRoot 配置核心服务

为应用添加服务提供商的模块也可以同时提供配置那些提供商的功能。

按照约定,模块的静态方法forRoot可以同时提供并配置服务。 它接收一个服务配置对象,并返回一个ModuleWithProviders。这个简单对象具有两个属性:

根模块AppModule会导入CoreModule类并把它的providers添加到AppModule的服务提供商中。

更精确的说法是,Angular 会先累加所有导入的提供商,然后才把它们追加到@NgModule.providers中。 这样可以确保我们显式添加到AppModule中的那些提供商总是优先于从其它模块中导入的提供商。

现在添加CoreModule.forRoot方法,以便配置核心中的UserService

我们曾经用一个可选的、被注入的UserServiceConfig服务扩展过核心的UserService服务。 如果有UserServiceConfigUserService就会据此设置用户名。

src/app/core/user.service.ts (constructor)

constructor(@Optional() config: UserServiceConfig) {
  if (config) { this._userName = config.userName; }
}

这里的CoreModule.forRoot接收UserServiceConfig对象:

src/app/core/core.module.ts (forRoot)

static forRoot(config: UserServiceConfig): ModuleWithProviders {
  return {
    ngModule: CoreModule,
    providers: [
      {provide: UserServiceConfig, useValue: config }
    ]
  };
}

最后,我们在AppModuleimports列表中调用它。

src/app//app.module.ts (imports)

  imports: [
    BrowserModule,
    ContactModule,
    CoreModule.forRoot({userName: 'Miss Marple'}),
    AppRoutingModule
  ],

该应用不再显示默认的 “Sherlock Holmes”,而是用 “Miss Marple” 作为用户名称。

只在应用的根模块AppModule中调用forRoot。 如果在其它模块(特别是惰性加载模块)中调用它则违反了设计意图,并会导致运行时错误。

别忘了导入其返回结果,而且不要把它添加到@NgModule的其它任何列表中。

禁止多次导入CoreModule

只有根模块AppModule才能导入CoreModule。 如果惰性加载模块导入了它,就会出问题

我们可以祈祷任何开发人员都不会犯错。 但是最好还是对它进行一些保护,以便让它“尽快出错”。只要把下列代码添加到CoreModule的构造函数中就可以了。

constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
  if (parentModule) {
    throw new Error(
      'CoreModule is already loaded. Import it in the AppModule only');
  }
}

这个构造函数会要求 Angular 把CoreModule注入自身。这看起来像一个危险的循环注入。

确实,如果 Angular 在当前注入器中查阅CoreModule,这确实会是一个循环引用。 不过,@SkipSelf装饰器意味着“在当前注入器的所有祖先注入器中寻找CoreModule。”

如果该构造函数在我们所期望的AppModule中运行,就没有任何祖先注入器能够提供CoreModule的实例,于是注入器会放弃查找。

默认情况下,当注入器找不到想找的提供商时,会抛出一个错误。 但@Optional装饰器表示找不到该服务也无所谓。 于是注入器会返回nullparentModule参数也就被赋成了空值,而构造函数没有任何异常。

如果我们错误的把CoreModule导入了一个惰性加载模块(例如HeroModule)中,那就不一样了。

Angular 创建一个惰性加载模块,它具有自己的注入器,它是根注入器的子注入器@SkipSelf让 Angular 在其父注入器中查找CoreModule,这次,它的父注入器却是根注入器了(而上次父注入器是空)。 当然,这次它找到了由根模块AppModule导入的实例。 该构造函数检测到存在parentModule,于是抛出一个错误。

总结

完工!你可以到下面的在线例子中试验它,并下载最终版本的全部源码。

plunker

You can also download this example.

常见问题 (FAQ)

现在,你已经理解了 Angular 的模块。可能你还会对烹饪宝典中的 Angular 模块常见问题感兴趣, 它解答了很多关于设计和实现方面的问题。