NgModules
可以幫你把應用組織成一些緊密相關的代碼塊。
這里回答的是開發(fā)者常問起的關于 NgModule
的設計與實現(xiàn)問題。
把可聲明的類(組件、指令和管道)添加到 declarations
列表中。
這些類只能在應用程序的一個并且只有一個模塊中聲明。 只有當它們從屬于某個模塊時,才能把在此模塊中聲明它們。
聲明的就是組件、指令和管道這些可以被加到模塊的 declarations
列表中的類。它們也是所有能被加到 declarations
中的類。
只有可聲明的類才能加到模塊的 declarations
列表中。
不要聲明:
@NgModule
)還是第三方模塊。@angular/forms
的 FORMS_DIRECTIVES
,因為 FormsModule
已經(jīng)聲明過它們了。
AppComponent
經(jīng)常被同時列在 declarations
和 bootstrap
中。 另外你還可能看到 HeroComponent
被同時列在 declarations
、exports
和 entryComponent
中。
這看起來是多余的,不過這些函數(shù)具有不同的功能,從它出現(xiàn)在一個列表中無法推斷出它也應該在另一個列表中。
AppComponent
可能被聲明在此模塊中,但可能不是引導組件。AppComponent
可能在此模塊中引導,但可能是由另一個特性模塊聲明的。
這個錯誤通常意味著你或者忘了聲明指令“x”
,或者你沒有導入“x”
所屬的模塊。
如果
“x”
其實不是屬性,或者是組件的私有屬性(比如它不帶@Input
或@Output
裝飾器),那么你也同樣會遇到這個錯誤。
導入你需要在當前模塊的組件模板中使用的那些公開的(被導出的)可聲明類。
這意味著要從 @angular/common
中導入 CommonModule
才能訪問 Angular 的內(nèi)置指令,比如 NgIf
和 NgFor
。 你可以直接導入它或者從重新導出過該模塊的其它模塊中導入它。
如果你的組件有 [(ngModel)]
雙向綁定表達式,就要從 @angular/forms
中導入 FormsModule
。
如果當前模塊中的組件包含了共享模塊和特性模塊中的組件、指令和管道,就導入這些模塊。
只能在根模塊 AppModule
中導入 BrowserModule
。
幾乎所有要在瀏覽器中使用的應用的根模塊(AppModule)都應該從 @angular/platform-browser
中導入 BrowserModule
。
BrowserModule
提供了啟動和運行瀏覽器應用的那些基本的服務提供者。
BrowserModule
還從 @angular/common
中重新導出了 CommonModule
,這意味著 AppModule
中的組件也同樣可以訪問那些每個應用都需要的 Angular 指令,如 NgIf
和 NgFor
。
在其它任何模塊中都不要導入BrowserModule
。 特性模塊和惰性加載模塊應該改成導入 CommonModule
。 它們需要通用的指令。它們不需要重新初始化全應用級的提供者。
沒有任何問題。當三個模塊全都導入模塊'A'
時,Angular 只會首次遇到時加載一次模塊'A'
,之后就不會這么做了。
無論 A
出現(xiàn)在所導入模塊的哪個層級,都會如此。 如果模塊'B'
導入模塊'A'
、模塊'C'
導入模塊'B'
,模塊'D'
導入 [C, B, A]
,那么'D'
會觸發(fā)模塊'C'
的加載,'C'
會觸發(fā)'B'
的加載,而'B'
會加載'A'
。 當 Angular 在'D'
中想要獲取'B'
和'A'
時,這兩個模塊已經(jīng)被緩存過了,可以立即使用。
Angular 不允許模塊之間出現(xiàn)循環(huán)依賴,所以不要讓模塊'A'
導入模塊'B'
,而模塊'B'
又導入模塊'A'
。
特性模塊中導入 CommonModule
可以讓它能用在任何目標平臺上,不僅是瀏覽器。那些跨平臺庫的作者應該喜歡這種方式的。
導出那些其它模塊希望在自己的模板中引用的可聲明類。這些也是你的公共類。 如果你不導出某個類,它就是私有的,只對當前模塊中聲明的其它組件可見。
你可以導出任何可聲明類(組件、指令和管道),而不用管它是聲明在當前模塊中還是某個導入的模塊中。
你可以重新導出整個導入過的模塊,這將導致重新導出它們導出的所有類。重新導出的模塊甚至不用先導入。
不要導出:
HttpClientModule
,因為它不導出任何東西。 它唯一的用途是一起把 http 的那些服務提供者添加到應用中。毫無疑問!
模塊是從其它模塊中選取類并把它們重新導出成統(tǒng)一、便利的新模塊的最佳方式。
模塊可以重新導出其它模塊,這會導致重新導出它們導出的所有類。 Angular 自己的 BrowserModule
就重新導出了一組模塊,例如:
exports: [CommonModule, ApplicationModule]
模塊還能導出一個組合,它可以包含自己的聲明、某些導入的類以及導入的模塊。
不要費心去導出純服務類。 純服務類的模塊不會導出任何可供其它模塊使用的可聲明類。 例如,不用重新導出 HttpClientModule
,因為它沒有導出任何東西。 它唯一的用途是把那些 http
服務提供者一起添加到應用中。
靜態(tài)方法 forRoot()
是一個約定,它可以讓開發(fā)人員更輕松的配置模塊的想要單例使用的服務及其提供者。RouterModule.forRoot()
就是一個很好的例子。
應用把一個 Routes
對象傳給 RouterModule.forRoot()
,為的就是使用路由配置全應用級的 Router
服務。 RouterModule.forRoot()
返回一個ModuleWithProviders
對象。 你把這個結(jié)果添加到根模塊 AppModule
的 imports
列表中。
只能在應用的根模塊 AppModule
中調(diào)用并導入 forRoot()
的結(jié)果。 在其它模塊,特別是惰性加載模塊中,不要導入它。 要了解關于 forRoot()
的更多信息,參見單例服務一章的 the forRoot()
模式部分。
對于服務來說,除了可以使用 forRoot()
外,更好的方式是在該服務的 @Injectable()
裝飾器中指定 providedIn
: 'root'
,它讓該服務自動在全應用級可用,這樣它也就默認是單例的。
RouterModule
也提供了靜態(tài)方法 forChild()
,用于配置惰性加載模塊的路由。
forRoot()
和 forChild()
都是約定俗成的方法名,它們分別用于在根模塊和特性模塊中配置服務。
當你寫類似的需要可配置的服務提供者時,請遵循這個約定。
列在引導模塊的 @NgModule.providers
中的服務提供者具有全應用級作用域。 往 NgModule.providers
中添加服務提供者將導致該服務被發(fā)布到整個應用中。
當你導入一個模塊時,Angular 就會把該模塊的服務提供者(也就是它的 providers
列表中的內(nèi)容)加入該應用的根注入器中。
這會讓該提供者對應用中所有知道該提供者令牌(token
)的類都可見。
通過 NgModule
導入來實現(xiàn)可擴展性是 NgModule
體系的主要設計目標。 把 NgModule
的提供者并入應用程序的注入器可以讓庫模塊使用新的服務來強化應用程序變得更容易。 只要添加一次 HttpClientModule
,那么應用中的每個組件就都可以發(fā)起 Http
請求了。
不過,如果你期望模塊的服務只對那個特性模塊內(nèi)部聲明的組件可見,那么這可能會帶來一些不受歡迎的意外。 如果 HeroModule
提供了一個 HeroService
,并且根模塊 AppModule
導入了 HeroModule
,那么任何知道 HeroService
類型的類都可能注入該服務,而不僅是在 HeroModule
中聲明的那些類。
要限制對某個服務的訪問,可以考慮惰性加載提供該服務的 NgModule
。
和啟動時就加載的模塊中的提供者不同,惰性加載模塊中的提供者是局限于模塊的。
當 Angular 路由器惰性加載一個模塊時,它創(chuàng)建了一個新的運行環(huán)境。 那個環(huán)境擁有自己的注入器,它是應用注入器的直屬子級。
路由器把該惰性加載模塊的提供者和它導入的模塊的提供者添加到這個子注入器中。
這些提供者不會被擁有相同令牌的應用級別提供者的變化所影響。 當路由器在惰性加載環(huán)境中創(chuàng)建組件時,Angular 優(yōu)先使用惰性加載模塊中的服務實例,而不是來自應用的根注入器的。
當同時加載了兩個導入的模塊,它們都列出了使用同一個令牌的提供者時,后導入的模塊會“獲勝”,這是因為這兩個提供者都被添加到了同一個注入器中。
當 Angular 嘗試根據(jù)令牌注入服務時,它使用第二個提供者來創(chuàng)建并交付服務實例。
每個注入了該服務的類獲得的都是由第二個提供者創(chuàng)建的實例。 即使是聲明在第一個模塊中的類,它取得的實例也是來自第二個提供者的。
如果模塊 A
提供了一個使用令牌'X'
的服務,并且導入的模塊 B
也用令牌'X'
提供了一個服務,那么模塊 A
中定義的服務“獲勝”了。
由根 AppModule
提供的服務相對于所導入模塊中提供的服務有優(yōu)先權。換句話說:AppModule
總會獲勝。
如果一個模塊在應用程序啟動時就加載,它的 @NgModule.providers
具有全應用級作用域。 它們也可用于整個應用的注入中。
導入的提供者很容易被由其它導入模塊中的提供者替換掉。 這雖然是故意這樣設計的,但是也可能引起意料之外的結(jié)果。
作為一個通用的規(guī)則,應該只導入一次帶提供者的模塊,最好在應用的根模塊中。 那里也是配置、包裝和改寫這些服務的最佳位置。
假設模塊需要一個定制過的 HttpBackend
,它為所有的 Http
請求添加一個特別的請求頭。 如果應用中其它地方的另一個模塊也定制了 HttpBackend
或僅僅導入了 HttpClientModule
,它就會改寫當前模塊的 HttpBackend
提供者,丟掉了這個特別的請求頭。 這樣服務器就會拒絕來自該模塊的請求。
要消除這個問題,就只能在應用的根模塊 AppModule
中導入 HttpClientModule
。
如果你必須防范這種“提供者腐化”現(xiàn)象,那就不要依賴于“啟動時加載”模塊的 providers
。
只要可能,就讓模塊惰性加載。 Angular 給了惰性加載模塊自己的子注入器。 該模塊中的提供者只對由該注入器創(chuàng)建的組件樹可見。
如果你必須在應用程序啟動時主動加載該模塊,就改成在組件中提供該服務。
繼續(xù)看這個例子,假設某個模塊的組件真的需要一個私有的、自定義的 HttpBackend
。
那就創(chuàng)建一個“頂層組件”來扮演該模塊中所有組件的根。 把這個自定義的 HttpBackend
提供者添加到這個頂層組件的 providers
列表中,而不是該模塊的 providers
中。 回憶一下,Angular 會為每個組件實例創(chuàng)建一個子注入器,并使用組件自己的 providers
來配置這個注入器。
當該組件的子組件想要一個 HttpBackend
服務時,Angular 會提供一個局部的 HttpBackend
服務,而不是應用的根注入器創(chuàng)建的那個。 子組件將正確發(fā)起 http 請求,而不管其它模塊對 HttpBackend
做了什么。
確保把模塊中的組件都創(chuàng)建成這個頂層組件的子組件。
你可以把這些子組件都嵌在頂層組件的模板中。或者,給頂層組件一個 <router-outlet>
,讓它作為路由的宿主。 定義子路由,并讓路由器把模塊中的組件加載進該路由出口(outlet
)中。
雖然通過在惰性加載模塊中或組件中提供某個服務來限制它的訪問都是可行的方式,但在組件中提供服務可能導致這些服務出現(xiàn)多個實例。因此,應該優(yōu)先使用惰性加載的方式。
通過在服務的 @Injectable()
裝飾器中(例如服務)指定 providedIn: 'root'
來定義全應用級提供者,或者 InjectionToken
的構(gòu)造器(例如提供令牌的地方),都可以定義全應用級提供者。 通過這種方式創(chuàng)建的服務提供者會自動在整個應用中可用,而不用把它列在任何模塊中。
如果某個提供者不能用這種方式配置(可能因為它沒有有意義的默認值),那就在根模塊 AppModule
中注冊這些全應用級服務,而不是在 AppComponent
中。
惰性加載模塊及其組件可以注入 AppModule
中的服務,卻不能注入 AppComponent
中的。
只有當該服務必須對 AppComponent
組件樹之外的組件不可見時,才應該把服務注冊進 AppComponent
的 providers
中。 這是一個非常罕見的異常用法。
更一般地說,優(yōu)先把提供者注冊進模塊中,而不是組件中。
Angular 把所有啟動期模塊的提供者都注冊進了應用的根注入器中。 這些服務是由根注入器中的提供者創(chuàng)建的,并且在整個應用中都可用。 它們具有應用級作用域。
某些服務(比如 Router
)只有當注冊進應用的根注入器時才能正常工作。
相反,Angular 使用 AppComponent
自己的注入器注冊了 AppComponent
的提供者。 AppComponent
服務只在該組件及其子組件樹中才能使用。 它們具有組件級作用域。
AppComponent
的注入器是根注入器的子級,注入器層次中的下一級。 這對于沒有路由器的應用來說幾乎是整個應用了。 但對那些帶路由的應用,路由操作位于頂層,那里不存在 AppComponent
服務。這意味著惰性加載模塊不能使用它們。
提供者應該使用 @Injectable
語法進行配置。只要可能,就應該把它們在應用的根注入器中提供(providedIn: 'root'
)。 如果它們只被惰性加載的上下文中使用,那么這種方式配置的服務就是惰性加載的。
如果要由消費方來決定是否把它作為全應用級提供者,那么就要在模塊中(@NgModule.providers
)注冊提供者,而不是組件中(@Component.providers
)。
當你必須把服務實例的范圍限制到某個組件及其子組件樹時,就把提供者注冊到該組件中。 指令的提供者也同樣照此處理。
例如,如果英雄編輯組件需要自己私有的緩存英雄服務實例,那就應該把 HeroService
注冊進 HeroEditorComponent
中。 這樣,每個新的 HeroEditorComponent
的實例都會得到一份自己的緩存服務實例。 編輯器的改動只會作用于它自己的服務,而不會影響到應用中其它地方的英雄實例。
總是在根模塊 AppModule
中注冊全應用級服務,而不要在根組件 AppComponent
中。
急性加載的場景
當急性加載的模塊提供了服務時,比如 UserService
,該服務是在全應用級可用的。如果根模塊提供了 UserService
,并導入了另一個也提供了同一個 UserService
的模塊,Angular 就會把它們中的一個注冊進應用的根注入器中(參見如果兩次導入了同一個模塊會怎樣?)。
然后,當某些組件注入 UserService
時,Angular 就會發(fā)現(xiàn)它已經(jīng)在應用的根注入器中了,并交付這個全應用級的單例服務。這樣不會出現(xiàn)問題。
惰性加載場景
現(xiàn)在,考慮一個惰性加載的模塊,它也提供了一個名叫 UserService
的服務。
當路由器準備惰性加載 HeroModule
的時候,它會創(chuàng)建一個子注入器,并且把 UserService
的提供者注冊到那個子注入器中。子注入器和根注入器是不同的。
當 Angular 創(chuàng)建一個惰性加載的 HeroComponent
時,它必須注入一個 UserService
。 這次,它會從惰性加載模塊的子注入器中查找 UserService
的提供者,并用它創(chuàng)建一個 UserService
的新實例。 這個 UserService
實例與 Angular 在主動加載的組件中注入的那個全應用級單例對象截然不同。
這個場景導致你的應用每次都創(chuàng)建一個新的服務實例,而不是使用單例的服務。
Angular 會把 @NgModule.providers
中的提供者添加到應用的根注入器中…… 除非該模塊是惰性加載的,這種情況下,Angular 會創(chuàng)建一子注入器,并且把該模塊的提供者添加到這個子注入器中。
這意味著模塊的行為將取決于它是在應用啟動期間加載的還是后來惰性加載的。如果疏忽了這一點,可能導致嚴重后果。
為什么 Angular 不能像主動加載模塊那樣把惰性加載模塊的提供者也添加到應用程序的根注入器中呢?為什么會出現(xiàn)這種不一致?
歸根結(jié)底,這來自于 Angular 依賴注入系統(tǒng)的一個基本特征: 在注入器還沒有被第一次使用之前,可以不斷為其添加提供者。 一旦注入器已經(jīng)創(chuàng)建和開始交付服務,它的提供者列表就被凍結(jié)了,不再接受新的提供者。
當應用啟動時,Angular 會首先使用所有主動加載模塊中的提供者來配置根注入器,這發(fā)生在它創(chuàng)建第一個組件以及注入任何服務之前。 一旦應用開始工作,應用的根注入器就不再接受新的提供者了。
之后,應用邏輯開始惰性加載某個模塊。 Angular 必須把這個惰性加載模塊中的提供者添加到某個注入器中。 但是它無法將它們添加到應用的根注入器中,因為根注入器已經(jīng)不再接受新的提供者了。 于是,Angular 在惰性加載模塊的上下文中創(chuàng)建了一個新的子注入器。
某些模塊及其服務只能被根模塊 AppModule
加載一次。 在惰性加載模塊中再次導入這個模塊會導致錯誤的行為,這個錯誤可能非常難于檢測和診斷。
為了防范這種風險,可以寫一個構(gòu)造函數(shù),它會嘗試從應用的根注入器中注入該模塊或服務。如果這種注入成功了,那就說明這個類是被第二次加載的,你就可以拋出一個錯誤,或者采取其它挽救措施。
某些 NgModule
(例如 BrowserModule
)就實現(xiàn)了那樣一個守衛(wèi)。 下面是一個名叫 GreetingModule
的 NgModule
的 自定義構(gòu)造函數(shù)。
Path:"src/app/greeting/greeting.module.ts (Constructor)" 。
constructor (@Optional() @SkipSelf() parentModule?: GreetingModule) {
if (parentModule) {
throw new Error(
'GreetingModule is already loaded. Import it in the AppModule only');
}
}
Angular 根據(jù)組件類型命令式加載的組件是入口組件.
而通過組件選擇器聲明式加載的組件則不是入口組件。
Angular 會聲明式的加載組件,它使用組件的選擇器在模板中定位元素。 然后,Angular 會創(chuàng)建該組件的 HTML 表示,并把它插入 DOM 中所選元素的內(nèi)部。它們不是入口組件。
而用于引導的根 AppComponent
則是一個入口組件。 雖然它的選擇器匹配了 "index.html" 中的一個元素,但是 "index.html" 并不是組件模板,而且 AppComponent
選擇器也不會在任何組件模板中出現(xiàn)。
在路由定義中用到的組件也同樣是入口組件。 路由定義根據(jù)類型來引用組件。 路由器會忽略路由組件的選擇器(即使它有選擇器),并且把該組件動態(tài)加載到 RouterOutlet
中。
引導組件是入口組件的一種。 它是被 Angular 的引導(應用啟動)過程加載到 DOM 中的入口組件。 其它入口組件則是被其它方式動態(tài)加載的,比如被路由器加載。
@NgModule.bootstrap
屬性告訴編譯器這是一個入口組件,同時它應該生成一些代碼來用該組件引導此應用。
不需要把組件同時列在 bootstrap
和 entryComponent
列表中 —— 雖然這樣做也沒壞處。
大多數(shù)應用開發(fā)者都不需要把組件添加到 entryComponents
中。
Angular 會自動把恰當?shù)慕M件添加到入口組件中。 列在 @NgModule.bootstrap
中的組件會自動加入。 由路由配置引用到的組件會被自動加入。 用這兩種機制添加的組件在入口組件中占了絕大多數(shù)。
如果你的應用要用其它手段來根據(jù)類型引導或動態(tài)加載組件,那就得把它顯式添加到 entryComponents
中。
雖然把組件加到這個列表中也沒什么壞處,不過最好還是只添加真正的入口組件。 不要添加那些被其它組件的模板引用過的組件。
原因在于搖樹優(yōu)化。對于產(chǎn)品化應用,你會希望加載盡可能小而快的代碼。 代碼中應該僅僅包括那些實際用到的類。 它應該排除那些從未用過的組件,無論該組件是否被聲明過。
事實上,大多數(shù)庫中聲明和導出的組件你都用不到。 如果你從未引用它們,那么搖樹優(yōu)化器就會從最終的代碼包中把這些組件砍掉。
如果Angular 編譯器為每個聲明的組件都生成了代碼,那么搖樹優(yōu)化器的作用就沒有了。
所以,編譯器轉(zhuǎn)而采用一種遞歸策略,它只為你用到的那些組件生成代碼。
編譯器從入口組件開始工作,為它在入口組件的模板中找到的那些組件生成代碼,然后又為在這些組件中的模板中發(fā)現(xiàn)的組件生成代碼,以此類推。 當這個過程結(jié)束時,它就已經(jīng)為每個入口組件以及從入口組件可以抵達的每個組件生成了代碼。
如果該組件不是入口組件或者沒有在任何模板中發(fā)現(xiàn)過,編譯器就會忽略它。
每個應用都不一樣。根據(jù)不同程度的經(jīng)驗,開發(fā)者會做出不同的選擇。下列建議和指導原則廣受歡迎。
為那些可能會在應用中到處使用的組件、指令和管道創(chuàng)建 SharedModule
。 這種模塊應該只包含 declarations
,并且應該導出幾乎所有 declarations
里面的聲明。
SharedModule
可以重新導出其它小部件模塊,比如 CommonModule
、FormsModule
和提供你廣泛使用的 UI 控件的那些模塊。
SharedModule
不應該帶有 providers
,原因在前面解釋過了。 它的導入或重新導出的模塊中也不應該有 providers
。 如果你要違背這條指導原則,請務必想清楚你在做什么,并要有充分的理由。
在任何特性模塊中(無論是你在應用啟動時主動加載的模塊還是之后惰性加載的模塊),你都可以隨意導入這個 SharedModule
。
特性模塊是你圍繞特定的應用業(yè)務領域創(chuàng)建的模塊,比如用戶工作流、小工具集等。它們包含指定的特性,并為你的應用提供支持,比如路由、服務、窗口部件等。 要對你的應用中可能會有哪些特性模塊有個概念,考慮如果你要把與特定功能(比如搜索)有關的文件放進一個目錄下,該目錄的內(nèi)容就可能是一個名叫 SearchModule
的特性模塊。 它將會包含構(gòu)成搜索功能的全部組件、路由和模板。
在 Angular 應用中,NgModule
會和 JavaScript 的模塊一起工作。
在現(xiàn)代 JavaScript 中,每個文件都是模塊(參見模塊)。 在每個文件中,你要寫一個 export
語句將模塊的一部分公開。
Angular 模塊是一個帶有 @NgModule
裝飾器的類,而 JavaScript 模塊則沒有。 Angular 的 NgModule
有自己的 imports
和 exports
來達到類似的目的。
你可以導入其它 NgModules
,以便在當前模塊的組件模板中使用它們導出的類。 你可以導出當前 NgModules
中的類,以便其它 NgModules
可以導入它們,并用在自己的組件模板中。
Angular 編譯器在組件模板內(nèi)查找其它組件、指令和管道。一旦找到了,那就是一個“模板引用”。
Angular 編譯器通過在一個模板的 HTML 中匹配組件或指令的選擇器(selector
),來查找組件或指令。
編譯器通過分析模板 HTML 中的管道語法中是否出現(xiàn)了特定的管道名來查找對應的管道。
Angular 只查詢兩種組件、指令或管道: 1)那些在當前模塊中聲明過的,以及 2)那些被當前模塊導入的模塊所導出的。
Angular 編譯器會把你所編寫的應用代碼轉(zhuǎn)換成高性能的 JavaScript 代碼。 在編譯過程中,@NgModule
的元數(shù)據(jù)扮演了很重要的角色。
你寫的代碼是無法直接執(zhí)行的。 比如組件。 組件有一個模板,其中包含了自定義元素、屬性型指令、Angular 綁定聲明和一些顯然不屬于原生 HTML 的古怪語法。
Angular 編譯器讀取模板的 HTML,把它和相應的組件類代碼組合在一起,并產(chǎn)出組件工廠。
組件工廠為組件創(chuàng)建純粹的、100% JavaScript 的表示形式,它包含了 @Component
元數(shù)據(jù)中描述的一切:HTML、綁定指令、附屬的樣式等……
由于指令和管道都出現(xiàn)在組件模板中,*Angular
編譯器**也同樣會把它們組合進編譯后的組件代碼中。
@NgModule
元數(shù)據(jù)告訴 Angular 編譯器要為當前模塊編譯哪些組件,以及如何把當前模塊和其它模塊鏈接起來。
更多建議: