Angular采用模块的形式来组织应用,将相关业务功能或者有内部联系的部分进行内聚化设计,封装成单独的模块,而整个应该则由多个模块来构成
应用模块化
- 打包特定领域/工作流的功能,隔离功能实现
- 方便组织应用,多个模块可独立开发
- 方便导入使用外部控件,很多Angular库和第三方库都是以模块形式提供,例如:FormsModule、HttpModule、RouterModule等
- 方便把服务加到应用程序中
模块化带来应用设计的简易化,应用组织就像搭积木一样方便,像这样:
模块的组成
模块在angular里只是由@NgModule装饰器提供元数据的类,这个元数据对象指示如何编译和运行该模块,基本构成:
@NgModule({
imports:[], //导入其它模块
declarations: [], // 声明组件指令管道
providers: [] //声明服务提供商
exports: [] //公开某些类
})
export class ContactModule { }
(1) imports模块
导入支持模块,会发生几件事:
1 . 被导入模块公开的组件/指令/管道将和导入目标模块declarations合并,并供目标模块使用,但导入的成员优先级没目标模块的高
2 . 被导入模块的providers会累加到导入目标模块的提供商,追加到@NgModule.providers中,并把它们注入到目标模块一级的注入器,比如导入到根模块,则注入到根注入器
3 . 被导入的模块的路由和导入目标模块路由将合并
(2) declarations[]
该数组声明本模块所拥有的组件,指令,管道等成员,数组里成员有以下特点:
- 数组里声明的成员默认都是私有的,除非放进exports公开他们
- 一个组件/指令/管道只能属于一个模块,不能同时把一个类声明在几个模块中
- 数组里的成员可以公开给其他模块直接导入使用
(3) providers[]
模块是为模块中所有成员提供服务的最佳途径,在模块中声明服务提供商有几个特点:
- 模块内声明的服务商是模块级的,可供模块所有组件或其他成员使用
- 如果此模块被直接导入到根模块时,服务商注册到根注入器,整个应用都可使用
- 如果此模块被导入到延迟模块,则注入到模块子注入器,可供延迟模块内使用
- 延迟加载模块是限制服务商在模块内生效的唯一途径
(3) exports[]
设置公开成员,至于在公开数组里声明的成员才能其他模块导入使用,可供导出的成员类型有如下几种:
- declarations数组中的成员
- imports数组中的模块
- 不在imports数组中的其他模块(可不导入就直接导出)
模块成员导入后有两种使用方式:
1 . 通过选择器直接在模板中使用,使用于组件/指令/管道
2 . 通过路由导航来使用,适用于导入的组件,不过更好的设计是将路由移至被导入模块,导入后合并路由即可
如果是在本模块路由到导入模块的某个组件时,该组件不必公开,私有属性直接导入即可
模块树
模块树是组成angular的基本树之一,体现了应用的基本结构,angular一般可通过直接导入和懒加载两种方式来将各个模块联系在一起,下图是一个基本模块树的关联结构:
应用模块的组建方式
一个angular应用一般有四种类型的模块:
- 引导模块(AppModule),一般指根模块,用于引导和组织整个应用
- 核心模块(CoreModule),用于封装一次性的类,隐藏它们的实现细节,一般包含只在应用启动时使用到的组件类和全应用级服务提供商,根模块导入它来获取相关能力
- 共享模块(ShareModule),用于封装公共组件、指令和管道,共享给那些需要它们的模块,
- 特性模块(FeatrueModule),用于封装工作流和业务功能
下图是一个应用中模块的典型组建方式
最后,一些需要注意的细节:
1 . 根模块和特性模块共享着相同的执行环境和同一个依赖注入器,在某个模块中定义的服务在所有其他模块中也都能用。特性模块通过自己提供的服务和对外公开的组件、指令、管道来与其它模块进行协同工作
2 . 在根注入器注册的提供商,每个服务均为单例,在全应用范围的组件都可使用,无论组件时主动导入还是惰性加载的
3 . 共享模块中不应出现providers服务商,否则如果一个惰性加载模块导入了此共享模块,就会相应生成一份服务的实例,这个在设计中时不被期望的
4 . 导入目标模块不会继承被导入模块中对组件、指令或管道的访问权,两个模块的imports数组是互不相干的,除非被导入模块将导入模块重新导出。所以,对公共模块的使用,并不是在根模块导入一次就可以了,而应该在每一个需要使用的模块中都需要导入
5 . 被导入模块的公开指令的优先级小于导入目标模块的指令,渲染时先执行导入的后执行本模块的,如果有同名指令存在,本模块的覆盖导入的;而如果在本模块的declarations如果有同名指令,后申明的覆盖先申明的
6 . 多次导入同一个指令是没问题的,Angular 会移除重复的类,只注册一次。但如果有两个不同的类,只是恰好有相同的名字,angular认为并没有重复,会同时保留这两个指令,只是在使用的时候两个指令都会先后调用到,优先级高的获最后结果
7 . 显式添加到AppModule中的那些提供商在优先级上要优于从其它模块中导入的提供商
8 . 立即模块加载是直接导入,在应用启动时加载它的路由和组件;懒加载模块是通过路由进行惰性加载,指的是用户第一次访问时异步获取加载文件,加载速度取决于浏览器一次加载文件的数量,比如:谷歌一般一次8个,所以要规划好懒加载模块的大小,以免加载时间过长
9 . 对于惰性加载模块,一般不要直接导入使用,因为一般这种模块都会有自己的路由配置,导入的同时也会导入它的路由,会对目标模块的路由导航造成影响,因为路由匹配是第一次匹配到即胜出的原则,
如果惰性模块的路由和目标模块相似,会引起不必要的错误
10 . 只能在根模块AppModule中导入BrowserModule,在其它任何模块中都不要导入BrowserModule。特性模块和惰性加载模块应该改成导入CommonModule, 它们需要通用的指令,它们不需要重新初始化全应用级的提供商。
如果你在惰性加载模块中导入BrowserModule,Angular就会抛出一个错误, 特性模块中导入CommonModule可以让它能用在任何目标平台上。
11 . Angular不允许模块之间出现循环依赖,所以不要让模块’A’导入模块’B’,而模块’B’又导入模块’A’
12 . 静态方法forRoot和forChild是一个约定,它可以让开发人员更轻松的配置模块的提供商。
RouterModule.forRoot就是一个很好的例子,应用把一个Routes对象传给forRoot(),为的就是使用路由配置全应用级的Router服务。只能在应用的根模块AppModule中调用并导入.forRoot的结果。 在其它模块中导入它,特别是惰性加载模块中,是违反设计目标的并会导致一个运行时错误。
RouterModule也提供了静态方法forChild,用于配置惰性加载模块的路由。
13 . 导入的提供商很容易被由其它导入模块中的提供商替换
假设模块需要一个定制过的HttpBackend,它为所有的Http请求添加一个特别的请求头。 如果应用中其它地方的另一个模块也定制了HttpBackend或仅仅导入了HttpModule,它就会改写当前模块的HttpBackend提供商,丢掉了这个特别的请求头。 这样服务器就会拒绝来自该模块的请求。
如果你必须防范这种“提供商腐化”现象,那就不要依赖于“启动时加载”模块的providers。
14 . 如何知道一个模块或服务是否已经加载过了?
src/app/core/core.module.ts (Constructor)
constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error(
'CoreModule is already loaded. Import it in the AppModule only');
}
}
15 . Angular总是会动态加载AppComponent,无论把它的类型列在了@NgModule.bootstrap函数中,还是命令式的调用该模块的ngDoBootstrap方法来引导它。
在路由定义中用到的组件也同样是入口组件,路由定义根据类型来引用组件,忽略路由组件的选择器(即使它有选择器),并且把该组件动态加载到RouterOutlet中。
编译器无法通过在其它组件的模板中查找来发现这些入口组件。 我们必须通过把它们加入entryComponents列表中来让编译器知道它们的存在。
Angular会自动把下列类型的组件添加到模块的entryComponents(入口组件)中:
- 那些出现在@NgModule.bootstrap列表中的组件。
- 那些被路由定义引用的组件。
我们并不需要显式的引用这些组件,虽然引用了也没坏处。
16 . 引导组件是入口组件的一种。 它是被Angular的引导(应用启动)过程加载到DOM中的入口组件。 其它入口组件则是被其它方式动态加载的,比如被路由器加载。
@NgModule.bootstrap属性告诉编译器这是一个入口组件,同时它应该生成一些代码来用该组件引导此应用。
不需要把组件同时列在bootstrap和entryComponent列表中 —— 虽然这样做也没坏处
17 . 虽然你可以使用惰性加载模块来提供实例,但不是所有的服务都能惰性加载。比如,像路由之类的模块只能在根模块中使用。路由器需要使用浏览器中的全局对象 location 进行工作
18 . 在应用搭建中根据实际需要可作不同的划分
(1) 根据功能不同可分为:根模块,特性模块,懒加载模块,路由模块,服务模块,库模块
(2) 根据代码组织不同还可以分为:根模块,核心模块,共享模块,路由模块,服务模块,特性模块