快速理解 Angular Service:關於生命週期, providedIn root, lazy Module 那些事
前言
Angular 自從 6 開始,在裝飾器@Injectable()
增加了參數providedIn: 'root'
從官方文件,亦可看到,除了'root'
外,還有'any'
而在@NgModule
裡,又有provider
可以傳入Service
…
而這之間到底有什麼差別?該使用什麼方式注入Service
才是比較好的作法?
若再考慮效能,該怎麼使用會更好呢?
結論
先上結論,快速重點筆記
providedIn: 'root'
是 Angular 6 以後新增的功能,主要是簡化寫法,避免Service
須在每個@NgModule.provider
重覆書寫的冗事- 若
@Injectable()
不提供任何參數,則須在@NgModule.provider
裡傳入該Service
- 若
Service
同時存在於@NgModule.provider
與providedIn
,則以@NgModule.provider
為主,會覆蓋掉providedIn
的設定 - 若無特殊需求,大部份情況可以直接無腦使用
providedIn: 'root'
,不必寫@NgModule.provider
- 很多人都沒注意到,
@Component
其實也有provider
參數。代表Service
限定於該@Component
及其子Component下使用。這Service
會跟隨@Component
創建與消失 - 特例:
AppModule
與AppComponent
都是根元素,若Service
要寫在provider
裡,不論寫在哪用起來體感都一樣。但官方建議寫在AppModule
最好 providedIn: 'root'
是在整個 Angular 應用中提供單例(singleton)服務,避免產生多個實例- ★
providedIn: 'root'
若只有lazy-loading Module
使用時,則會跟隨 lazy-load 初始化時才會載入(會存在於子注入器)。其Service
生命週期等同於該lazy-loading Module
providedIn: 'root'
若已不在使用,但沒刪掉該實體檔案,打包時的搖樹優化將自動搖掉,可以有效降低打包結果的檔案大小。但寫在@NgModule.provider
則不會(原理與terser有關。可參考這篇,文末參考資料有簡體中文的文章分享)- 若不希望
Service
被全域共用,則有兩種寫法:- ★(建議使用) 在
Service
直接傳入可使用的模組 class。如@Injectable({ providedIn: UserModule })
- 於
Service
裝飾器不傳入參數,僅寫@Injectable()
,但在該@NgModule
的provider
寫此Service
- 若
Service
會被兩個或以上@NgModule
使用,則須提升到全域(即'root'
),以確保只會存在一個實例 - 若希望各
@NgModule
持有自己的Service
實例,可在providedIn
傳入'any'
。此參數具體用法見後面
- ★(建議使用) 在
providedIn
有以下參數:'root'
,'any'
,'platform'
,Module class名字
'root'
:全域使用'any'
:eagerModule,則會置於全域共用使用同一單例;若lazy Module,則會各自產生自己的實例。若此Service
同時被eager 與 lazy 共同使用,則會存在兩個實例。eager使用全域的,lazy使用自己的。故若Service
裡面資料有狀態的(stateful),須注意自己的業務邏輯是否符合此情境Module class名字
:相當於寫在@NgModule.provider
,即限定此Service
僅能在該Module以下使用'platform'
:當一個頁面有多個Angular實例時,可以共用。使用情況極低
@NgModule.provider
除了單純傳入陣列外,尚可設定Service
產生方式useClass
:單純傳入Service
陣列,即為此方式的簡寫。若希望是不同的實作,則使用useClass
代換。如單元測試常使用useExist
:相當於為現有的Service
取別名(alias name)。兩個不同名字Service
,但指向同一個實例。在重構時有機會使用到useFactory
:每次使用時都會產生新的實例。若此Service
有在注入其他服務,可在傳入deps: []
提供相應注入實例useValue
:單純注入成一個靜態物件
- 注入
Service
的裝飾器調整Service
查找狀況@optional
:若找不到Service
,不報錯,返回null@self
:僅在當前元件查找,若找不到就會報錯。可搭配@optional
避免錯誤@skipSelf
:使用父層的Service
,忽略子層的。若父層找不到則報錯。可搭配@optional
避免錯誤@Host
:限制Angular搜尋Service
的方式,僅查找自己或 component 的 HTML 元素階層往上一層之父元件。可搭配@optional
避免錯誤@self
vs@Host
較複雜,可以參考:stack Overflow / Difference between @Self and @Host、官方文件 / 依賴注入實戰、IT邦幫忙 / 關於 @Self、@Optional、@SkipSelf 的二三事與 @Host 的陷阱
個人推薦的建議作法
為求效能,大部份Module都會寫成lazy Module
我推薦在沒有特殊情況下,單純Service
一律使用providedIn
共用的Service
毫無疑問就是providedIn: 'root'
而多人維護下最怕的就是Service
被意料之外的Module使用了,而該Module owner又是屬於別人
在重構時還得考慮他人使用情況,徒增困擾
故lazy Module自己的Service
,在providedIn
就直接限定僅自己使用
亦可確保此Service
生命週期會跟隨自己的Module產生
當Service
有較複雜的情境時
再透過@NgModule.provider
覆蓋掉providedIn
,使用useXXX
控制Service
產生方式
當有function須跨不同owner Module使用時,則將function搬至'root'
層的Service
現有的則注入該rootService
,直接call root層使用
以避免跨太遠去改到他人業務範圍的程式碼,造成額外的權責問題
亦不會影響他人現有的業務邏輯
在告知對方有這樣的調整即可
當這是一致的開發習慣時,就算忘記告知
對方看到自己的程式碼變成這樣,也可以迅速理解是此段邏輯有被多人共用了
當自己有需求要調整這function,就不必擔心影響到其他人
簡單介紹:多級注入器
Angular 預設行為,先由自己層查找,找不到再往上一層
一直都找不到時,會一路走到最根層NullInjector()
,直接拋出錯誤
若不希望報錯,則搭配使用@optional
裝飾器避免錯誤
若純為eager Module使用,在啟動時就會先行註冊進全域
若純為lazy Module,則該Module 初始化時,註冊進自己的子注入器
若同時都使用到,則須看providedIn
傳入何種參數
'root'
皆使用全域單例'any'
則會存在兩個實例。eager使用全域的,lazy使用自己的。故若Service
裡面資料有狀態的(stateful),須注意自己的業務邏輯是否符合此情境
可再搭配@self
、@skipSelf
、@Host
限制 Angular 搜尋Service
路線與方式
注入Service
的裝飾器調整Service
查找狀況
@optional
:若找不到Service
,不報錯,返回null@self
:僅在當前元件查找,若找不到就會報錯。可搭配@optional
避免錯誤@skipSelf
:使用父層的Service
,忽略子層的。若父層找不到則報錯。可搭配@optional
避免錯誤@Host
:限制Angular搜尋Service
的方式,僅查找自己或 component 的 HTML 元素階層往上一層之父元件。可搭配@optional
避免錯誤@self
vs@Host
較複雜,可以參考:stack Overflow / Difference between @Self and @Host、官方文件 / 依賴注入實戰、IT邦幫忙 / 關於 @Self、@Optional、@SkipSelf 的二三事與 @Host 的陷阱
具體內容可參考:官方文件 / 多級注入器
NgModule.provider vs providedIn
NgModule.provider會覆蓋掉providedIn,以便於複雜的情況下時,可以有較高的自定性。比如單元測試時會Mock Service
在大部份情況下,透過providedIn
傳參,應足以應付日常需求
providedIn
參數說明
providedIn
有以下參數:'root'
, 'any'
, 'platform'
, Module class名字
'root'
:全域使用'any'
:eagerModule,則會置於全域共用使用同一單例;若lazy Module,則會各自產生自己的實例。若此Service
同時被eager 與 lazy 共同使用,則會存在兩個實例。eager使用全域的,lazy使用自己的。故若Service
裡面資料有狀態的(stateful),須注意自己的業務邏輯是否符合此情境Module class名字
:相當於寫在@NgModule.provider
,即限定此Service
僅能在該Module以下使用'platform'
:當一個頁面有多個Angular實例時,可以共用。使用情況極低
具體內容可參考:官方文件 / 在模組中提供依賴
NgModule.provider
除了最直覺的傳入Service
陣列之外,尚可設定實例產生方式
單純傳入Service格式
@NgModule({
providers:[UserService, LoggerService]
})
設定實例產生方式格式
@NgModule({
providers:[{ provide: Logger, useClass: Logger }]
})
實例產生方式
useClass
:單純傳入Service
陣列,即為此方式的簡寫。若希望是不同的實作,則使用useClass
代換。如單元測試常使用useExist
:相當於為現有的Service
取別名(alias name)。兩個不同名字Service
,但指向同一個實例。在重構時有機會使用到useFactory
:每次使用時都會產生新的實例。若此Service
有在注入其他服務,可在傳入deps: []
提供相應注入實例useValue
:單純注入成一個靜態物件
參考資料
stack overflow / purpose of providedIn with the injectable decrator
juri.dev / Angular Services, providedIn and Lazy Modules
Li Mei / Angular性能优化:Tree Shaking
ProvidedIn root, any & platform in Angular
IT邦幫忙 / 關於 @Self、@Optional、@SkipSelf 的二三事與 @Host 的陷阱