Angular singleton service

Angular singleton service

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

  • 把 @Injectable() 的 providedIn 属性声明为 root
  • 把该服务包含在 AppModule 或某个只会被 AppModule 导入的模块中。

1. 使用 providedIn

从 Angular 6.0 开始,创建单例服务的首选方式就是在那个服务类的 @Injectable 装饰器上把 providedIn 设置为 root。这会告诉 Angular 在应用的根上提供此服务。src/app/user.service.ts

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

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

2. NgModule 的 providers 数组

在基于 Angular 6.0 以前的版本构建的应用中,服务是注册在 NgModule 的 providers 数组中的,就像这样:

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

如果这个 NgModule 是根模块 AppModule,此 UserService 就会是单例的,并且在整个应用中都可用。虽然你可能会看到这种形式的代码,但是最好使用在服务自身的 @Injectable() 装饰器上设置 providedIn 属性的形式,因为 Angular 6.0 可以对这些服务进行摇树优化。

3. 还有一种是使用forRoot(),例如RouteModule

forRoot() 和 Routerlink

RouterModule 中提供了 Router 服务,同时还有一些路由指令,比如 RouterOutlet 和 routerLink 等。应用的根模块导入了 RouterModule,以便应用中有一个 Router 服务,并且让应用的根组件可以访问各个路由器指令。任何一个特性模块也必须导入 RouterModule,这样它们的组件模板中才能使用这些路由器指令。

如果 RouterModule 没有 forRoot(),那么每个特性模块都会实例化一个新的 Router 实例,而这会破坏应用的正常逻辑,因为应用中只能有一个 Router 实例。通过使用 forRoot() 方法,应用的根模块中会导入 RouterModule.forRoot(...),从而获得一个 Router 实例,而所有的特性模块要导入 RouterModule.forChild(...),它就不会实例化另外的 Router

防止重复导入 GreetingModule

只有根模块 AppModule 才能导入 GreetingModule。如果一个惰性加载模块也导入了它, 该应用就会为服务生成多个实例

要想防止惰性加载模块重复导入 GreetingModule,可以添加如下的 GreetingModule 构造函数。

src/app/greeting/greeting.module.ts

content_copyconstructor (@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 在其父注入器中查找 CoreModule,这次,它的父注入器是根注入器(而上次的父注入器是空)。 当然,这次它找到了由根模块 AppModule 导入的实例。 该构造函数检测到存在 parentModule,于是抛出一个错误。

大体总结一下Angular中声明service的不同方式和应用场景。

使用@Component

这时service与组件本身生命周期保持一致,非单例,适合声明一些需要暂存数据的工具类或者仅在某个或某几个组件中需要缓存数据的状态管理类service

使用@NgModuleproviders

这时service与应用本身生命周期保持一致(非懒加载),单例,适合声明一些需要在全局缓存数据的状态管理类service

但是有一个特例,懒加载模块中的service是会在模块加载时重新创建一个实例的,懒加载模块中均会注入后创建的service实例,因此懒加载模块与非懒加载模块间的service非单例。

使用forRoot

使用forRoot可以保证当前模块即使是懒加载模块,在加载时也不会重新创建一个新的service实例,因为懒加载模块在加载时,会临时创建一个从属于根injector的子injector,根据Angular中的依赖注入流程,当尝试通过一个子injector中注入不存在的实例对象时,会尝试向父级injector获取,因此最终可保证该service在应用任何地方被注入均是单例。

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.