Angular9 单例服务

单例服务是指在应用中只存在一个实例的服务。

提供单例服务

在 Angular 中有两种方式来生成单例服务:

  • @Injectable() 中的 providedIn 属性设置为 "root"

  • 把该服务包含在 AppModule 或某个只会被 AppModule 导入的模块中。

使用 providedIn

从 Angular 6.0 开始,创建单例服务的首选方式就是在那个服务类的 @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 可以对这些服务进行摇树优化。

forRoot() 模式

通常,你只需要用 providedIn 提供服务,用 forRoot()/forChild() 提供路由即可。 不过,理解 forRoot() 为何能够确保服务只有单个实例,可以让你学会更深层次的开发知识。

如果模块同时定义了 providers(服务)和 declarations(组件、指令、管道),那么,当你同时在多个特性模块中加载此模块时,这些服务就会被注册在多个地方。这会导致出现多个服务实例,并且该服务的行为不再像单例一样。

有多种方式来防止这种现象:

  • providedIn 语法代替在模块中注册服务的方式。

  • 把你的服务分离到它们自己的模块中。

  • 在模块中分别定义 forRoot()forChild() 方法。

使用 forRoot() 来把提供者从该模块中分离出去,这样你就能在根模块中导入该模块时带上 providers,并且在子模块中导入它时不带 providers

  1. 在该模块中创建一个静态方法 forRoot()

  2. 把这些提供者放进 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,你也可以使用这种技巧来把它们分开。你可能会在某些传统应用中看到这种模式。 不过,从 Angular 6.0 开始,提供服务的最佳实践是使用 @Injectable()providedIn 属性。

forRoot() 的工作原理

forRoot() 会接受一个服务配置对象,并返回一个 ModuleWithProviders 对象,它带有下列属性:

  • ngModule:在这个例子中,就是 GreetingModule 类。

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

根模块 AppModule 导入了 GreetingModule,并把它的 providers 添加到了 AppModule 的服务提供者列表中。特别是,Angular 会把所有从其它模块导入的提供者追加到本模块的 @NgModule.providers 中列出的提供者之前。这种顺序可以确保你在 AppModuleproviders 中显式列出的提供者,其优先级高于导入模块中给出的提供者。

在这个范例应用中,导入 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,这次注入就会导致死循环,但是 @SkipSelf() 装饰器的意思是 "在注入器树中层次高于我的祖先注入器中查找 GreetingModule。"

如果该构造函数如预期般执行在 AppModule 中,那就不会有任何祖先注入器可以提供 CoreModule 的实例,所以该注入器就会放弃注入。

默认情况下,当注入器找不到想找的提供者时,会抛出一个错误。 但 @Optional() 装饰器表示找不到该服务也无所谓。 于是注入器会返回 nullparentModule 参数也就被赋成了空值,而构造函数没有任何异常。

但如果你把 GreetingModule 导入到像 CustomerModule 这样的惰性加载模块中,事情就不一样了。

Angular 创建惰性加载模块时会给它一个自己的注入器,它是根注入器的子注入器。 @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 { }
  2. 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

公众号
微信公众号

编程狮公众号

意见反馈
返回顶部