填写这份《一分钟调查》,帮我们(开发组)做得更好!去填写Home

单例服务

Singleton services

前提条件:

Prerequisites:

本页中描述的这种全应用级单例服务的例子位于在线例子 / 下载范例,它示范了 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

    Declare root for the value of the @Injectable() providedIn property

  • 把该服务包含在 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:

注意:有两个范例应用可以让你查看这种情况,更高级的方式参见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 { return { ngModule: GreetingModule, providers: [ {provide: UserServiceConfig, useValue: config } ] }; }
src/app/greeting/greeting.module.ts
      
      static forRoot(config: UserServiceConfig): ModuleWithProviders {
  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 { return { ngModule: GreetingModule, providers: [ {provide: UserServiceConfig, useValue: config } ] }; }
src/app/greeting/greeting.module.ts (forRoot)
      
      static forRoot(config: UserServiceConfig): ModuleWithProviders {
  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 在其父注入器中查找 CoreModule,这次,它的父注入器是根注入器(而上次的父注入器是空)。 当然,这次它找到了由根模块 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 { 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: