Angular模块 (NgModule)
Angular 模块是带有 @NgModule 装饰器函数的类。 @NgModule接收一个元数据对象,该对象告诉 Angular 如何编译和运行模块代码。 它标记出该模块拥有的组件、指令和管道, 并把它们的一部分公开出去,以便外部组件使用它们。 它可以向应用的依赖注入器中添加服务提供商。
每个 Angular 应用都有一个根模块类。 按照约定,它的类名叫做AppModule,被放在app.module.ts文件中。
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 { }
- 这个元数据只导入了一个辅助模块,BrowserModule,每个运行在浏览器中的应用都必须导入它
- BrowserModule注册了一些关键的应用服务提供商。 它还包括了一些通用的指令,例如NgIf和NgFor,所以这些指令在该模块的任何组件模板中都是可用的
下面范例AppComponent显示被绑定的标题:
src/app/app.component.ts (minimal)
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: '{{title}}
',
})
export class AppComponent {
title = 'Minimal NgModule';
}
- @NgModule.bootstrap属性把这个AppComponent标记为引导 (bootstrap) 组件。 当 Angular 引导应用时,它会在 DOM 中渲染AppComponent,并把结果放进index.html的
元素标记内部
Angular编译(JIT和AOT)
- 即时 (JiT) 编译
src/main.ts (dynamic)
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);
-
预编译器 (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中导入的那个。
导入支持性模块
src/app/title.component.html (ngIf)
Welcome, {{user}}
- 虽然AppModule没有声明过NgIf指令,但该应用仍然能正常编译和运行。为什么这样没问题呢?Angular 的编译器遇到不认识的 HTML 时应该不是忽略就是报错才对。
Angular 能识别NgIf指令,是因为我们以前导入过它。最初版本的AppModule就导入了BrowserModule。
src/app/app.module.ts (imports)
imports: [ BrowserModule ],
- 更准确的说,NgIf是在来自@angular/common的CommonModule中声明的。
- CommonModule提供了很多应用程序中常用的指令,包括NgIf和NgFor等。
- BrowserModule导入了CommonModule并且重新导出了它。最终的效果是:只要导入BrowserModule就自动获得了CommonModule中的指令。
- 很多熟悉的 Angular 指令并不属于CommonModule。 例如,NgModel和RouterLink分别属于 Angular 的FormsModule模块和RouterModule模块。在使用那些指令之前,我们也必须导入那些模块。
解决指令冲突
如果有两个同名指令,都叫做HighlightDirective,该怎么办呢?我们只要在 import 时使用as关键字来为第二个指令创建个别名就可以了。
src/app/app.module.1b.ts
import {
HighlightDirective as ContactHighlightDirective
} from './contact/highlight.directive';
当两个指令在同一个元素上争相设置颜色时,后声明的那个会胜出,因为它对 DOM 的修改覆盖了前一个
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}`);
}
}
src/app/contact/highlight.directive.ts
import { Directive, ElementRef } from '@angular/core';
@Directive({ selector: '[highlight], input' })
/** Highlight the attached element or an InputElement in blue */
export class HighlightDirective {
constructor(el: ElementRef) {
el.nativeElement.style.backgroundColor = 'powderblue';
console.log(
`* Contact highlight called for ${el.nativeElement.tagName}`);
}
}
- 在该例子中,联系人的HighlightDirective把应用标题的文本染成了蓝色,而我们原本期望它保持金色。
- 真正的问题在于,有两个不同的类试图做同一件事。多次导入同一个指令是没问题的,Angular 会移除重复的类,而只注册一次。从 Angular 的角度看,两个类并没有重复。Angular 会同时保留这两个指令,并让它们依次修改同一个 HTML 元素。
- 如果我们使用相同的选择器定义了两个不同的组件类,并指定了同一个元素标记,编译器就会报错说它无法在同一个 DOM位置插入两个不同的组件,我们可以通过创建特性模块来消除组件与指令的冲突。 特性模块可以把来自一个模块中的声明和来自另一个的区隔开
特性模块
- 特性模块是带有@NgModule装饰器及其元数据的类,就像根模块一样。 特性模块的元数据和根模块的元数据的属性是一样的
- 根模块和特性模块还共享着相同的执行环境。它们共享着同一个依赖注入器,这意味着某个模块中定义的服务在所有模块中也都能用
它们在技术上有两个显著的不同点:
- 我们引导根模块来启动应用,但导入特性模块来扩展应用
- 特性模块可以对其它模块暴露或隐藏自己的实现
用路由器实现惰性 (lazy) 加载
应用路由
- 该应用有三个特性模块:联系人 (Contact) 、英雄 (Hero) 和危机 (Crisis)
- ContactComponent组件是应用启动时的默认页
- ContactModule仍然会在应用启动时被主动加载
- HeroModule和CrisisModule会被惰性加载
src/app/app.component.ts (v3 - Template)
template: `
`
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 {}
- 第一个路由把空白 URL(例如http://host.com/)重定向到了另一个路径为contact的路由(例如http://host.com/contact)
- contact路由并不是在这里定义的,而是定义在联系人特性区自己的路由文件contact.routing.ts中。 对于带有路由组件的特性模块,其标准做法就是让它们定义自己的路由
另外两个路由使用惰性加载语法来告诉路由器要到哪里去找这些模块
src/app/app-routing.module.ts
{ path: 'crisis', loadChildren: 'app/crisis/crisis.module#CrisisModule' },
{ path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule' }
- 惰性加载模块的位置是字符串而不是类型。 在本应用中,该字符串同时标记出了模块文件和模块类,两者用#分隔开。
RouterModule.forRoot和forChild方法
- 只在根模块当中调用RouterModule.forRoot()
- 总是在特性路由模块中调用RouterModule.forChild()
共享模块
我们添加SharedModule来存放这些公共组件、指令和管道,并且共享给那些需要它们的模块
- 创建src/app/shared目录
- 把AwesomePipe和HighlightDirective从src/app/contact移到src/app/shared中
- 从src/app/和src/app/hero目录中删除HighlightDirective类
- 创建SharedModule类来管理这些共享的素材
- 更新其它特性模块,导入SharedModule
下面就是这个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, HighlightDirective,
CommonModule, FormsModule ]
})
export class SharedModule { }
值得注意的有:
- 它导入了CommonModule,这是因为它的组件需要这些公共指令
- 正如我们所期待的,它声明并导出了工具性的管道、指令和组件类
- 它重新导出了CommonModule和FormsModule
重新导出其它模块
- 当回顾应用程序时,我们注意到很多需要SharedModule的组件也同时用到了来自CommonModule的NgIf和NgFor指令, 并且还通过来自FormsModule的[(ngModel)]指令绑定到了组件的属性,那些声明这些组件的模块将不得不同时导入CommonModule、FormsModule和SharedModule
- 通过让SharedModule重新导出CommonModule和FormsModule模块,我们可以消除这种重复,于是导入SharedModule的模块也同时免费获得了CommonModule和FormsModule
- 实际上,SharedModule本身所声明的组件没绑定过[(ngModel)],那么,严格来说SharedModule并不需要导入FormsModule
- 这时SharedModule仍然可以导出FormsModule,而不需要先把它列在imports中
为什么 Service 没有被共享
- 虽然很多组件都共享着同一个服务实例,但它们是靠Angular 的依赖注入体系实现的,而不是模块体系
- 不要在共享模块中把应用级单例添加到providers中。 否则如果一个惰性加载模块导入了此共享模块,就会导致它自己也生成一份此服务的实例
核心 (Core) 模块
我们可以把一些组件收集到单独的CoreModule中,并且只在应用启动时导入它一次,而不会在其它地方导入它
用 CoreModule.forRoot 配置核心服务
模块的静态方法forRoot可以同时提供并配置服务,它接收一个服务配置对象,并返回一个ModuleWithProviders。这个简单对象具有两个属性:
- ngModule - CoreModule类
- providers - 配置好的服务提供商
根模块AppModule会导入CoreModule类并把它的providers添加到AppModule的服务提供商中,更精确的说法是,Angular 会先累加所有导入的提供商,然后才把它们追加到@NgModule.providers中, 这样可以确保我们显式添加到AppModule中的那些提供商总是优先于从其它模块中导入的提供商
禁止多次导入CoreModule
src/app/core/core.module.ts
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装饰器表示找不到该服务也无所谓。 于是注入器会返回null,parentModule参数也就被赋成了空值,而构造函数没有任何异常。
- 如果我们错误的把CoreModule导入了一个惰性加载模块(例如HeroModule)中,那就不一样了。
- Angular 创建一个惰性加载模块,它具有自己的注入器,它是根注入器的子注入器。 @SkipSelf让 Angular 在其父注入器中查找CoreModule,这次,它的父注入器却是根注入器了(而上次父注入器是空)。 当然,这次它找到了由根模块AppModule导入的实例。 该构造函数检测到存在parentModule,于是抛出一个错误。