1,NgModule简介

NgModules 用于配置注入器和编译器,并帮你把那些相关的东西组织在一起。

NgModule 是一个带有 @NgModule 装饰器的类。 @NgModule 的参数是一个元数据对象,用于描述如何编译组件的模板,以及如何在运行时创建注入器。 它会标出该模块自己的组件、指令和管道,通过 exports 属性公开其中的一部分,以便外部组件使用它们。 NgModule 还能把一些服务提供商添加到应用的依赖注入器中。

Angular 模块化

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

Angular 自己的库都是 NgModule,比如 FormsModule、HttpClientModule 和 RouterModule。 很多第三方库也是 NgModule,比如 Material Design、 Ionic 和 AngularFire2。

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

模块还可以把服务加到应用中。 这些服务可能是内部开发的(比如你自己写的),或者来自外部的(比如 Angular 的路由和 HTTP 客户端)。

模块可以在应用启动时急性加载,也可以由路由器进行异步的惰性加载。

NgModule 的元数据会做这些:

  • 声明某些组件、指令和管道属于这个模块。

  • 公开其中的部分组件、指令和管道,以便其它模块中的组件模板中可以使用它们。

  • 导入其它带有组件、指令和管道的模块,这些模块中的元件都是本模块所需的。

  • 提供一些供应用中的其它组件使用的服务。

每个 Angular 应用都至少有一个模块,也就是根模块。 你可以引导那个模块,以启动该应用。

对于那些只有少量组件的简单应用,根模块就是你所需的一切。 随着应用的成长,你要把这个根模块重构成一些特性模块,它们代表一组密切相关的功能集。 然后你再把这些模块导入到根模块中。

2,JavaScript 模块 vs. NgModule

JavaScript 模块

在 JavaScript 中,模块是内含 JavaScript 代码的独立文件。要让其中的东西可用,你要写一个导出语句,通常会放在相应的代码之后,类似这样:

export class AppComponent { ... }

然后,当你在其它文件中需要这个文件的代码时,要像这样导入它:

import { AppComponent } from './app.component';

JavaScript 模块让你能为代码加上命名空间,防止因为全局变量而引起意外

NgModules

NgModule 是一些带有 @NgModule 装饰器的类。@NgModule 装饰器的 imports 数组会告诉 Angular 哪些其它的 NgModule 是当前模块所需的。 imports 数组中的这些模块与 JavaScript 模块不同,它们都是 NgModule 而不是常规的 JavaScript 模块。 带有 @NgModule 装饰器的类通常会习惯性地放在单独的文件中,但单独的文件并不像 JavaScript 模块那样作为必要条件,而是因为它带有 @NgModule 装饰器及其元数据。

NgModule 类与 JavaScript 模块有下列关键性的不同:

  • NgModule 只绑定了可声明的类,这些可声明的类只是供 Angular 编译器用的。

  • 与 JavaScript 类把它所有的成员类都放在一个巨型文件中不同,你要把该模块的类列在它的 @NgModule.declarations 列表中。

  • NgModule 只能导出可声明的类。这可能是它自己拥有的也可能是从其它模块中导入的。它不会声明或导出任何其它类型的类。

  • 与 JavaScript 模块不同,NgModule 可以通过把服务提供商加到 @NgModule.providers 列表中,来用服务扩展整个应用。

3,常用模块

Angular 应用需要不止一个模块,它们都为根模块服务。 如果你要把某些特性添加到应用中,可以通过添加模块来实现。 下列是一些常用的 Angular 模块,其中带有一些其内容物的例子:

NgModule

导入自

为何使用

BrowserModule

@angular/platform-browser

当你想要在浏览器中运行应用时

CommonModule

@angular/common

当你想要使用 NgIf 和 NgFor 时

FormsModule

@angular/forms

当要构建模板驱动表单时(它包含 NgModel )

ReactiveFormsModule

@angular/forms

当要构建响应式表单时

RouterModule@angular/router

要使用路由功能,并且你要用到 RouterLink,.forRoot() 和 .forChild() 时

HttpClientModule

@angular/common/http

当你要和服务器对话时

导入模块

当你使用这些 Angular 模块时,在 AppModule(或适当的特性模块)中导入它们,并把它们列在当前 @NgModule 的 imports数组中。比如,在 Angular CLI 生成的基本应用中,BrowserModule 会在 app.module.ts 中 AppModule 的顶部最先导入。

/* import modules so that AppModule can access them */
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [ /* add modules here so Angular knows to use them */
    BrowserModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

文件顶部的这些导入是 JavaScript 的导入语句,而 @NgModule 中的 imports 数组则是 Angular 特有的。 要了解更多的不同点,参见 JavaScript 模块 vs. NgModule。

BrowserModule 和 CommonModule

BrowserModule 导入了 CommonModule,它贡献了很多通用的指令,比如 ngIf 和 ngFor。 另外,BrowserModule 重新导出了 CommonModule,以便它所有的指令在任何导入了 BrowserModule 的模块中都可以使用。

对于运行在浏览器中的应用来说,都必须在根模块中 AppModule 导入 BrowserModule ,因为它提供了启动和运行浏览器应用时某些必须的服务。BrowserModule 的提供商是面向整个应用的,所以它只能在根模块中使用,而不是特性模块。 特性模块只需要 CommonModule 中的常用指令,它们不需要重新安装所有全应用级的服务。

如果你把 BrowserModule 导入了惰性加载的特性模块中,Angular 就会返回一个错误,并告诉你要改用 CommonModule。

//意思是browsemodile是保证应用在浏览器运行所必须的,而commonmodule其实是包含在browsemodule中的,只是也可以值导入commonmodule,它从browsemodule中导出为特定功能的module

4,特性模块的分类

下面是特性模块的五个常用分类,包括五组:

  • 领域特性模块。

  • 带路由的特性模块。

  • 路由模块。

  • 服务特性模块

  • 可视部件特性模块。

虽然下面的指南中描述了每种类型的使用及其典型特征,但在实际的应用中,你还可能看到它们的混合体。

特性模块

指导原则

领域

领域特性模块用来给用户提供应用程序领域中特有的用户体验,比如编辑客户信息或下订单等。

它们通常会有一个顶级组件来充当该特性的根组件,并且通常是私有的。用来支持它的各级子组件。

领域特性模块大部分由 declarations 组成,只有顶级组件会被导出。

领域特性模块很少会有服务提供商。如果有,那么这些服务的生命周期必须和该模块的生命周期完全相同。

领域特性模块通常会由更高一级的特性模块导入且只导入一次。

对于缺少路由的小型应用,它们可能只会被根模块 AppModule 导入一次。

路由

带路由的特性模块是一种特殊的领域特性模块,但它的顶层组件会作为路由导航时的目标组件。

根据这个定义,所有惰性加载的模块都是路由特性模块。

带路由的特性模块不会导出任何东西,因为它们的组件永远不会出现在外部组件的模板中。

惰性加载的路由特性模块不应该被任何模块导入。如果那样做就会导致它被急性加载,破坏了惰性加载的设计用途。 也就是说你应该永远不会看到它们在 AppModule 的 imports 中被引用。 急性加载的路由特性模块必须被其它模块导入,以便编译器能了解它所包含的组件。

路由特性模块很少会有服务提供商,原因参见惰性加载的特性模块中的解释。如果那样做,那么它所提供的服务的生命周期必须与该模块的生命周期完全相同。不要在路由特性模块或被路由特性模块所导入的模块中提供全应用级的单例服务。

路由

路由模块为其它模块提供路由配置,并且把路由这个关注点从它的配套模块中分离出来。

路由模块通常会做这些:

  • 定义路由。

  • 把路由配置添加到该模块的 imports 中。

  • 把路由守卫和解析器的服务提供商添加到该模块的 providers 中。

  • 路由模块应该与其配套模块同名,但是加上“Routing”后缀。比如,foo.module.ts 中的 FooModule 就有一个位于 foo-routing.module.ts 文件中的 FooRoutingModule 路由模块。 如果其配套模块是根模块 AppModule,AppRoutingModule 就要使用 RouterModule.forRoot(routes) 来把路由器配置添加到它的 imports 中。 所有其它路由模块都是子模块,要使用 RouterModule.forChild(routes)。

  • 按照惯例,路由模块会重新导出这个 RouterModule,以便其配套模块中的组件可以访问路由器指令,比如 RouterLink 和 RouterOutlet。

  • 路由模块没有自己的可声明对象。组件、指令和管道都是特性模块的职责,而不是路由模块的。

路由模块只应该被它的配套模块导入。

服务

服务模块提供了一些工具服务,比如数据访问和消息。理论上,它们应该是完全由服务提供商组成的,不应该有可声明对象。Angular 的 HttpClientModule 就是一个服务模块的好例子。

根模块 AppModule 是唯一的可以导入服务模块的模块。

窗口部件

窗口部件模块为外部模块提供组件、指令和管道。很多第三方 UI 组件库都是窗口部件模块。

窗口部件模块应该完全由可声明对象组成,它们中的大部分都应该被导出。

窗口部件模块很少会有服务提供商。

如果任何模块的组件模板中需要用到这些窗口部件,就请导入相应的窗口部件模块。

下表中汇总了各种特性模块类型的关键特征。

特性模块

声明 declarations

提供商 providers

导出什么

被谁导入

领域

有

罕见

顶级组件

特性模块,AppModule

路由

有

罕见

无

无

路由

无

是(守卫)

RouterModule

特性(供路由使用)

服务

无

有

无

AppModule

窗口部件

有

罕见

有

特性

 

5,入口组件

入口组件有两种主要的类型:

  • 引导用的根组件。

  • 在路由定义中指定的组件。

引导用的入口组件

下面这个例子中指定了一个引导用组件 AppComponent,位于基本的 app.module.ts 中:、

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent] // bootstrapped entry component
})

可引导组件是一个入口组件,Angular 会在引导过程中把它加载到 DOM 中。 其它入口组件是在其它时机动态加载的,比如用路由器。

Angular 会动态加载根组件 AppComponent,是因为它的类型作为参数传给了 @NgModule.bootstrap 函数。

引导用的组件必须是入口组件,因为引导过程是命令式的,所以它需要一个入口组件。

路由到的入口组件

入口组件的第二种类型出现在路由定义中,就像这样:

content_copyconst routes: Routes = [
  {
    path: '',
    component: CustomerListComponent
  }
];

路由定义使用组件类型引用了一个组件:component: CustomerListComponent。

所有路由组件都必须是入口组件。这需要你把同一个组件添加到两个地方(路由中和 entryComponents 中),但编译器足够聪明,可以识别出这里是一个路由定义,因此它会自动把这些路由组件添加到 entryComponents 中。

entryComponents 数组

虽然 @NgModule 装饰器具有一个 entryComponents 数组,但大多数情况下你不用显式设置入口组件,因为 Angular 会自动把 @NgModule.bootstrap 中的组件以及路由定义中的组件添加到入口组件中。 虽然这两种机制足够自动添加大多数入口组件,但如果你要用其它方式根据类型来命令式的引导或动态加载某个组件,你就必须把它们显式添加到 entryComponents 中了。

entryComponents 和编译器

对于生产环境的应用,你总是希望加载尽可能小的代码。 这些代码应该只包含你实际使用到的类,并且排除那些从未用到的组件。因此,Angular 编译器只会为那些可以从 entryComponents 中直接或间接访问到的组件生成代码。 这意味着,仅仅往 @NgModule.declarations 中添加更多引用,并不能表达出它们在最终的代码包中是必要的。

实际上,很多库声明和导出的组件都是你从未用过的。 比如,Material Design 库会导出其中的所有组件,因为它不知道你会用哪一个。然而,显然你也不打算全都用上。 对于那些你没有引用过的,摇树优化工具就会把这些组件从最终的代码包中摘出去。

如果一个组件既不是入口组件也不没有在模板中使用过,摇树优化工具就会把它扔出去。 所以,最好只添加那些真正的入口组件,以便让应用尽可能保持精简。

6,特性模块

随着应用的增长,你可能需要组织与特定应用有关的代码。 这将帮你把特性划出清晰的边界。使用特性模块,你可以把与特定的功能或特性有关的代码从其它代码中分离出来。 为应用勾勒出清晰的边界,有助于开发人员之间、小组之间的协作,有助于分离各个指令,并帮助管理根模块的大小。

特性模块 vs. 根模块

与核心的 Angular API 的概念相反,特性模块是最佳的组织方式。特性模块提供了聚焦于特定应用需求的一组功能,比如用户工作流、路由或表单。 虽然你也可以用根模块做完所有事情,不过特性模块可以帮助你把应用划分成一些聚焦的功能区。特性模块通过它提供的服务以及共享出的组件、指令和管道来与根模块和其它模块合作。

如何制作特性模块

如果你已经有了 Angular CLI 生成的应用,可以在项目的根目录下输入下面的命令来创建特性模块。把这里的 CustomerDashboard 替换成你的模块名。你可以从名字中省略掉“Module”后缀,因为 CLI 会自动追加上它:

ng generate module CustomerDashboard 

这会让 CLI 创建一个名叫 customer-dashboard 的文件夹,其中有一个名叫 customer-dashboard.module.ts,内容如下

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class CustomerDashboardModule { }

//此处是特性模块生成的ts文件,其内容和app.module是一样的

无论根模块还是特性模块,其 NgModule 结构都是一样的。在 CLI 生成的特性模块中,在文件顶部有两个 JavaScript 的导入语句:第一个导入了 NgModule,它像根模块中一样让你能使用 @NgModule 装饰器;第二个导入了 CommonModule,它提供了很多像 ngIf 和 ngFor 这样的常用指令。 特性模块导入 CommonModule,而不是 BrowserModule,后者只应该在根模块中导入一次。 CommonModule 只包含常用指令的信息,比如 ngIf 和 ngFor,它们在大多数模板中都要用到,而 BrowserModule为浏览器所做的应用配置只会使用一次。

declarations 数组让你能添加专属于这个模块的可声明对象(组件、指令和管道)。 要添加组件,就在命令行中输入如下命令,这里的 customer-dashboard 是一个目录,CLI 会把特性模块生成在这里,而 CustomerDashboard 就是该组件的名字:

ng generate component customer-dashboard/CustomerDashboard

这会在 customer-dashboard 中为新组件生成一个目录,并使用 CustomerDashboardComponent 的信息修改这个特性模块:

// import the new component
import { CustomerDashboardComponent } from './customer-dashboard/customer-dashboard.component';
@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [
    CustomerDashboardComponent   //此处把本特性模块的 组件声明到module中
  ],
})

导入特性模块

要想把这个特性模块包含进应用中,你还得让根模块 app.module.ts 知道它。注意,在 customer-dashboard.module.ts文件底部 CustomerDashboardModule 的导出部分。这样就把它暴露出来,以便其它模块可以拿到它。要想把它导入到 AppModule 中,就把它加入 app.module.ts 的导入表中,并将其加入 imports 数组:

import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
// import the feature module here so you can add it to the imports array below
import { CustomerDashboardModule } from './customer-dashboard/customer-dashboard.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    CustomerDashboardModule // add the feature module here   //此处就i是特性模块里面自定义的组件,而在本应用模块中的自定义组件是声明到decalars中的
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

现在 AppModule 知道这个特性模块了。如果你往该特性模块中加入过任何服务提供商,AppModule 也同样会知道它,其它模块中也一样。不过,NgModule 并不会暴露出它们的组件。

渲染特性模块的组件模板

当 CLI 为这个特性模块生成 CustomerDashboardComponent 时,还包含一个模板 customer-dashboard.component.html,它带有如下页面脚本:

<p>
  customer-dashboard works!
</p>

要想在 AppComponent 中查看这些 HTML,你首先要在 CustomerDashboardModule 中导出 CustomerDashboardComponent。 在 customer-dashboard.module.ts 中,declarations 数组的紧下方,加入一个包含 CustomerDashboardModule 的 exports 数组:

exports: [
  CustomerDashboardComponent    //此块代码是在生成的特性模板中的类似appmodule.ts中的exports数组里面
]  

然后,在 AppComponent 的 app.component.html 中,加入标签 <app-customer-dashboard>:

<h1>
  {{title}}
</h1>

<!-- add the selector from the CustomerDashboardComponent -->
<app-customer-dashboard></app-customer-dashboard>

7,服务提供商

提供服务

如果你是用 Angular CLI 创建的应用,那么可以使用下列 CLI 的 ng generate 命令在项目根目录下创建一个服务。把其中的 User 替换成你的服务名。

ng generate service User

该命令会创建下列 UserService 骨架:

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

@Injectable({
  providedIn: 'root',  //providedIn: 'root' 指定 Angular 应该在根注入器中提供该服务。
})
export class UserService {
}

现在,你就可以在应用中到处注入 UserService 了。

该服务本身是 CLI 创建的一个类,并且加上了 @Injectable() 装饰器。默认情况下,该装饰器是用 providedIn 属性进行配置的,它会为该服务创建一个提供商。在这个例子中,providedIn: 'root' 指定 Angular 应该在根注入器中提供该服务。

提供商的作用域

当你把服务提供商添加到应用的根注入器中时,它就在整个应用程序中可用了。 另外,这些服务提供商也同样对整个应用中的类是可用的 —— 只要它们有供查找用的服务令牌。

你应该始终在根注入器中providedIn 与 NgModule提供这些服务 —— 除非你希望该服务只有在消费方要导入特定的 @NgModule 时才生效。

providedIn 与 NgModule

也可以规定某个服务只有在特定的 @NgModule 中提供。比如,如果你你希望只有当消费方导入了你创建的 UserModule 时才让 UserService 在应用中生效,那就可以指定该服务要在该模块中提供:

import { Injectable } from '@angular/core';
import { UserModule } from './user.module';

@Injectable({
  providedIn: UserModule,  //指定只在本UserModule里面提供服务
})
export class UserService {
}

上面的例子展示的就是在模块中提供服务的首选方式。之所以推荐该方式,是因为当没有人注入它时,该服务就可以被摇树优化掉。如果没办法指定哪个模块该提供这个服务,你也可以在那个模块中为该服务声明一个提供商:

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

import { UserService } from './user.service';

@NgModule({
  providers: [UserService],
})
export class UserModule {
}

使用惰性加载模块限制提供商的作用域

在 CLI 生成的基本应用中,模块是急性加载的,这意味着它们都是由本应用启动的,Angular 会使用一个依赖注入体系来让一切服务都在模块间有效。对于急性加载式应用,应用中的根注入器会让所有服务提供商都对整个应用有效。

当使用惰性加载时,这种行为需要进行改变。惰性加载就是只有当需要时才加载模块,比如路由中。它们没办法像急性加载模块那样进行加载。这意味着,在它们的 providers 数组中列出的服务都是不可用的,因为根注入器并不知道这些模块。

当 Angular 的路由器惰性加载一个模块时,它会创建一个新的注入器。这个注入器是应用的根注入器的一个子注入器。想象一棵注入器树,它有唯一的根注入器,而每一个惰性加载模块都有一个自己的子注入器。路由器会把根注入器中的所有提供商添加到子注入器中。如果路由器在惰性加载时创建组件, Angular 会更倾向于使用从这些提供商中创建的服务实例,而不是来自应用的根注入器的服务实例。

任何在惰性加载模块的上下文中创建的组件(比如路由导航),都会获取该服务的局部实例,而不是应用的根注入器中的实例。而外部模块中的组件,仍然会收到来自于应用的根注入器创建的实例。

虽然你可以使用惰性加载模块来提供实例,但不是所有的服务都能惰性加载。比如,像路由之类的模块只能在根模块中使用。路由器需要使用浏览器中的全局对象 location 进行工作。

使用组件限定服务提供商的作用域

另一种限定提供商作用域的方式是把要限定的服务添加到组件的 providers 数组中。组件中的提供商和 NgModule 中的提供商是彼此独立的。 当你要急性加载一个自带了全部所需服务的模块时,这种方式是有帮助的。 在组件中提供服务,会限定该服务只能在该组件中有效(同一模块中的其它组件不能访问它)。

src/app/app.component.ts

content_copy@Component({
/* . . . */
  providers: [UserService]
})

在模块中提供服务还是在组件中?

通常,要在根模块中提供整个应用都需要的服务,在惰性加载模块中提供限定范围的服务。

路由器工作在根级,所以如果你把服务提供商放进组件(即使是 AppComponent)中,那些依赖于路由器的惰性加载模块,将无法看到它们。

当你必须把一个服务实例的作用域限定到组件及其组件树中时,可以使用组件注册一个服务提供商。 比如,用户编辑组件 UserEditorComponent,它需要一个缓存 UserService 实例,那就应该把 UserService 注册进 UserEditorComponent中。 然后,每个 UserEditorComponent 的实例都会获取它自己的缓存服务实例。

8,单例服务

提供单例服务

在 Angular 中有两种方式来生成单例服务:

  • 把 @Injectable() 的 providedIn 属性声明为 root。

  • 把该服务包含在 AppModule 或某个只会被 AppModule 导入的模块中。

使用 providedIn

从 Angular 6.0 开始,创建单例服务的首选方式就是在那个服务类的 @Injectable 装饰器上把 providedIn 设置为 root。这会告诉 Angular 在应用的根上提供此服务。

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

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

NgModule 的 providers 数组

在基于 Angular 6.0 以前的版本构建的应用中,服务是注册在 NgModule 的 providers 数组中的,就像这样:

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

如果这个 NgModule 是根模块 AppModule,此 UserService 就会是单例的,并且在整个应用中都可用。虽然你可能会看到这种形式的代码,但是最好使用在服务自身的 @Injectable() 装饰器上设置 providedIn 属性的形式,因为 Angular 6.0 可以对这些服务进行摇树优化。

forRoot() 模式

通常,你只需要用 providedIn 提供服务,用 forRoot()/forChild() 提供路由即可。 不过,理解 forRoot() 为何能够确保服务只有单个实例,可以让你学会更深层次的开发知识。

如果模块同时定义了 providers(服务)和 declarations(组件、指令、管道),那么,当你同时在多个特性模块中加载此模块时,这些服务就会被注册在多个地方。这会导致出现多个服务实例,并且该服务的行为不再像单例一样。

有多种方式来防止这种现象:

 

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐