填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

單例服務

Singleton services

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

A singleton service is a service for which only one instance exists in an app.

本頁中描述的這種全應用級單例服務的例子位於現場演練 / 下載範例,它示範了 NgModule 的所有已文件化的特性。

For a sample app using the app-wide singleton service that this page describes, see the現場演練 / 下載範例showcasing all the documented features of NgModules.

提供單例服務

Providing a singleton service

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

There are two ways to make a service a singleton in Angular:

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

    Set the providedIn property of the @Injectable() to "root".

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

    Include the service in the AppModule or in a module that is only imported by the AppModule

使用 providedIn

Using providedIn

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

Beginning with Angular 6.0, the preferred way to create a singleton service is to set providedIn to root on the service's @Injectable() decorator. This tells Angular to provide the service in the application root.

import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class UserService { }
src/app/user.service.ts
      
      import { Injectable } from '@angular/core';

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

要想深入瞭解關於服務的資訊,參閱《英雄之旅》課程中的服務一章。

For more detailed information on services, see the Services chapter of the Tour of Heroes tutorial.

NgModule 的 providers 陣列

NgModule providers array

在基於 Angular 6.0 以前的版本建構的應用中,服務是註冊在 NgModule 的 providers 陣列中的,就像這樣:

In apps built with Angular versions prior to 6.0, services are registered NgModule providers arrays as follows:

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

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

If this NgModule were the root AppModule, the UserService would be a singleton and available throughout the app. Though you may see it coded this way, using the providedIn property of the @Injectable() decorator on the service itself is preferable as of Angular 6.0 as it makes your services tree-shakable.

forRoot() 模式

The forRoot() pattern

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

Generally, you'll only need providedIn for providing services and forRoot()/forChild() for routing. However, understanding how forRoot() works to make sure a service is a singleton will inform your development at a deeper level.

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

If a module defines both providers and declarations (components, directives, pipes), then loading the module in multiple feature modules would duplicate the registration of the service. This could result in multiple service instances and the service would no longer behave as a singleton.

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

There are multiple ways to prevent this:

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

    Use the providedIn syntax instead of registering the service in the module.

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

    Separate your services into their own module.

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

    Define forRoot() and forChild() methods in the module.

注意:有兩個範例應用可以讓你檢視這種情況,更進階的方式參閱NgModules 現場演練NgModules 現場演練,它在路由模組中包含 forRoot()forChild(),而 GreetingModule 是一個比較簡單的延遲載入範例延遲載入範例。在延遲載入模組中有簡要的解釋。

Note: There are two example apps where you can see this scenario; the more advancedNgModules live exampleNgModules live example, which contains forRoot() and forChild() in the routing modules and the GreetingModule, and the simplerLazy Loading live exampleLazy Loading live example. For an introductory explanation see the Lazy Loading Feature Modules guide.

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

Use forRoot() to separate providers from a module so you can import that module into the root module with providers and child modules without providers.

  1. 在該模組中建立一個靜態方法 forRoot()

    Create a static method forRoot() on the module.

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

    Place the providers into the forRoot() method.

static forRoot(config: UserServiceConfig): ModuleWithProviders<GreetingModule> { return { ngModule: GreetingModule, providers: [ {provide: UserServiceConfig, useValue: config } ] }; }
src/app/greeting/greeting.module.ts
      
      static forRoot(config: UserServiceConfig): ModuleWithProviders<GreetingModule> {
  return {
    ngModule: GreetingModule,
    providers: [
      {provide: UserServiceConfig, useValue: config }
    ]
  };
}
    

forRoot()Router

forRoot() and the Router

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

RouterModule provides the Router service, as well as router directives, such as RouterOutlet and routerLink. The root application module imports RouterModule so that the application has a Router and the root application components can access the router directives. Any feature modules must also import RouterModule so that their components can place router directives into their templates.

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

If the RouterModule didn’t have forRoot() then each feature module would instantiate a new Router instance, which would break the application as there can only be one Router. By using the forRoot() method, the root application module imports RouterModule.forRoot(...) and gets a Router, and all feature modules import RouterModule.forChild(...) which does not instantiate another Router.

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

Note: If you have a module which has both providers and declarations, you can use this technique to separate them out and you may see this pattern in legacy apps. However, since Angular 6.0, the best practice for providing services is with the @Injectable() providedIn property.

forRoot() 的工作原理

How forRoot() works

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

forRoot() takes a service configuration object and returns a ModuleWithProviders, which is a simple object with the following properties:

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

    ngModule: in this example, the GreetingModule class.

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

    providers: the configured providers.

在這個現場演練現場演練 / 下載範例中,根模組 AppModule 匯入了 GreetingModule,並把它的 providers 新增到了 AppModule 的服務提供者列表中。特別是,Angular 會把所有從其它模組匯入的提供者追加到本模組的 @NgModule.providers 中列出的提供者之前。這種順序可以確保你在 AppModuleproviders 中顯式列出的提供者,其優先順序高於匯入模組中給出的提供者。

In thelive examplelive example / 下載範例the root AppModule imports the GreetingModule and adds the providers to the AppModule providers. Specifically, Angular accumulates all imported providers before appending the items listed in @NgModule.providers. This sequence ensures that whatever you add explicitly to the AppModule providers takes precedence over the providers of imported modules.

在這個範例應用中,匯入 GreetingModule,並只在 AppModule 中呼叫一次它的 forRoot() 方法。像這樣註冊它一次就可以防止出現多個實例。

The sample app imports GreetingModule and uses its forRoot() method one time, in AppModule. Registering it once like this prevents multiple instances.

你還可以在 GreetingModule 中新增一個用於配置 UserServiceforRoot() 方法。

You can also add a forRoot() method in the GreetingModule that configures the greeting UserService.

在下面的例子中,可選的注入 UserServiceConfig 擴充套件了 UserService。如果 UserServiceConfig 存在,就從這個配置中設定使用者名稱。

In the following example, the optional, injected UserServiceConfig extends the greeting UserService. If a UserServiceConfig exists, the UserService sets the user name from that config.

constructor(@Optional() config?: UserServiceConfig) { if (config) { this._userName = config.userName; } }
src/app/greeting/user.service.ts (constructor)
      
      constructor(@Optional() config?: UserServiceConfig) {
  if (config) { this._userName = config.userName; }
}
    

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

Here's forRoot() that takes a UserServiceConfig object:

static forRoot(config: UserServiceConfig): ModuleWithProviders<GreetingModule> { return { ngModule: GreetingModule, providers: [ {provide: UserServiceConfig, useValue: config } ] }; }
src/app/greeting/greeting.module.ts (forRoot)
      
      static forRoot(config: UserServiceConfig): ModuleWithProviders<GreetingModule> {
  return {
    ngModule: GreetingModule,
    providers: [
      {provide: UserServiceConfig, useValue: config }
    ]
  };
}
    

最後,在 AppModuleimports列表中呼叫它。在下面的程式碼片段中,省略了檔案的另一部分。要檢視完整檔案,參閱現場演練 / 下載範例或繼續閱讀本文件的後續章節。

Lastly, call it within the imports list of the AppModule. In the following snippet, other parts of the file are left out. For the complete file, see the現場演練 / 下載範例, or continue to the next section of this document.

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

該應用不再顯示預設的 “Sherlock Holmes”,而是用 “Miss Marple” 作為使用者名稱稱。

The app displays "Miss Marple" as the user instead of the default "Sherlock Holmes".

記住:在本檔案的頂部要以 JavaScript import 形式匯入 GreetingModule,並且不要把它多次加入到本 @NgModuleimports 列表中。

Remember to import GreetingModule as a Javascript import at the top of the file and don't add it to more than one @NgModule imports list.

防止重複匯入 GreetingModule

Prevent reimport of the GreetingModule

只有根模組 AppModule 才能匯入 GreetingModule。如果一個延遲載入模組也匯入了它, 該應用就會為服務產生多個實例

Only the root AppModule should import the GreetingModule. If a lazy-loaded module imports it too, the app can generate multiple instances of a service.

要想防止延遲載入模組重複匯入 GreetingModule,可以新增如下的 GreetingModule 建構函式。

To guard against a lazy loaded module re-importing GreetingModule, add the following GreetingModule constructor.

constructor(@Optional() @SkipSelf() parentModule?: GreetingModule) { if (parentModule) { throw new Error( 'GreetingModule is already loaded. Import it in the AppModule only'); } }
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,這次注入就會導致死迴圈,但是 @SkipSelf() 裝飾器的意思是 "在注入器樹中層次高於我的祖先注入器中查詢 GreetingModule。"

The constructor tells Angular to inject the GreetingModule into itself. The injection would be circular if Angular looked for GreetingModule in the current injector, but the @SkipSelf() decorator means "look for GreetingModule in an ancestor injector, above me in the injector hierarchy."

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

If the constructor executes as intended in the AppModule, there would be no ancestor injector that could provide an instance of CoreModule and the injector should give up.

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

By default, the injector throws an error when it can't find a requested provider. The @Optional() decorator means not finding the service is OK. The injector returns null, the parentModule parameter is null, and the constructor concludes uneventfully.

但如果你把 GreetingModule 匯入到像 CustomerModule 這樣的延遲載入模組中,事情就不一樣了。

It's a different story if you improperly import GreetingModule into a lazy loaded module such as CustomersModule.

Angular 建立延遲載入模組時會給它一個自己的注入器,它是根注入器的子注入器@SkipSelf() 讓 Angular 在其父注入器中查詢 GreetingModule,這次,它的父注入器是根注入器(而上次的父注入器是空)。 當然,這次它找到了由根模組 AppModule 匯入的實例。 該建構函式檢測到存在 parentModule,於是丟擲一個錯誤。

Angular creates a lazy loaded module with its own injector, a child of the root injector. @SkipSelf() causes Angular to look for a GreetingModule in the parent injector, which this time is the root injector. Of course it finds the instance imported by the root AppModule. Now parentModule exists and the constructor throws the error.

以下這兩個檔案僅供參考:

Here are the two files in their entirety for reference:

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

關於 NgModule 的更多知識

More on NgModules

你還可能對下列內容感興趣:

You may also be interested in: