动机
角度应用程序从根NgModule开始。 Angular通过其由NgModules组成的模块系统来管理应用程序的依赖关系。除了普通的JavaScript模块,NgModules还可确保代码模块化和封装。
模块还提供最高级别的组织代码。每个NgModule都以自己的代码块为根。该模块为其代码提供从上到下的封装。然后,整个代码块可以导出到任何其他模块。从这个意义上说,NgModules就像守门员一样对待自己的代码块。
Angular的文档实用程序来自Angular编写的NgModules。除非声明它的NgModule包含在根中,否则没有可用的实用程序。这些实用程序还必须从其主机模块导出,以便导入程序可以使用它们。这种封装形式使开发人员能够在同一文件系统中生成自己的NgModule。
另外,了解为什么Angular CLI(命令行界面)从@angular/core
导入BrowserModule
是有意义的。只要使用CLI命令生成新应用程序,就会发生这种情况: ng new [name-of-app]
。
在大多数情况下,理解实施的点可能就足够了。但是,了解实现如何将自身连接到根目录甚至更好。这一切都是通过将BrowserModule
导入根目录而自动完成的。
NgModule装饰器
Angular通过修饰泛型类来定义其模块。 @NgModule
装饰器指示类对Angular的模块化目的。 NgModule类合并了可从模块范围访问/实例化的根依赖项。 “范围”表示源自模块元数据的任何内容。
import { NgModule } from '@angular/core';
@NgModule({
// … metadata …
})
export class AppModule { }
NgModule元数据
CLI生成的根NgModule包括以下元数据字段。这些字段为NgModule主持的代码块提供配置。
declarations: []
imports: []
providers: []
bootstrap: []
声明
声明数组包括由NgModule托管的所有组件,指令或管道。除非在元数据中明确导出,否则它们对模块是私有的。鉴于此用例,组件,指令和管道被昵称为“声明”。 NgModule必须声明一个唯一的声明。声明不能在单独的NgModules中声明两次。否则会抛出错误。请参阅以下示例。
import { NgModule } from '@angular/core';
import { TwoComponent } from './components/two.component.ts';
@NgModule({
declarations: [ TwoComponent ]
})
export class TwoModule { }
@NgModule({
imports: [ TwoModule ],
declarations: [ TwoComponent ]
})
export class OneModule { }
Angular为了NgModule封装而抛出一个错误。声明是NgModule的私有,默认情况下声明它们。如果多个NgModule需要某个可声明的,它们应该导入声明的NgModule。然后,此NgModule必须导出所需的声明,以便其他NgModule可以使用它。
import { NgModule } from '@angular/core';
import { TwoComponent } from './components/two.component.ts';
@NgModule({
declarations: [ TwoComponent ],
exports: [ TwoComponent ]
})
export class TwoModule { }
@NgModule({
imports: [ TwoModule ] // this module can now use TwoComponent
})
export class OneModule { }
上面的例子不会抛出错误。 TwoComponent已在两个NgModules之间唯一声明。 OneModule也可以访问TwoComponent,因为它导入了TwoModule。 TwoModule依次导出TwoComponent供外部使用。
进口
imports数组只接受NgModules。除了其他NgModules之外,此数组不接受声明,服务或其他任何内容。导入模块可以访问模块公布的可声明内容。
这解释了为什么导入BrowserModule
可以访问其各种实用程序。 BrowserModule
声明的每个可声明实用程序都从其元数据导出。导入BrowserModule
,导出的NgModule可以使用这些导出的声明。服务根本不导出,因为它们缺少相同的封装。
供应商
考虑到声明的封装,缺乏服务封装可能看起来很奇怪。请记住,服务与声明或导出分开,与提供者数组分开。
当Angular编译时,它会使根NgModule变平并将其导入到一个模块中。服务组合在一起由合并的NgModule托管的单个提供者阵列中。声明通过一组编译时标志来维护它们的封装。
如果NgModule提供程序包含匹配的标记值,则导入的根模块优先。过去,导入的最后一个NgModule优先。请参阅下一个示例。特别注意导入另外两个的NgModule。识别这会如何影响所提供服务的优先级。
import { NgModule } from '@angular/core';
@NgModule({
providers: [ AwesomeService ], // 1st precedence + importing module
imports: [
BModule,
CModule
]
})
export class AModule { }
@NgModule({
providers: [ AwesomeService ] // 3rd precedence + first import
})
export class BModule { }
@NgModule({
providers: [ AwesomeService ] // 2nd precedence + last import
})
export class CModule { }
从AModule的范围内实例化AwesomeService会产生AModule元数据中提供的AwesomeService实例。如果AModule的提供者省略了这项服务,那么CModule的AwesomeService将优先。如果CModule的提供者省略了AwesomeService,那么对于BModule来说等等。
引导
引导程序阵列接受组件。对于Array的每个组件,Angular将组件作为其自己的index.html
文件的根插入。 CLI生成的应用程序的根NgModule将始终具有此字段。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [ AppComponent ],
imports: [ BrowserModule ],
providers: [],
bootstrap: [ AppComponent ]
})
export class AppModule { }
AppComponent的元素将注入应用程序的基本HTML( index.html
)。组件树的其余部分从那里展开。总体NgModule的范围涵盖整个树以及从引导程序阵列注入的任何其他树。该数组通常只包含一个元素。这个组件将模块表示为单个元素及其底层树。
NgModules与JavaScript模块
您已经在前面的示例中看到过Angular和JavaScript模块一起工作。最顶层的import..from
语句构成了JavaScript模块系统。每个语句的目标的文件位置必须导出与请求匹配的类,变量或函数。 import { TARGET } from './path/to/exported/target'
。
在JavaScript中,模块是文件分隔的。如前所述,使用import..from
关键字导入文件。另一方面,NgModules是类分隔的,并用@NgModule
。因此,许多Angular模块可以存在于单个文件中。 JavaScript不会发生这种情况,因为文件定义了一个模块。
约定,惯例说每个@NgModule
装饰类应该有自己的文件。即便如此,要知道文件在Angular中不构成自己的模块。用@NgModule
类创建了这种区别。
JavaScript模块提供对@NgModule
元数据的标记引用。这发生在托管NgModule类的文件的顶部。 NgModule在其元数据字段(声明,导入,提供程序等)中使用这些标记。 @NgModule
可以装饰一个类的唯一原因是JavaScript从文件的顶部导入它。
// JavaScript module system provides tokens
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { AppService } from './app.service';
// Javascript module system is strict about where it imports. It can only import at the top of files.
// Angular NgModule uses those tokens in its metadata settings
@NgModule({ // import { NgModule } from '@angular/core';
declarations: [
AppComponent // import { AppComponent } from './app.component';
],
imports: [
BrowserModule // import { BrowserModule } from '@angular/platform-browser';
],
providers: [
AppService // import { AppService } from './app.service';
],
bootstrap: [
AppComponent // import { AppComponent } from './app.component';
]
})
export class AppModule { }
// JavaScript module system exports the class. Other modules can now import AppModule.
上面的例子没有介绍任何新内容。这里的重点是解释两个模块化系统如何协同工作的评论。 JavaScript提供令牌引用,而NgModule使用这些令牌来封装和配置其底层代码块。
功能模块
应用程序增长超时。适当地扩展它们需要应用程序组织。一个坚实的系统将使进一步发展更容易。
在Angular中,原理图确保目标驱动的代码段保持可区分。除了子NgModule原理图之外,还有NgModules本身。它们也是一种原理图。它们位于原理图列表中的其余部分,不包括应用程序本身。
一旦应用程序开始扩展,根模块就不应该独立存在。功能模块包括与根NgModule一起使用的任何NgModule。您可以将根模块视为具有bootstrap: []
元数据字段。功能应用程序确保根模块不会过度饱和其元数据。
功能模块代表任何导入模块隔离一段代码。他们可以独立处理整个应用部分。这意味着它可以在根模块导入要素模块的任何应用程序中使用。这种策略可以节省开发人员在多个应用程序中的时间和精力!它还使应用程序的根NgModule保持精简状态。
在应用程序的根NgModule中,将特征模块的标记添加到根的imports
数组中就可以了。无论功能模块导出或提供的内容都可供根目录使用。
// ./awesome.module.ts
import { NgModule } from '@angular/core';
import { AwesomePipe } from './awesome/pipes/awesome.pipe';
import { AwesomeComponent } from './awesome/components/awesome.component';
import { AwesomeDirective } from './awesome/directives/awesome.directive';
@NgModule({
exports: [
AwesomePipe,
AwesomeComponent,
AwesomeDirective
]
declarations: [
AwesomePipe,
AwesomeComponent,
AwesomeDirective
]
})
export class AwesomeModule { }
// ./app.module.ts
import { AwesomeModule } from './awesome.module';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
AwesomeModule,
BrowserModule
],
providers: [],
bootstrap: [
AppComponent
]
})
export class AppModule { }
// ./app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<!-- AwesomeDirective -->
<h1 appAwesome>This element mutates as per the directive logic of appAwesome.</h1>
<!-- AwesomePipe -->
<p>Generic output: {{ componentData | awesome }}</p>
<section>
<!-- AwesomeComponent -->
<app-awesome></app-awesome>
</section>
`
})
export class AppComponent {
componentData: string = "Lots of transformable data!";
}
<app-awesome></app-awesome>
(组件), awesome
(管道)和appAwesome
(指令)是AwesomeModule独有的。如果没有导出这些声明或AppModule忽略将AwesomeModule添加到其导入中,则AppComponent的模板将无法使用AwesomeModule的声明。 AwesomeModule是根NgModule AppModule的功能模块。
Angular提供了一些自己的模块,可以在输入时补充root。这是因为这些功能模块导出了他们创建的内容。
静态模块方法
有时,模块提供了使用自定义配置对象配置的选项。这是通过利用模块类中的静态方法实现的。
这种方法的一个示例是RoutingModule
,它直接在模块上提供.forRoot(...)
方法。
要定义自己的静态模块方法,请使用static
关键字将其添加到模块类。返回类型必须是ModuleWithProviders
。
// configureable.module.ts
import { AwesomeModule } from './awesome.module';
import { ConfigureableService, CUSTOM_CONFIG_TOKEN, Config } from './configurable.service';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
@NgModule({
imports: [
AwesomeModule,
BrowserModule
],
providers: [
ConfigureableService
]
})
export class ConfigureableModule {
static forRoot(config: Config): ModuleWithProviders {
return {
ngModule: ConfigureableModule,
providers: [
ConfigureableService,
{
provide: CUSTOM_CONFIG_TOKEN,
useValue: config
}
]
};
}
}
// configureable.service.ts
import { Inject, Injectable, InjectionToken } from '@angular/core';
export const CUSTOM_CONFIG_TOKEN: InjectionToken<string> = new InjectionToken('customConfig');
export interface Config {
url: string
}
@Injectable()
export class ConfigureableService {
constructor(
@Inject(CUSTOM_CONFIG_TOKEN) private config: Config
)
}
请注意, forRoot(...)
方法返回的对象几乎与NgModule
配置相同。
forRoot(...)
方法接受用户在导入模块时可以提供的自定义配置对象。
imports: [
...
ConfigureableModule.forRoot({ url: 'http://localhost' }),
...
]
然后使用名为CUSTOM_CONFIG_TOKEN
的自定义InjectionToken
提供ConfigureableService
并将其注入ConfigureableService
。应使用forRoot(...)
方法仅导入一次ConfigureableModule
。这为CUSTOM_CONFIG_TOKEN
提供了自定义配置。所有其他模块都应该导入ConfigureableModule
而不使用forRoot(...)
方法。
来自Angular的NgModule示例
Angular提供了各种可从@angular
导入的模块。两个最常导入的模块是CommonModule
和HttpClientModule
。
CommonModule
实际上是BrowserModule
一个子集。两者都提供对*ngIf
和*ngFor
结构指令的访问。 BrowserModule
包含Web浏览器的特定于平台的安装。 CommonModule
省略了此安装。 BrowserModule
应该导入到Web应用程序的根NgModule中。 CommonModule
为不需要平台安装的功能模块提供*ngIf
和*ngFor
。
HttpClientModule
提供HttpClient
服务。请记住,服务位于@NgModule
元数据的providers数组中。它们不可申报。在编译期间,每个NgModule都合并到一个模块中。与声明不同,服务不是封装的。它们都可以通过位于合并的NgModule旁边的根注入器实例化。
回到关键点。与任何其他服务一样, HttpClient
通过依赖注入(DI)通过其构造函数实例化为类。使用DI,根注入器将HttpClient
的实例注入构造函数。此服务允许开发人员使用服务的实现发出HTTP请求。
HttpClient
实现包括HttpClientModule
提供程序数组。只要根NgModule导入HttpClientModule
, HttpClient
就会按照预期从根的范围内实例化。
结论
您可能已经利用了Angular的NgModules。 Angular使得将模块很容易地放入根NgModule的imports数组中。实用程序通常从导入的模块的元数据中导出。因此,为什么它的实用程序在根NgModule中输入后突然变得可用。
NgModules与普通的JavaScript模块紧密配合。一个提供令牌,而一个使用它们进行配置。他们的团队合作产生了Angular框架独有的强大的模块化系统。它提供了一个新的组织层,高于除应用程序之外的所有其他原理图。
希望本文能够进一步加深您对NgModules的理解。对于一些更奇特的用例,Angular可以进一步利用这个系统。本文介绍了基础知识,以便您可以使用以下链接了解更多信息。
更多Angular教程
学习更多Angular教程