Angular 6+依賴注入使用指南:providedIn與providers對比
本文由達觀數據研究院根據《Total Guide To Angular 6+ Dependency Injection — providedIn vs providers》編譯,如有不當,還請指正。
Angular 6為我們提供了更好的語法——provideIn,用於將服務註冊到Angular依賴注入機制中。
然而,新語法帶來了非常多使用上的困惑,在GitHub評論,Slack和Stack Overflow上看到一些開發者經常混淆。所以現在,讓我們把這一切都說清楚。
接下來我們將會學習什麼?
- 依賴注入回顧(可選)
- 使用舊語法進行依賴注入——
providers: []
- 使用新語法進行依賴注入——
providedIn: 'root' | SomeModule
-
providedIn
的使用場景 - 在項目中如何使用新語法的最佳實踐
- 總結
依賴注入
讓我們快速回顧一下依賴注入是什麼,如果感覺簡單,你可以跳過這一小節。
依賴注入(DI)是一種創建依賴其他對象的方法。在創建一個新的對象實例時,依賴注入系統將會提供依賴對象(稱為依賴關係) - Angular Docs
我們的組件和服務都是類,每個類都有一個名為constructor的特殊函數,當我們想要在我們的應用程序中創建該類的對象(實例)時調用它。
在我們的服務中,我們都看到過類似於constructor(private http: HttpClient)
這樣的代碼。假如沒有Angular DI機制,我們必須手動提供HttpClient來創建我們自己的服務。
我們的代碼會像這樣: const myService = new MyService(httpClient)
;但是,我們還需要獲得httpClient
對象。
於是,我需要再實例一個HttpClient: const httpClient = new HttpClient(httpHandler)
;但httpHandler
又從哪來?如果這樣創建下去,到底什麼時候是個頭。而且,這個過程相當繁瑣,而且很容易出錯。
幸好,Angular 的DI機制自動地幫我們完成了上述的所有操作,我們所要做的只是在組件的構造函數中指定依賴項,組件將會很輕鬆地就能用到這些依賴。可天下沒有免費的午餐...
使用舊語法進行依賴注入
為了讓工程實踐做的更好,Angular必須了解我們想要注入到組件和服務中的每一個實體。
在Angular 6發布以前,唯一的方法是在providers: []
中指定服務,如下:

根據具體使用場景, providers: []
將有三種不同的用法:
1、在預加載的模塊的@NgModule
裝飾器中指定providers: []
2、在懶加載的模塊的@NgModule
裝飾器中指定providers: []
3、在@Component
和@Directive
裝飾器中指定providers: []
在預加載模塊中使用providers: []
在這種情況下,服務將是全局單例的。即使它被多個模塊的providers: []
重複申明,它也不會重新創建實例。注入器只會創建一個實例,這是因為它們最終都會註冊到根級註入器。
在懶加載模塊中使用providers: []
在應用程序運行初始化後一段時間,懶加載模塊中提供的服務實例才會在子注入器(懶加載模塊)上創建。如果在預加載模塊中註入這些服務,將會報No provider for MyService!
錯誤。
在@Component
和@Directive
中使用providers: []
服務是按組件實例化的,並且可以在組件及其子樹中的所有子組件中訪問。在這種情況下,服務不是單例的,每次我們在另一個組件的模板中使用組件時,我們都會獲得所提供服務的新實例。這也意味著服務實例將與組件一起銷毀......

上面圖中, RandomService
在RandomComponent
中被註冊,因此,每當我們在模板中使用<random> </ random>
組件時,我們將得到不同的隨機數。
如果在模塊級別提供RandomService
並且將被作為單例提供,則不會出現這種情況。在這種情況下, <random> </ random>
組件的每次使用都會顯示相同的隨機數,因為該數字是在服務實例化期間生成的。
使用新語法進行依賴注入
隨著Angular 6的出現,我們可以使用全新的語法在我們的應用程序中建立依賴項,官方名稱是“Tree-shakable providers”,我們通過使用@Injectable
裝飾器的新增的provideIn
屬性來使用它。
我們可以將provideIn視為以反向方式指定依賴關係。現在不是模塊申明需要哪些服務,而是服務本身宣布它應該提供給哪些模塊使用
申明的模塊可以是root
或其他任何可用模塊。另外, root
實際上是AppModule
的別名,這是一個很好的語法糖,我們因此不需要額外導入AppModule
。

新語法非常簡單,現在讓我們實踐一下,來探索在應用程序開發過程中可能遇到的一些有趣場景......
使用providedIn: 'root'
在大多數情況下,這是對我們有用的最常見的解決方案。此解決方案的主要好處是,只有真正“使用”這些服務時才會打包服務代碼。 “使用”代表注入某些組件或其他服務。
另一方面, providedIn: 'root'
在代碼可複用方面為開發人員帶來了巨大的積極影響。
在 `providedIn` 出现之前,需要在主模块的 `providers: []` 中注入所有公共服务。然后,组件需要导入该模块,这将导致所有(可能的大量)的服务导入进该组件,即使我们只想使用其中一个服务。
現在, providedIn: 'root'
解決了這個問題,我們不需要在模塊中導入這些服務,我們要做的僅僅是使用它們。
懶加載providedIn: 'root'
解決方案
如果我們在懶加載中使用providedIn: 'root'
來實現服務會發生什麼?
從技術上講, 'root'
代表AppModule
,但Angular足夠聰明,如果該服務只是在惰性組件/服務中註入,那麼它只會綁定在延遲加載的bundle中。
如果我們又額外將服務注入到其他正常加載的模塊中,那麼該服務會自動綁定到mian
的bundle中。
簡單來講:
1、如果服務僅被注入到懶加載模塊,它將捆綁在懶加載包中
2、如果服務又被注入到正常模塊中,它將捆綁在主包中這種行為的問題在於,在擁有大量模塊和數百項服務的大型應用程序中,它可能變得非常不可預測。
幸運的是,有一種方法可以防止這種情況的發生,我們將在下面的章節中探討如何加強模塊的邊界。
使用providedIn: EagerlyImportedModule
這個解決方案通常沒有意義,我們應該堅持使用provideIn:'root'
。
它可用於防止應用程序的其餘部分注入服務而無需導入相應的模塊,但這其實並不是必需的。
附註-延遲加載模塊的多重好處
Angular最大的優點之一是我們可以非常容易的將應用程序分成完全獨立的邏輯塊,這有以下好處…
1、更小的初始化代碼,這意味著更快的加載和啟動時間
2、懶惰加載的模塊是真正隔離的。主機應用程序應該引用它們的唯一一點是某些路由的loadChildren
屬性。
這意味著,如果使用正確,可以將整個模塊刪除或外部化為獨立的應用程序/庫。可能有數百個組件和服務的模塊可以在不影響應用程序其餘部分的情況下隨意移動,這是非常令人驚奇的!
這種隔離的另一個巨大好處是,對懶惰模塊的邏輯進行更改永遠不會導致應用程序的其他部分出錯。
使用providedIn: LazyLoadedModule

這個解決方案非常棒,因為它可以幫助我們防止在所需模塊之外使用我們的服務。在開發大型應用程序時,保持依賴關係圖是非常有必要的,因為無約束的無處不在的注入可能會導致無法解決的巨大混亂!
不幸的是,有一個小問題……循環依賴

幸運的是,我們可以通過創建一個LazyServiceModule
來避免這個問題,它將是LazyModule
的一個子模塊,並將被用作我們想要提供的所有懶加載服務的“錨”。如下圖所示:

雖然有點不方便,但我們只需增加一個模塊,這種方法結合了兩者的優點:1. 它防止我们将懒加载的服务注入应用程序的正常加载模块
2. 只有当服务被真正注入其他惰性组件时,它才会打包到服务中
新語法能在@Component
和@Directive
中使用嗎?
不,它們並不能。
我們仍然需要在@Component
或@Directive
中使用provider:[]
來創建多個服務實例(每個組件)。目前還沒有辦法解決這個問題......

最佳實踐
庫<br>當處理開發庫、實用程序或任何其他形式的可重用Angular邏輯時, providedIn: 'root'
是非常好的解決方案。
當消費者應用程序只需要可用庫功能的一個子集時,它也處理的非常好。只有真正使用的東西才會打包進我們的應用程序中,我們都希望打包出來的文件越小越好。
懶加載模塊<br>使用providedIn: LazyServicesModule
,然後由LazyModule
導入,再由Angular路由器惰性加載,以實施嚴格的模塊邊界和可維護的架構!
這種方法可以防止我們將懶加載的服務注入應用程序的正常加載模塊
使用providedIn: 'root'
, 'root'
將會正常工作,服務也會被正確捆綁,但是使用providedIn: LazyServiceModule
為我們提供了早期的“ missing provider ”錯誤,這是一個很好的早期信號,這有助於我們重新思考我們的架構。
什麼時候使用老的providers:[]
語法?
我們需要將配置傳遞給我們的服務嗎?
或者換句話說,我們是否有一個使用SomeModule.forRoot(someConfig)
解決的場景?
在這種情況下,我們仍然需要使用providers: []
,因為新的語法無助於我們定制服務。
另一方面,如果我們曾經使用SomeModule.forRoot()
來阻止延遲加載模塊創建服務的其他實例,我們可以簡單地使用providedIn: 'root'
來實現這一點。

總結
- 將
providedIn: 'root'
用於在整個應用程序中作為單例可用的服務; - 永遠不要使用
providedIn:EagerLiymportedmodule
,您不需要它,如果有一些非常特殊的用例,那麼請使用providers: []
來代替; - 使用
providedIn: LazyServiceModule
來防止我們將懶加載的服務注入應用程序的正常加載模塊; - 如果我們想使用
LazyServiceModule
,那麼我們必須將其導入LazyModule
,以防止循環依賴警告。然後,LazyModule
將以標準方式使用Angular Router為某些路由進行懶加載。 - 使用
@Component
或@Directive
內部的providers: []
,為特定的組件子樹提供服務,這也將導致創建多個服務實例(每個組件使用一個服務實例) - 始終嘗試保守地確定您的服務範圍,以防止依賴蔓延和由此產生的巨大混亂。
ABOUT |關於譯者王玉略: 達觀數據前端開發工程師,負責達觀數據前端開發,喜歡探索新技術,致力於將代碼與日常生活相結合,提高生活效率。