原文連接: Avoiding common confusions with modules in Angular
Angular Modules 是個至關複雜的話題,甚至 Angular 開發團隊在官網上寫了好幾篇有關 NgModule 的文章教程。這些教程清晰的闡述了 Modules 的大部份內容,可是仍欠缺一些內容,致使不少開發者被誤導。我看到不少開發者因爲不知道 Modules 內部是如何工做的,因此常常理解錯相關概念,使用 Modules API 的姿式也不正確。html
本文將深度解釋 Modules 內部工做原理,爭取幫你消除一些常見的誤解,而這些錯誤我在 StackOverflow 上常常看到有人提問。node
Angular 引入了模塊封裝的概念,這個和 ES 模塊概念很相似(注:ES Modules 概念能夠查看 TypeScript 中文網的 Modules),基本意思是全部聲明類型,包括組件、指令和管道,只能夠在當前模塊內部,被其餘聲明的組件使用。好比,若是我在 App 組件中使用 A 模塊的 a-comp 組件:git
@Component({ selector: 'my-app', template: ` <h1>Hello {{name}}</h1> <a-comp></a-comp> ` }) export class AppComponent { }
Angular 編譯器就會拋出錯誤:github
Template parse errors: 'a-comp' is not a known element
這是由於 App 模塊中沒有申明 a-comp 組件,若是我想要使用這個組件,就不得不導入 A 模塊,就像這樣:json
@NgModule({ imports: [..., AModule] }) export class AppModule { }
上面描述的就是 模塊封裝。不只如此,若是想要 a-comp 組件正常工做,得設置它爲能夠公開訪問,即在 A 模塊的 exports 屬性中導出這個組件:bootstrap
@NgModule({ ... declarations: [AComponent], exports: [AComponent] }) export class AModule { }
同理,對於指令和管道,也得遵照 模塊封裝 的規則:緩存
@NgModule({ ... declarations: [ PublicPipe, PrivatePipe, PublicDirective, PrivateDirective ], exports: [PublicPipe, PublicDirective] }) export class AModule {}
須要注意的是,模塊封裝 原則不適用於在 entryComponents 屬性中註冊的組件,若是你在使用動態視圖時,像 譯 關於 Angular 動態組件你須要知道的 這篇文章中所描述的方式去實例化動態組件,就不須要在 A 模塊的 exports 屬性中去導出 a-comp 組件。固然,還得導入 A 模塊。網絡
大多數初學者會認爲 providers 也有封裝規則,但實際上沒有。在 非懶加載模塊 中申明的任何 provider 均可以在程序內的任何地方被訪問,下文將會詳細解釋緣由。app
初學者最大的一個誤解就是認爲,一個模塊導入其餘模塊後會造成一個模塊層級,認爲該模塊會成爲這些被導入模塊的父模塊,從而造成一個相似模塊樹的層級,固然這麼想也很合理。但實際上,不存在這樣的模塊層級。由於 全部模塊在編譯階段會被合併,因此導入和被導入模塊之間不存在任何層級關係。ide
就像 組件 同樣,Angular 編譯器也會爲根模塊生成一個模塊工廠,根模塊就是你在 main.ts 中,以參數傳入 bootstrapModule() 方法的模塊:
platformBrowserDynamic().bootstrapModule(AppModule);
Angular 編譯器使用 createNgModuleFactory 方法來建立該模塊工廠(注:可參考 L274 -> L60 -> L109 -> L153-L155 -> L50),該方法須要幾個參數(注:爲清晰理解,不翻譯。最新版本不包括第三個依賴參數。):
最後兩點解釋了爲什麼 providers 和 entry components 沒有模塊封裝規則,由於編譯結束後沒有多個模塊,而僅僅只有一個合併後的模塊。而且在編譯階段,編譯器不知道你將如何使用 providers 和動態組件,因此編譯器去控制封裝。可是在編譯階段的組件模板解析過程時,編譯器知道你是如何使用組件、指令和管道的,因此編譯器能控制它們的私有申明。(注:providers 和 entry components 是整個程序中的動態部分 dynamic content,Angular 編譯器不知道它會被如何使用,可是模板中寫的組件、指令和管道,是靜態部分 static content,Angular 編譯器在編譯的時候知道它是如何被使用的。這點對理解 Angular 內部工做原理仍是比較重要的。)
讓咱們看一個生成模塊工廠的示例,假設你有 A 和 B 兩個模塊,而且每個模塊都定義了一個 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,並導入 A 和 B 模塊:
@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_<BComponent>, ComponentFactory_<AComponent>, ComponentFactory_<AppComponent> ]) // 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', []) ]);
從上面代碼知道,全部模塊的 providers 和 entry components 都將會被合併,並傳給 moduleDef() 方法,因此不管導入多少個模塊,編譯器只會合併模塊,並只生成一個模塊工廠。該模塊工廠會使用模塊注入器來生成合並模塊對象(注:查看 L232),然而因爲只有一個合併模塊,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 中導入 AModule 和 BModule:
@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,和兩個模塊 AModule 和 BModule, AModule 提供 AService 、 {provide:'a', value:'a'} 和 {provide:'b', value:'b'} 服務,而 BModule 提供 BService 和 {provide: 'b', useValue: 'c'}。 AModule 和 BModule 按照前後順序導入根模塊 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: ` <p>{{name}}</p> <!--<a-comp></a-comp>--> ` }) 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.js 和 module.factory.js。
先看下 module.js。 AppModule 類會被編譯爲以下代碼,發現咱們在 @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 會覆蓋其餘模塊的 providers, BModule 中的 providers 會覆蓋 AModule 的 providers,由於 BModule 在 AModule 以後導入,能夠交換導入順序看看發生什麼。其中,ɵcmf 是 createNgModuleFactory,ɵmod 是 moduleDef,ɵmpd 是 moduleProvideDef, moduleProvideDef 第一個參數是 enum NodeFlags 節點類型,用來表示當前節點是什麼類型,好比 i0.ɵmpd(256, "a", "a", []) 中的 256 表示 TypeValueProvider 是個值類型。
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<any>) => { ... const module = factory.create(parentInjector); ... })); } private loadModuleFactory(loadChildren) { ... return this.loader.load(loadChildren) } }
查看這行代碼:
const module = factory.create(parentInjector);
傳入父注入器來建立懶加載模塊新對象。
查看官網是如何介紹的(注:不翻譯):
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 {...}
每個導入的模塊(如 CarouselModule,CheckboxModule 等等)再也不定義任何 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<any> 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
還有一個須要注意的是 forRoot 和 forChild 僅僅是方法而已,因此能夠傳參。好比,@angular/router 包中的 RouterModule,就定義了 forRoot 方法並傳入了額外的參數:
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 使用了 forRoot 和 forChild 方法來分割 providers,並傳入參數來配置相應的 providers。
在 Stackoverflow 上有段時間有位開發者提了個問題,擔憂若是在非懶加載模塊和懶加載模塊導入相同的模塊,在運行時會致使該模塊代碼有重複。這個擔憂能夠理解,不過沒必要擔憂,由於全部模塊加載器會緩存全部加載的模塊對象。
當 SystemJS 加載一個模塊後會緩存該模塊,下次當懶加載模塊又再次導入該模塊時,SystemJS 模塊加載器會從緩存裏取出該模塊,而不是執行網絡請求,這個過程對全部模塊適用(注:Angular 內置了 SystemJsNgModuleLoader 模塊加載器)。好比,當你在寫 Angular 組件時,從 @angular/core 包中導入 Component 裝飾器:
import { Component } from '@angular/core';
你在程序裏多處引用了這個包,可是 SystemJS 並不會每次加載這個包,它只會加載一次並緩存起來。
若是你使用 angular-cli 或者本身配置 Webpack,也一樣道理,它只會加載一次並緩存起來,並給它分配一個 ID,其餘模塊會使用該 ID 來找到該模塊,從而能夠拿到該模塊提供的多種多樣的服務。