单例服务
Singleton services
单例服务是指在应用中只存在一个实例的服务。
A singleton service is a service for which only one instance exists in an app.
本页中描述的这种全应用级单例服务的例子位于
For a sample app using the app-wide singleton service that this page describes, see the
提供单例服务
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 theAppModule
使用 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 {
}
要想深入了解关于服务的信息,参见《英雄指南》教程中的服务一章。
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 是根模块 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()
andforChild()
methods in the module.
注意:有两个范例应用可以让你查看这种情况,更高级的方式参见forRoot()
和 forChild()
,而 GreetingModule
是一个比较简单的
Note: There are two example apps where you can see this scenario; the more advancedforRoot()
and forChild()
in the routing modules and the GreetingModule
, and the simpler
使用 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
.
在该模块中创建一个静态方法
forRoot()
。Create a static method
forRoot()
on the module.把这些提供者放进
forRoot()
方法中。Place the providers into the
forRoot()
method.
static forRoot(config: UserServiceConfig): ModuleWithProviders {
return {
ngModule: GreetingModule,
providers: [
{provide: UserServiceConfig, useValue: config }
]
};
}
forRoot()
和 Router
forRoot()
and the Router
RouterModule
中提供了 Router
服务,同时还有一些路由指令,比如 RouterOutlet
和 routerLink
等。应用的根模块导入了 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, theGreetingModule
class.providers
- 配置好的服务提供者providers
: the configured providers.
在这个AppModule
导入了 GreetingModule
,并把它的 providers
添加到了 AppModule
的服务提供者列表中。特别是,Angular 会把所有从其它模块导入的提供者追加到本模块的 @NgModule.providers
中列出的提供者之前。这种顺序可以确保你在 AppModule
的 providers
中显式列出的提供者,其优先级高于导入模块中给出的提供者。
In theAppModule
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
中添加一个用于配置 UserService
的 forRoot()
方法。
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; }
}
下面是一个接受 UserServiceConfig
参数的 forRoot()
方法:
Here's forRoot()
that takes a UserServiceConfig
object:
static forRoot(config: UserServiceConfig): ModuleWithProviders {
return {
ngModule: GreetingModule,
providers: [
{provide: UserServiceConfig, useValue: config }
]
};
}
最后,在 AppModule
的 imports
列表中调用它。在下面的代码片段中,省略了文件的另一部分。要查看完整文件,参见
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
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
,并且不要把它多次加入到本 @NgModule
的 imports
列表中。
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');
}
}
该构造函数要求 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()
装饰器表示找不到该服务也无所谓。 于是注入器会返回 null
,parentModule
参数也就被赋成了空值,而构造函数没有任何异常。
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 { }
关于 NgModule 的更多知识
More on NgModules
你还可能对下列内容感兴趣:
You may also be interested in:
共享模块解释了本页中涉及的这些概念。
Sharing Modules, which elaborates on the concepts covered on this page.