博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[译] 别再对 Angular Modules 感到迷惑
阅读量:6429 次
发布时间:2019-06-23

本文共 14879 字,大约阅读时间需要 49 分钟。

原文链接:

Angular Modules 是个相当复杂的话题,甚至 Angular 开发团队在官网上写了好几篇有关 的文章教程。这些教程清晰的阐述了 Modules 的大部分内容,但是仍欠缺一些内容,导致很多开发者被误导。我看到很多开发者由于不知道 Modules 内部是如何工作的,所以经常理解错相关概念,使用 Modules API 的姿势也不正确。

本文将深度解释 Modules 内部工作原理,争取帮你消除一些常见的误解,而这些错误我在 StackOverflow 上经常看到有人提问。

模块封装

Angular 引入了模块封装的概念,这个和 ES 模块概念很类似(注:ES Modules 概念可以查看 TypeScript 中文网的 ),基本意思是所有声明类型,包括组件、指令和管道,只可以在当前模块内部,被其他声明的组件使用。比如,如果我在 App 组件中使用 A 模块的 a-comp 组件:

@Component({  selector: 'my-app',  template: `      

Hello {
{name}}

`})export class AppComponent { }复制代码

Angular 编译器就会抛出错误:

Template parse errors: 'a-comp' is not a known element

这是因为 App 模块中没有申明 a-comp 组件,如果我想要使用这个组件,就不得不导入 A 模块,就像这样:

@NgModule({  imports: [..., AModule]})export class AppModule { }复制代码

上面描述的就是 模块封装。不仅如此,如果想要 a-comp 组件正常工作,得设置它为可以公开访问,即在 A 模块的 exports 属性中导出这个组件:

@NgModule({  ...  declarations: [AComponent],  exports: [AComponent]})export class AModule { }复制代码

同理,对于指令和管道,也得遵守 模块封装 的规则:

@NgModule({  ...  declarations: [    PublicPipe,     PrivatePipe,     PublicDirective,     PrivateDirective  ],  exports: [PublicPipe, PublicDirective]})export class AModule {}复制代码

需要注意的是,模块封装 原则不适用于在 entryComponents 属性中注册的组件,如果你在使用动态视图时,像 这篇文章中所描述的方式去实例化动态组件,就不需要在 A 模块的 exports 属性中去导出 a-comp 组件。当然,还得导入 A 模块。

大多数初学者会认为 providers 也有封装规则,但实际上没有。在 非懒加载模块 中申明的任何 provider 都可以在程序内的任何地方被访问,下文将会详细解释原因。

模块层级

初学者最大的一个误解就是认为,一个模块导入其他模块后会形成一个模块层级,认为该模块会成为这些被导入模块的父模块,从而形成一个类似模块树的层级,当然这么想也很合理。但实际上,不存在这样的模块层级。因为 所有模块在编译阶段会被合并,所以导入和被导入模块之间不存在任何层级关系。

就像 一样,Angular 编译器也会为根模块生成一个模块工厂,根模块就是你在 main.ts 中,以参数传入 bootstrapModule() 方法的模块:

platformBrowserDynamic().bootstrapModule(AppModule);复制代码

Angular 编译器使用 方法来创建该模块工厂(注:可参考 -> -> -> -> ),该方法需要几个参数(注:为清晰理解,不翻译。最新版本不包括第三个依赖参数。):

  • module class reference
  • bootstrap components
  • component factory resolver with entry components
  • definition factory with merged module providers

最后两点解释了为何 providersentry components 没有模块封装规则,因为编译结束后没有多个模块,而仅仅只有一个合并后的模块。并且在编译阶段,编译器不知道你将如何使用 providers 和动态组件,所以编译器去控制封装。但是在编译阶段的组件模板解析过程时,编译器知道你是如何使用组件、指令和管道的,所以编译器能控制它们的私有申明。(注:providersentry components 是整个程序中的动态部分 dynamic content,Angular 编译器不知道它会被如何使用,但是模板中写的组件、指令和管道,是静态部分 static content,Angular 编译器在编译的时候知道它是如何被使用的。这点对理解 Angular 内部工作原理还是比较重要的。)

让我们看一个生成模块工厂的示例,假设你有 AB 两个模块,并且每一个模块都定义了一个 provider 和一个 entry component

@NgModule({  providers: [{provide: 'a', useValue: 'a'}],  declarations: [AComponent],  entryComponents: [AComponent]})export class AModule {}@NgModule({  providers: [{provide: 'b', useValue: 'b'}],  declarations: [BComponent],  entryComponents: [BComponent]})export class BModule {}复制代码

根模块 App 也定义了一个 provider 和根组件 app,并导入 AB 模块:

@NgModule({  imports: [AModule, BModule],  declarations: [AppComponent],  providers: [{provide: 'root', useValue: 'root'}],  bootstrap: [AppComponent]})export class AppModule {}复制代码

当编译器编译 App 根模块生成模块工厂时,编译器会 合并 所有模块的 providers,并只为合并后的模块创建模块工厂,下面代码展示模块工厂是如何生成的:

createNgModuleFactory(    // reference to the AppModule class    AppModule,    // reference to the AppComponent that is used    // to bootstrap the application    [AppComponent],    // module definition with merged providers    moduleDef([        ...        // reference to component factory resolver        // with the merged entry components        moduleProvideDef(512, jit_ComponentFactoryResolver_5, ..., [            ComponentFactory_
, ComponentFactory_
, ComponentFactory_
]) // references to the merged module classes // and their providers moduleProvideDef(512, AModule, AModule, []), moduleProvideDef(512, BModule, BModule, []), moduleProvideDef(512, AppModule, AppModule, []), moduleProvideDef(256, 'a', 'a', []), moduleProvideDef(256, 'b', 'b', []), moduleProvideDef(256, 'root', 'root', [])]);复制代码

从上面代码知道,所有模块的 providersentry components 都将会被合并,并传给 moduleDef() 方法,所以无论导入多少个模块,编译器只会合并模块,并只生成一个模块工厂。该模块工厂会使用模块注入器来生成合并模块对象(注:查看 ),然而由于只有一个合并模块,Angular 将只会使用这些 providers,来生成一个单例的根注入器。

现在你可能想到,如果两个模块里定义了相同的 provider token,会发生什么?

第一个规则 则是导入其他模块的模块中定义的 provider 总是优先胜出,比如在 AppModule 中也同样定义一个 a provider

@NgModule({  ...  providers: [{provide: 'a', useValue: 'root'}],})export class AppModule {}复制代码

查看生成的模块工厂代码:

moduleDef([     ...     moduleProvideDef(256, 'a', 'root', []),     moduleProvideDef(256, 'b', 'b', []), ]);复制代码

可以看到最后合并模块工厂包含 moduleProvideDef(256, 'a', 'root', []),会覆盖 AModule 中定义的 {provide: 'a', useValue: 'a'}

第二个规则 是最后导入模块的 providers,会覆盖前面导入模块的 providers。同样,也在 BModule 中定义一个 a provider

@NgModule({  ...  providers: [{provide: 'a', useValue: 'b'}],})export class BModule {}复制代码

然后按照如下顺序在 AppModule 中导入 AModuleBModule

@NgModule({  imports: [AModule, BModule],  ...})export class AppModule {}复制代码

查看生成的模块工厂代码:

moduleDef([     ...     moduleProvideDef(256, 'a', 'b', []),     moduleProvideDef(256, 'root', 'root', []), ]);复制代码

所以上面代码已经验证了第二条规则。我们在 BModule 中定义了 {provide: 'a', useValue: 'b'},现在让我们交换模块导入顺序:

@NgModule({  imports: [BModule, AModule],  ...})export class AppModule {}复制代码

查看生成的模块工厂代码:

moduleDef([     ...     moduleProvideDef(256, 'a', 'a', []),     moduleProvideDef(256, 'root', 'root', []), ]);复制代码

和预想一样,由于交换了模块导入顺序,现在 AModule{provide: 'a', useValue: 'a'} 覆盖了 BModule{provide: 'a', useValue: 'b'}

注:上文作者提供了 AppModule 被 @angular/compiler 编译后的代码,并针对编译后的代码分析多个 modules 的 providers 会被合并。实际上,我们可以通过命令 yarn ngc -p ./tmp/tsconfig.json 自己去编译一个小实例看看,其中,./node_modules/.bin/ngc@angular/compiler-cli 提供的 cli 命令。我们可以使用 ng new module 新建一个项目,我的版本是 6.0.5。然后在项目根目录创建 /tmp 文件夹,然后加上 tsconfig.json,内容复制项目根目录的 tsconfig.json,然后加上一个 module.ts 文件。module.ts 内容包含根模块 AppModule,和两个模块 AModuleBModuleAModule 提供 AService{provide:'a', value:'a'}{provide:'b', value:'b'} 服务,而 BModule 提供 BService{provide: 'b', useValue: 'c'}AModuleBModule 按照先后顺序导入根模块 AppModule,完整代码如下:

import {Component, Inject, Input, NgModule} from '@angular/core';import "./goog"; // goog.d.ts 源码文件拷贝到 /tmp 文件夹下import "hammerjs";import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';export class AService {}@NgModule({  providers: [    AService,    {provide: 'a', useValue: 'a'},    {provide: 'b', useValue: 'b'},  ],})export class AModule {}export class BService {}@NgModule({  providers: [    BService,    {provide: 'b', useValue: 'c'}  ]})export class BModule {}@Component({  selector: 'app',  template: `    

{

{name}}

`})export class AppComp { name = 'lx1036';}export class AppService {}@NgModule({ imports: [AModule, BModule], declarations: [AppComp], providers: [ AppService, {provide: 'a', useValue: 'b'} ], bootstrap: [AppComp]})export class AppModule {}platformBrowserDynamic().bootstrapModule(AppModule).then(ngModuleRef => console.log(ngModuleRef));复制代码

然后 yarn ngc -p ./tmp/tsconfig.json 使用 @angular/compiler 编译这个 module.ts 文件会生成多个文件,包括 module.jsmodule.factory.js。 先看下 module.jsAppModule 类会被编译为如下代码,发现我们在 @NgModule 类装饰器中写的元数据,会被赋值给 AppModule.decorators 属性,如果是属性装饰器,会被赋值给 propDecorators 属性:

var AppModule = /** @class */ (function () {    function AppModule() {    }    AppModule.decorators = [        { type: core_1.NgModule, args: [{                    imports: [AModule, BModule],                    declarations: [AppComp],                    providers: [                        AppService,                        { provide: 'a', useValue: 'b' }                    ],                    bootstrap: [AppComp]                },] },    ];    return AppModule;}());exports.AppModule = AppModule;复制代码

然后看下 module.factory.js 文件,这个文件很重要,本文关于模块 providers 合并就可以从这个文件看出。该文件 AppModuleNgFactory 对象中就包含合并后的 providers,这些 providers 来自于 AppModule,AModule,BModule,并且 AppModule 中的 providers 会覆盖其他模块的 providersBModule 中的 providers 会覆盖 AModuleproviders,因为 BModuleAModule 之后导入,可以交换导入顺序看看发生什么。其中,ɵcmf 是 ,ɵmod 是 ,ɵmpd 是 moduleProvideDef 第一个参数是 节点类型,用来表示当前节点是什么类型,比如 i0.ɵmpd(256, "a", "a", []) 中的 256 表示 是个值类型。

Object.defineProperty(exports, "__esModule", { value: true });var i0 = require("@angular/core");var i1 = require("./module");var AModuleNgFactory = i0.ɵcmf(  i1.AModule,  [],  function (_l) {    return i0.ɵmod([      i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [[8, []], [3, i0.ComponentFactoryResolver], i0.NgModuleRef]),      i0.ɵmpd(4608, i1.AService, i1.AService, []),      i0.ɵmpd(1073742336, i1.AModule, i1.AModule, []),      i0.ɵmpd(256, "a", "a", []),      i0.ɵmpd(256, "b", "b", [])]    );  });exports.AModuleNgFactory = AModuleNgFactory;var BModuleNgFactory = i0.ɵcmf(  i1.BModule,  [],  function (_l) {    return i0.ɵmod([      i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [[8, []], [3, i0.ComponentFactoryResolver], i0.NgModuleRef]),      i0.ɵmpd(4608, i1.BService, i1.BService, []),      i0.ɵmpd(1073742336, i1.BModule, i1.BModule, []),      i0.ɵmpd(256, "b", "c", [])    ]);  });exports.BModuleNgFactory = BModuleNgFactory;var AppModuleNgFactory = i0.ɵcmf(  i1.AppModule,  [i1.AppComp], // AppModule 的 bootstrapComponnets 启动组件数据  function (_l) {    return i0.ɵmod([      i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [[8, [AppCompNgFactory]], [3, i0.ComponentFactoryResolver], i0.NgModuleRef]),      i0.ɵmpd(4608, i1.AService, i1.AService, []),      i0.ɵmpd(4608, i1.BService, i1.BService, []),      i0.ɵmpd(4608, i1.AppService, i1.AppService, []),      i0.ɵmpd(1073742336, i1.AModule, i1.AModule, []),      i0.ɵmpd(1073742336, i1.BModule, i1.BModule, []),      i0.ɵmpd(1073742336, i1.AppModule, i1.AppModule, []),      i0.ɵmpd(256, "a", "b", []),      i0.ɵmpd(256, "b", "c", [])]);  });exports.AppModuleNgFactory = AppModuleNgFactory;复制代码

自己去编译实践下,会比只看文章的解释,效率更高很多。

懒加载模块

现在又有一个令人困惑的地方-懒加载模块。官方文档是这样说的(注:不翻译):

Angular creates a lazy-loaded module with its own injector, a child of the root injector… So a lazy-loaded module that imports that shared module makes its own copy of the service.

所以我们知道 Angular 会为懒加载模块创建它自己的注入器,这是因为 Angular 编译器会为每一个懒加载模块编译生成一个 独立的组件工厂。这样在该懒加载模块中定义的 providers 不会被合并到主模块的注入器内,所以如果懒加载模块中定义了与主模块有着相同的 provider,则 Angular 编译器会为该 provider 创建一份新的服务对象。

所以懒加载模块也会创建一个层级,但是注入器的层级,而不是模块层级。 在懒加载模块中,导入的所有模块同样会在编译阶段被合并为一个,就和上文非懒加载模块一样。

以上相关逻辑是在 @angular/router 包的 RouterConfigLoader 代码里,该段展示了如何加载模块和创建注入器:

export class RouterConfigLoader {  load(parentInjector, route) {    ...    const moduleFactory$ = this.loadModuleFactory(route.loadChildren);    return moduleFactory$.pipe(map((factory: NgModuleFactory
) => { ... const module = factory.create(parentInjector); ... })); } private loadModuleFactory(loadChildren) { ... return this.loader.load(loadChildren) }}复制代码

查看这行代码:

const module = factory.create(parentInjector);复制代码

传入父注入器来创建懒加载模块新对象。

forRoot 和 forChild 静态方法

查看官网是如何介绍的(注:不翻译):

Add a CoreModule.forRoot method that configures the core UserService… Call forRoot only in the root application module, AppModule

这个建议是合理的,但是如果你不理解为什么这样做,最终会写出类似下面代码:

@NgModule({  imports: [    SomeLibCarouselModule.forRoot(),    SomeLibCheckboxModule.forRoot(),    SomeLibCloseModule.forRoot(),    SomeLibCollapseModule.forRoot(),    SomeLibDatetimeModule.forRoot(),    ...  ]})export class SomeLibRootModule {...}复制代码

每一个导入的模块(如 CarouselModuleCheckboxModule 等等)不再定义任何 providers,但是我觉得没理由在这里使用 forRoot,让我们一起看看为何在第一个地方需要 forRoot

当你导入一个模块时,通常会使用该模块的引用:

@NgModule({ providers: [AService] })export class A {}@NgModule({ imports: [A] })export class B {}复制代码

这种情况下,在 A 模块中定义的所有 providers 都会被合并到主注入器,并在整个程序上下文中可用,我想你应该已经知道原因-上文中已经解释了所有模块 providers 都会被合并,用来创建注入器。

Angular 也支持另一种方式来导入带有 providers 的模块,它不是通过使用模块的引用来导入,而是传一个实现了 ModuleWithProviders 接口的对象:

interface ModuleWithProviders {    ngModule: Type
providers?: Provider[] }复制代码

上文中我们可以这么改写:

@NgModule({})class A {}const moduleWithProviders = {    ngModule: A,    providers: [AService]};@NgModule({    imports: [moduleWithProviders]})export class B {}复制代码

最好能在模块对象内使用一个静态方法来返回 ModuleWithProviders,而不是直接使用 ModuleWithProviders 类型的对象,使用 forRoot 方法来重构代码:

@NgModule({})class A {  static forRoot(): ModuleWithProviders {    return {ngModule: A, providers: [AService]};  }}@NgModule({  imports: [A.forRoot()]})export class B {}复制代码

当然对于文中这个简单示例没必要定义 forRoot 方法返回 ModuleWithProviders 类型对象,因为可以在两个模块内直接定义 providers 或如上文使用一个 moduleWithProviders 对象,这里仅仅也是为了演示效果。然而如果我们想要分割 providers,并在被导入模块中分别定义这些 providers,那上文中的做法就很有意义了。

比如,如果我们想要为非懒加载模块定义一个全局的 A 服务,为懒加载模块定义一个 B 服务,就需要使用上文的方法。我们使用 forRoot 方法为非懒加载模块返回 providers,使用 forChild 方法为懒加载模块返回 providers

@NgModule({})class A {  static forRoot() {    return {ngModule: A, providers: [AService]};  }  static forChild() {    return {ngModule: A, providers: [BService]};  }}@NgModule({  imports: [A.forRoot()]})export class NonLazyLoadedModule {}@NgModule({  imports: [A.forChild()]})export class LazyLoadedModule {}复制代码

因为非懒加载模块会被合并,所以 forRoot 中定义的 providers 全局可用(注:包括非懒加载模块和懒加载模块),但是由于懒加载模块有它自己的注入器,你在 forChild 中定义的 providers 只在当前懒加载模块内可用(注:不翻译)。

Please note that the names of methods that you use to return ModuleWithProviders structure can be completely arbitrary. The names forChild and forRoot I used in the examples above are just conventional names recommended by Angular team and used in the RouterModuleimplementation.(注:即 forRoot 和 forChild 方法名称可以随便修改。)

好吧,回到最开始要看的代码:

@NgModule({  imports: [    SomeLibCarouselModule.forRoot(),    SomeLibCheckboxModule.forRoot(),    ...复制代码

根据上文的理解,就发现没有必要在每一个模块里定义 forRoot 方法,因为在多个模块中定义的 providers 需要全局可用,也没有为懒加载模块单独准备 providers(注:即本就没有切割 providers 的需求,但你使用 forRoot 强制来切割)。甚至,如果一个被导入模块没有定义任何 providers,那代码写的就更让人迷惑。

Use forRoot/forChild convention only for shared modules with providers that are going to be imported into both eager and lazy module modules

还有一个需要注意的是 forRootforChild 仅仅是方法而已,所以可以传参。比如,@angular/router 包中的 RouterModule,就定义了 方法并传入了额外的参数:

export class RouterModule {  static forRoot(routes: Routes, config?: ExtraOptions)复制代码

传入的 routes 参数是用来注册 ROUTES 标识(token)的:

static forRoot(routes: Routes, config?: ExtraOptions) {  return {    ngModule: RouterModule,    providers: [      {provide: ROUTES, multi: true, useValue: routes}复制代码

传入的第二个可选参数 config 是用来作为配置选项的(注:如配置预加载策略):

static forRoot(routes: Routes, config?: ExtraOptions) {  return {    ngModule: RouterModule,    providers: [      {        provide: PreloadingStrategy,        useExisting: config.preloadingStrategy ?          config.preloadingStrategy :          NoPreloading      }复制代码

正如你所看到的,RouterModule 使用了 forRootforChild 方法来分割 providers,并传入参数来配置相应的 providers

模块缓存

Stackoverflow 上有段时间有位开发者提了个问题,担心如果在非懒加载模块和懒加载模块导入相同的模块,在运行时会导致该模块代码有重复。这个担心可以理解,不过不必担心,因为所有模块加载器会缓存所有加载的模块对象。

当 SystemJS 加载一个模块后会缓存该模块,下次当懒加载模块又再次导入该模块时,SystemJS 模块加载器会从缓存里取出该模块,而不是执行网络请求,这个过程对所有模块适用(注:Angular 内置了 模块加载器)。比如,当你在写 Angular 组件时,从 @angular/core 包中导入 Component 装饰器:

import { Component } from '@angular/core';复制代码

你在程序里多处引用了这个包,但是 SystemJS 并不会每次加载这个包,它只会加载一次并缓存起来。

如果你使用 angular-cli 或者自己配置 Webpack,也同样道理,它只会加载一次并缓存起来,并给它分配一个 ID,其他模块会使用该 ID 来找到该模块,从而可以拿到该模块提供的多种多样的服务。

转载地址:http://wziga.baihongyu.com/

你可能感兴趣的文章
mac 删除文件夹里所有的.svn文件
查看>>
程序制作 代写程序 软件定制 代写Assignment 网络IT支持服务
查看>>
mysql 案例~select引起的性能问题
查看>>
直接读取图层
查看>>
springsecurity 源码解读 之 RememberMeAuthenticationFilter
查看>>
HTML5标准学习 - 编码
查看>>
JS 时间戳转星期几 AND js时间戳判断时间几天前
查看>>
UVa11426 最大公约数之和(正版)
查看>>
SQL练习之求解填字游戏
查看>>
UIApplication
查看>>
12:Web及MySQL服务异常监测案例
查看>>
数据库性能优化之冗余字段的作用
查看>>
DBA_实践指南系列9_Oracle Erp R12应用补丁AutoPatch/AutoControl/AutoConfig(案例)
查看>>
数据库设计三大范式
查看>>
ionic 字体的导入方法
查看>>
内部类详解
查看>>
类加载机制
查看>>
mongodb int型id 自增
查看>>
Java中的4种代码块
查看>>
Ocelot(七)- 入门
查看>>