Angular9 單例服務

2020-07-03 14:05 更新

單例服務是指在應用中只存在一個實例的服務。

提供單例服務

在 Angular 中有兩種方式來生成單例服務:

  • @Injectable() 中的 providedIn 屬性設置為 "root"。

  • 把該服務包含在 AppModule 或某個只會被 AppModule 導入的模塊中。

使用 providedIn

從 Angular 6.0 開始,創(chuàng)建單例服務的首選方式就是在那個服務類的 @Injectable 裝飾器上把 providedIn 設置為 root。這會告訴 Angular 在應用的根上提供此服務。

Path:"src/app/user.service.ts" 。

import { Injectable } from '@angular/core';


@Injectable({
  providedIn: 'root',
})
export class UserService {
}

NgModule 的 providers 數組

在基于 Angular 6.0 以前的版本構建的應用中,服務是注冊在 NgModuleproviders 數組中的,就像這樣:

@NgModule({
  ...
  providers: [UserService],
  ...
})

如果這個 NgModule 是根模塊 AppModule,此 UserService 就會是單例的,并且在整個應用中都可用。雖然你可能會看到這種形式的代碼,但是最好使用在服務自身的 @Injectable() 裝飾器上設置 providedIn 屬性的形式,因為 Angular 6.0 可以對這些服務進行搖樹優(yōu)化。

forRoot() 模式

通常,你只需要用 providedIn 提供服務,用 forRoot()/forChild() 提供路由即可。 不過,理解 forRoot() 為何能夠確保服務只有單個實例,可以讓你學會更深層次的開發(fā)知識。

如果模塊同時定義了 providers(服務)和 declarations(組件、指令、管道),那么,當你同時在多個特性模塊中加載此模塊時,這些服務就會被注冊在多個地方。這會導致出現多個服務實例,并且該服務的行為不再像單例一樣。

有多種方式來防止這種現象:

  • providedIn 語法代替在模塊中注冊服務的方式。

  • 把你的服務分離到它們自己的模塊中。

  • 在模塊中分別定義 forRoot()forChild() 方法。

使用 forRoot() 來把提供者從該模塊中分離出去,這樣你就能在根模塊中導入該模塊時帶上 providers,并且在子模塊中導入它時不帶 providers。

  1. 在該模塊中創(chuàng)建一個靜態(tài)方法 forRoot()。

  1. 把這些提供者放進 forRoot() 方法中。

Path:"src/app/greeting/greeting.module.ts" 。

static forRoot(config: UserServiceConfig): ModuleWithProviders<GreetingModule> {
  return {
    ngModule: GreetingModule,
    providers: [
      {provide: UserServiceConfig, useValue: config }
    ]
  };
}

forRoot() 和 Router

RouterModule 中提供了 Router 服務,同時還有一些路由指令,比如 RouterOutletrouterLink 等。應用的根模塊導入了 RouterModule,以便應用中有一個 Router 服務,并且讓應用的根組件可以訪問各個路由器指令。任何一個特性模塊也必須導入 RouterModule,這樣它們的組件模板中才能使用這些路由器指令。

如果 RouterModule 沒有 forRoot(),那么每個特性模塊都會實例化一個新的 Router 實例,而這會破壞應用的正常邏輯,因為應用中只能有一個 Router 實例。通過使用 forRoot() 方法,應用的根模塊中會導入 RouterModule.forRoot(...),從而獲得一個 Router 實例,而所有的特性模塊要導入 RouterModule.forChild(...),它就不會實例化另外的 Router

注:

  • 如果你的某個模塊也同時有 providersdeclarations,你也可以使用這種技巧來把它們分開。你可能會在某些傳統(tǒng)應用中看到這種模式。 不過,從 Angular 6.0 開始,提供服務的最佳實踐是使用 @Injectable()providedIn 屬性。

forRoot() 的工作原理

forRoot() 會接受一個服務配置對象,并返回一個 ModuleWithProviders 對象,它帶有下列屬性:

  • ngModule:在這個例子中,就是 GreetingModule 類。

  • providers - 配置好的服務提供者。

根模塊 AppModule 導入了 GreetingModule,并把它的 providers 添加到了 AppModule 的服務提供者列表中。特別是,Angular 會把所有從其它模塊導入的提供者追加到本模塊的 @NgModule.providers 中列出的提供者之前。這種順序可以確保你在 AppModuleproviders 中顯式列出的提供者,其優(yōu)先級高于導入模塊中給出的提供者。

在這個范例應用中,導入 GreetingModule,并只在 AppModule 中調用一次它的 forRoot() 方法。像這樣注冊它一次就可以防止出現多個實例。

你還可以在 GreetingModule 中添加一個用于配置 UserServiceforRoot() 方法。

在下面的例子中,可選的注入 UserServiceConfig 擴展了 UserService。如果 UserServiceConfig 存在,就從這個配置中設置用戶名。

Path:"src/app/greeting/user.service.ts (constructor)" 。

constructor(@Optional() config?: UserServiceConfig) {
  if (config) { this._userName = config.userName; }
}

下面是一個接受 UserServiceConfig 參數的 forRoot() 方法:

Path:"src/app/greeting/greeting.module.ts (forRoot)" 。

static forRoot(config: UserServiceConfig): ModuleWithProviders<GreetingModule> {
  return {
    ngModule: GreetingModule,
    providers: [
      {provide: UserServiceConfig, useValue: config }
    ]
  };
}

最后,在 AppModuleimports 列表中調用它。在下面的代碼片段中,省略了文件的另一部分。

Path:"src/app/app.module.ts (imports)" 。

import { GreetingModule } from './greeting/greeting.module';
@NgModule({
  imports: [
    GreetingModule.forRoot({userName: 'Miss Marple'}),
  ],
})

該應用不再顯示默認的 “Sherlock Holmes”,而是用 “Miss Marple” 作為用戶名稱。

注:

  • 在本文件的頂部要以 JavaScript import 形式導入 GreetingModule,并且不要把它多次加入到本 @NgModuleimports 列表中。

防止重復導入 GreetingModule

只有根模塊 AppModule 才能導入 GreetingModule。如果一個惰性加載模塊也導入了它, 該應用就會為服務生成多個實例。

要想防止惰性加載模塊重復導入 GreetingModule,可以添加如下的 GreetingModule 構造函數。

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');
  }
}

該構造函數要求 Angular 把 GreetingModule 注入它自己。 如果 Angular 在當前注入器中查找 GreetingModule,這次注入就會導致死循環(huán),但是 @SkipSelf() 裝飾器的意思是 "在注入器樹中層次高于我的祖先注入器中查找 GreetingModule。"

如果該構造函數如預期般執(zhí)行在 AppModule 中,那就不會有任何祖先注入器可以提供 CoreModule 的實例,所以該注入器就會放棄注入。

默認情況下,當注入器找不到想找的提供者時,會拋出一個錯誤。 但 @Optional() 裝飾器表示找不到該服務也無所謂。 于是注入器會返回 nullparentModule 參數也就被賦成了空值,而構造函數沒有任何異常。

但如果你把 GreetingModule 導入到像 CustomerModule 這樣的惰性加載模塊中,事情就不一樣了。

Angular 創(chuàng)建惰性加載模塊時會給它一個自己的注入器,它是根注入器的子注入器。 @SkipSelf() 讓 Angular 在其父注入器中查找 GreetingModule,這次,它的父注入器是根注入器(而上次的父注入器是空)。 當然,這次它找到了由根模塊 AppModule 導入的實例。 該構造函數檢測到存在 parentModule,于是拋出一個錯誤。

以下這兩個文件僅供參考:

  1. Path:"src/app/app.module.ts" 。

    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 { }

  1. Path:"src/app/greeting.module.ts" 。

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 }
      ]
    };
  }
}
以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號