單例服務(wù)是指在應(yīng)用中只存在一個(gè)實(shí)例的服務(wù)。
在 Angular 中有兩種方式來生成單例服務(wù):
@Injectable()
中的 providedIn
屬性設(shè)置為 "root"
。AppModule
或某個(gè)只會(huì)被 AppModule
導(dǎo)入的模塊中。
從 Angular 6.0 開始,創(chuàng)建單例服務(wù)的首選方式就是在那個(gè)服務(wù)類的 @Injectable
裝飾器上把 providedIn
設(shè)置為 root
。這會(huì)告訴 Angular 在應(yīng)用的根上提供此服務(wù)。
Path:"src/app/user.service.ts" 。
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class UserService {
}
在基于 Angular 6.0 以前的版本構(gòu)建的應(yīng)用中,服務(wù)是注冊(cè)在 NgModule
的 providers
數(shù)組中的,就像這樣:
@NgModule({
...
providers: [UserService],
...
})
如果這個(gè) NgModule
是根模塊 AppModule
,此 UserService
就會(huì)是單例的,并且在整個(gè)應(yīng)用中都可用。雖然你可能會(huì)看到這種形式的代碼,但是最好使用在服務(wù)自身的 @Injectable()
裝飾器上設(shè)置 providedIn
屬性的形式,因?yàn)?Angular 6.0 可以對(duì)這些服務(wù)進(jìn)行搖樹優(yōu)化。
通常,你只需要用 providedIn
提供服務(wù),用 forRoot()
/forChild()
提供路由即可。 不過,理解 forRoot()
為何能夠確保服務(wù)只有單個(gè)實(shí)例,可以讓你學(xué)會(huì)更深層次的開發(fā)知識(shí)。
如果模塊同時(shí)定義了 providers
(服務(wù))和 declarations
(組件、指令、管道),那么,當(dāng)你同時(shí)在多個(gè)特性模塊中加載此模塊時(shí),這些服務(wù)就會(huì)被注冊(cè)在多個(gè)地方。這會(huì)導(dǎo)致出現(xiàn)多個(gè)服務(wù)實(shí)例,并且該服務(wù)的行為不再像單例一樣。
有多種方式來防止這種現(xiàn)象:
providedIn
語法代替在模塊中注冊(cè)服務(wù)的方式。forRoot()
和 forChild()
方法。
使用 forRoot()
來把提供者從該模塊中分離出去,這樣你就能在根模塊中導(dǎo)入該模塊時(shí)帶上 providers
,并且在子模塊中導(dǎo)入它時(shí)不帶 providers
。
forRoot()
。forRoot()
方法中。Path:"src/app/greeting/greeting.module.ts" 。
static forRoot(config: UserServiceConfig): ModuleWithProviders<GreetingModule> {
return {
ngModule: GreetingModule,
providers: [
{provide: UserServiceConfig, useValue: config }
]
};
}
RouterModule
中提供了 Router
服務(wù),同時(shí)還有一些路由指令,比如 RouterOutlet
和 routerLink
等。應(yīng)用的根模塊導(dǎo)入了 RouterModule
,以便應(yīng)用中有一個(gè) Router
服務(wù),并且讓應(yīng)用的根組件可以訪問各個(gè)路由器指令。任何一個(gè)特性模塊也必須導(dǎo)入 RouterModule
,這樣它們的組件模板中才能使用這些路由器指令。
如果 RouterModule
沒有 forRoot()
,那么每個(gè)特性模塊都會(huì)實(shí)例化一個(gè)新的 Router 實(shí)例,而這會(huì)破壞應(yīng)用的正常邏輯,因?yàn)閼?yīng)用中只能有一個(gè) Router
實(shí)例。通過使用 forRoot()
方法,應(yīng)用的根模塊中會(huì)導(dǎo)入 RouterModule.forRoot(...)
,從而獲得一個(gè) Router
實(shí)例,而所有的特性模塊要導(dǎo)入 RouterModule.forChild(...)
,它就不會(huì)實(shí)例化另外的 Router
。
注:
- 如果你的某個(gè)模塊也同時(shí)有
providers
和declarations
,你也可以使用這種技巧來把它們分開。你可能會(huì)在某些傳統(tǒng)應(yīng)用中看到這種模式。 不過,從 Angular 6.0 開始,提供服務(wù)的最佳實(shí)踐是使用@Injectable()
的providedIn
屬性。
forRoot()
會(huì)接受一個(gè)服務(wù)配置對(duì)象,并返回一個(gè) ModuleWithProviders
對(duì)象,它帶有下列屬性:
ngModule
:在這個(gè)例子中,就是 GreetingModule
類。providers
- 配置好的服務(wù)提供者。
根模塊 AppModule
導(dǎo)入了 GreetingModule
,并把它的 providers
添加到了 AppModule
的服務(wù)提供者列表中。特別是,Angular 會(huì)把所有從其它模塊導(dǎo)入的提供者追加到本模塊的 @NgModule.providers
中列出的提供者之前。這種順序可以確保你在 AppModule
的 providers
中顯式列出的提供者,其優(yōu)先級(jí)高于導(dǎo)入模塊中給出的提供者。
在這個(gè)范例應(yīng)用中,導(dǎo)入 GreetingModule
,并只在 AppModule
中調(diào)用一次它的 forRoot()
方法。像這樣注冊(cè)它一次就可以防止出現(xiàn)多個(gè)實(shí)例。
你還可以在 GreetingModule
中添加一個(gè)用于配置 UserService
的 forRoot()
方法。
在下面的例子中,可選的注入 UserServiceConfig
擴(kuò)展了 UserService
。如果 UserServiceConfig
存在,就從這個(gè)配置中設(shè)置用戶名。
Path:"src/app/greeting/user.service.ts (constructor)" 。
constructor(@Optional() config?: UserServiceConfig) {
if (config) { this._userName = config.userName; }
}
下面是一個(gè)接受 UserServiceConfig
參數(shù)的 forRoot()
方法:
Path:"src/app/greeting/greeting.module.ts (forRoot)" 。
static forRoot(config: UserServiceConfig): ModuleWithProviders<GreetingModule> {
return {
ngModule: GreetingModule,
providers: [
{provide: UserServiceConfig, useValue: config }
]
};
}
最后,在 AppModule
的 imports
列表中調(diào)用它。在下面的代碼片段中,省略了文件的另一部分。
Path:"src/app/app.module.ts (imports)" 。
import { GreetingModule } from './greeting/greeting.module';
@NgModule({
imports: [
GreetingModule.forRoot({userName: 'Miss Marple'}),
],
})
該應(yīng)用不再顯示默認(rèn)的 “Sherlock Holmes”,而是用 “Miss Marple” 作為用戶名稱。
注:
- 在本文件的頂部要以 JavaScript import 形式導(dǎo)入
GreetingModule
,并且不要把它多次加入到本@NgModule
的imports
列表中。
只有根模塊 AppModule
才能導(dǎo)入 GreetingModule
。如果一個(gè)惰性加載模塊也導(dǎo)入了它, 該應(yīng)用就會(huì)為服務(wù)生成多個(gè)實(shí)例。
要想防止惰性加載模塊重復(fù)導(dǎo)入 GreetingModule
,可以添加如下的 GreetingModule
構(gòu)造函數(shù)。
Path:"src/app/greeting/greeting.module.ts" 。
constructor (@Optional() @SkipSelf() parentModule?: GreetingModule) {
if (parentModule) {
throw new Error(
'GreetingModule is already loaded. Import it in the AppModule only');
}
}
該構(gòu)造函數(shù)要求 Angular 把 GreetingModule
注入它自己。 如果 Angular 在當(dāng)前注入器中查找 GreetingModule
,這次注入就會(huì)導(dǎo)致死循環(huán),但是 @SkipSelf()
裝飾器的意思是 "在注入器樹中層次高于我的祖先注入器中查找 GreetingModule。"
如果該構(gòu)造函數(shù)如預(yù)期般執(zhí)行在 AppModule
中,那就不會(huì)有任何祖先注入器可以提供 CoreModule
的實(shí)例,所以該注入器就會(huì)放棄注入。
默認(rèn)情況下,當(dāng)注入器找不到想找的提供者時(shí),會(huì)拋出一個(gè)錯(cuò)誤。 但 @Optional()
裝飾器表示找不到該服務(wù)也無所謂。 于是注入器會(huì)返回 null
,parentModule
參數(shù)也就被賦成了空值,而構(gòu)造函數(shù)沒有任何異常。
但如果你把 GreetingModule
導(dǎo)入到像 CustomerModule
這樣的惰性加載模塊中,事情就不一樣了。
Angular 創(chuàng)建惰性加載模塊時(shí)會(huì)給它一個(gè)自己的注入器,它是根注入器的子注入器。 @SkipSelf()
讓 Angular 在其父注入器中查找 GreetingModule
,這次,它的父注入器是根注入器(而上次的父注入器是空)。 當(dāng)然,這次它找到了由根模塊 AppModule
導(dǎo)入的實(shí)例。 該構(gòu)造函數(shù)檢測到存在 parentModule
,于是拋出一個(gè)錯(cuò)誤。
以下這兩個(gè)文件僅供參考:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
/* App Root */
import { AppComponent } from './app.component';
/* Feature Modules */
import { ContactModule } from './contact/contact.module';
import { GreetingModule } from './greeting/greeting.module';
/* Routing Module */
import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
BrowserModule,
ContactModule,
GreetingModule.forRoot({userName: 'Miss Marple'}),
AppRoutingModule
],
declarations: [
AppComponent
],
bootstrap: [AppComponent]
})
export class AppModule { }
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
import { GreetingComponent } from './greeting.component';
import { UserServiceConfig } from './user.service';
@NgModule({
imports: [ CommonModule ],
declarations: [ GreetingComponent ],
exports: [ GreetingComponent ]
})
export class GreetingModule {
constructor (@Optional() @SkipSelf() parentModule?: GreetingModule) {
if (parentModule) {
throw new Error(
'GreetingModule is already loaded. Import it in the AppModule only');
}
}
static forRoot(config: UserServiceConfig): ModuleWithProviders<GreetingModule> {
return {
ngModule: GreetingModule,
providers: [
{provide: UserServiceConfig, useValue: config }
]
};
}
}
更多建議: