Angular9 單例服務(wù)

2020-07-03 14:05 更新

單例服務(wù)是指在應(yīng)用中只存在一個(gè)實(shí)例的服務(wù)。

提供單例服務(wù)

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

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

  • 把該服務(wù)包含在 AppModule 或某個(gè)只會(huì)被 AppModule 導(dǎo)入的模塊中。

使用 providedIn

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

NgModule 的 providers 數(shù)組

在基于 Angular 6.0 以前的版本構(gòu)建的應(yīng)用中,服務(wù)是注冊(cè)在 NgModuleproviders 數(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)化。

forRoot() 模式

通常,你只需要用 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ù)的方式。

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

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

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

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

  1. 把這些提供者放進(jìn) 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 服務(wù),同時(shí)還有一些路由指令,比如 RouterOutletrouterLink 等。應(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í)有 providersdeclarations,你也可以使用這種技巧來把它們分開。你可能會(huì)在某些傳統(tǒng)應(yīng)用中看到這種模式。 不過,從 Angular 6.0 開始,提供服務(wù)的最佳實(shí)踐是使用 @Injectable()providedIn 屬性。

forRoot() 的工作原理

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 中列出的提供者之前。這種順序可以確保你在 AppModuleproviders 中顯式列出的提供者,其優(yōu)先級(jí)高于導(dǎo)入模塊中給出的提供者。

在這個(gè)范例應(yīng)用中,導(dǎo)入 GreetingModule,并只在 AppModule 中調(diào)用一次它的 forRoot() 方法。像這樣注冊(cè)它一次就可以防止出現(xiàn)多個(gè)實(shí)例。

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

在下面的例子中,可選的注入 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 }
    ]
  };
}

最后,在 AppModuleimports 列表中調(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,并且不要把它多次加入到本 @NgModuleimports 列表中。

防止重復(fù)導(dǎo)入 GreetingModule

只有根模塊 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ì)返回 nullparentModule 參數(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è)文件僅供參考:

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)