Angular Modules 是個至關複雜的話題,甚至 Angular 開發團隊在官網上寫了好幾篇有關 NgModule 的文章教程。這些教程清晰的闡述了 Modules 的大部份內容,可是仍欠缺一些內容,致使不少開發者被誤導。我看到不少開發者因爲不知道 Modules 內部是如何工做的,因此常常理解錯相關概念,使用 Modules API 的姿式也不正確。node
本文將深度解釋 Modules 內部工做原理,爭取幫你消除一些常見的誤解,而這些錯誤我在 StackOverflow 上常常看到有人提問。git
Angular 引入了模塊封裝的概念,這個和 ES 模塊概念很相似(注:ES Modules 概念能夠查看 TypeScript 中文網的 Modules),基本意思是全部聲明類型,包括組件、指令和管道,只能夠在當前模塊內部,被其餘聲明的組件使用。好比,若是我在 App 組件中使用 A 模塊的 a-comp 組件:github
@Component({
selector: 'my-app',
template: ` <h1>Hello {{name}}</h1> <a-comp></a-comp> `
})
export class AppComponent { }
複製代碼
Angular 編譯器就會拋出錯誤:json
Template parse errors: 'a-comp' is not a known elementbootstrap
這是由於 App 模塊中沒有申明 a-comp 組件,若是我想要使用這個組件,就不得不導入 A 模塊,就像這樣:緩存
@NgModule({
imports: [..., AModule]
})
export class AppModule { }
複製代碼
上面描述的就是 模塊封裝。不只如此,若是想要 a-comp 組件正常工做,得設置它爲能夠公開訪問,即在 A 模塊的 exports 屬性中導出這個組件:bash
@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 模塊。app
大多數初學者會認爲 providers 也有封裝規則,但實際上沒有。在 非懶加載模塊 中申明的任何 provider 均可以在程序內的任何地方被訪問,下文將會詳細解釋緣由。
初學者最大的一個誤解就是認爲,一個模塊導入其餘模塊後會造成一個模塊層級,認爲該模塊會成爲這些被導入模塊的父模塊,從而造成一個相似模塊樹的層級,固然這麼想也很合理。但實際上,不存在這樣的模塊層級。由於 全部模塊在編譯階段會被合併,因此導入和被導入模塊之間不存在任何層級關係。
就像 組件 同樣,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 來找到該模塊,從而能夠拿到該模塊提供的多種多樣的服務。