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()
和 Router
link
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()
装饰器表示找不到该服务也无所谓。 于是注入器会返回 null
,parentModule
参数也就被赋成了空值,而构造函数没有任何异常。
但如果你把 GreetingModule
导入到像 CustomerModule
这样的惰性加载模块中,事情就不一样了。
Angular 创建惰性加载模块时会给它一个自己的注入器,它是根注入器的子注入器。 @SkipSelf()
让 Angular 在其父注入器中查找 CoreModule
,这次,它的父注入器是根注入器(而上次的父注入器是空)。 当然,这次它找到了由根模块 AppModule
导入的实例。 该构造函数检测到存在 parentModule
,于是抛出一个错误。
大体总结一下Angular
中声明service
的不同方式和应用场景。
使用@Component
这时service
与组件本身生命周期保持一致,非单例,适合声明一些需要暂存数据的工具类或者仅在某个或某几个组件中需要缓存数据的状态管理类service
使用@NgModule
的providers
这时service
与应用本身生命周期保持一致(非懒加载),单例,适合声明一些需要在全局缓存数据的状态管理类service
。
但是有一个特例,懒加载模块中的service
是会在模块加载时重新创建一个实例的,懒加载模块中均会注入后创建的service
实例,因此懒加载模块与非懒加载模块间的service
非单例。
使用forRoot
使用forRoot
可以保证当前模块即使是懒加载模块,在加载时也不会重新创建一个新的service
实例,因为懒加载模块在加载时,会临时创建一个从属于根injector
的子injector
,根据Angular中的依赖注入流程,当尝试通过一个子injector
中注入不存在的实例对象时,会尝试向父级injector
获取,因此最终可保证该service
在应用任何地方被注入均是单例。