问题
I'm trying to understand core module and singleton services in angular 4. The official documentation (https://angular.io/guide/ngmodule) says the following things:
UserService is an application-wide singleton. You don't want each module to have its own separate instance. Yet there is a real danger of that happening if the SharedModule provides the UserService.
CoreModule provides the UserService. Angular registers that provider with the app root injector, making a singleton instance of the UserService available to any component that needs it, whether that component is eagerly or lazily loaded.
We recommend collecting such single-use classes and hiding their details inside a CoreModule. A simplified root AppModule imports CoreModule in its capacity as orchestrator of the application as a whole.
import { CommonModule } from '@angular/common';
import { TitleComponent } from './title.component';
import { UserService } from './user.service';
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
@NgModule({
imports: [ CommonModule ],
declarations: [ TitleComponent ],
exports: [ TitleComponent ],
providers: [ UserService ]
})
export class CoreModule {
constructor (@Optional() @SkipSelf() parentModule: CoreModule) { ... }
}
So I'm using the Core Module providing singleton services, and the constructor
constructor (@Optional() @SkipSelf() parentModule: CoreModule) { ... }
prevent to import the Core Module more than one time.
1) BUT, what if I provide the UserService in another module (e.g in a lazy-loading module) ? This lazy-loaded module has a new instance of the service?
And about forRoot method:
@NgModule({
imports: [ CommonModule ],
providers: [ UserService ]
})
export class CoreModule {
}
static forRoot(config: UserServiceConfig): ModuleWithProviders {
return {
ngModule: CoreModule,
providers: [
{provide: UserServiceConfig, useValue: config }
]
};
}
}
2) If I import the CoreModule using CoreModule.forRoot() in the AppModule, what happen to the UserService ? Is it provided too?
Thanks
回答1:
The documentation is confusing, particularly this line:
UserService is an application-wide singleton. You don't want each module to have its own separate instance. Yet there is a real danger of that happening if the SharedModule provides the UserService.
There's no danger of that happening if you don't use lazy loaded modules. Let's see an example. You have A
module that imports B
module. Both modules define providers:
@NgModule({
providers: {provide: 'b', 'b'}
})
export class BModule {}
@NgModule({
imports: [AModule]
providers: {provide: 'a', 'a'}
})
export class AModule {}
What happens when the compiler generates a module factory is that it merges these providers together and factory only for one module will be created. Here is how it will look:
var AModuleNgFactory = jit_createNgModuleFactory0(
// reference to the module class
jit_AppModule1,
// array of bootstrap components
[jit_AppComponent2],
function (_l) {
return jit_moduleDef3([
// array of providers
jit_moduleProvideDef4(256, 'b', 'b', []),
jit_moduleProvideDef4(256, 'a', 'a', [])
...,
]);
You can see that the providers are merged. Now, if you define two modules with the same provider token, the modules will be merged and the providers from the module that imports the other will override the imported module providers:
@NgModule({
providers: {provide: 'a', 'b'}
})
export class BModule {}
@NgModule({
imports: [AModule]
providers: {provide: 'a', 'a'}
})
export class AModule {}
The factory definition will look like this now:
function (_l) {
return jit_moduleDef3([
// array of providers
jit_moduleProvideDef4(256, 'a', 'a', []),
...,
]);
So no matter how many modules you import, only one factory with merged providers is created. And only one root injector is created. The injector that components create is not "real" injector - check this answer to understand why.
This lazy-loaded module has a new instance of the service?
When it comes to lazy loaded modules, Angular generates separate factories for them. It means that providers defined in them are not merged into the main module injector. So if a lazy loaded module defines the provider with the same token, Angular will create new instance of that service even if there's already one in the main module injector.
If I import the CoreModule using CoreModule.forRoot() in the AppModule
To understand what forRoot
does, see RouterModule.forRoot(ROUTES) vs RouterModule.forChild(ROUTES).
回答2:
1) Yes. This relates to the fact that the dependency injector is hierarchical.
This means, every module has a set of elements that can be injected (module level), and if one of its elements requires a dependency which doesn't isnt present at a module level, then the dependency injector will look for the dependency in the parent of the module (the one module that imported it), and so on until it finds the dependency or reaches the root (app.module), where it will throw an error if the dependency cant be resolved (hierarchy level).
2) Yes, the UserService
will be provided. forRoot
will create a different "version" of the CoreModule
, in which the "normal" CoreModule is expanded with the extra properties that you add.
In most cases, forRoot
will take the "normal" version of the module and include the providers array, to ensure that services are singleton. The "normal" version of the module, will only have components, pipes or other non singletone elements.
Take as example the TranslateModule
of ngx-translate (extracted relevant section):
@NgModule({
declarations: [
TranslatePipe,
TranslateDirective
],
exports: [
TranslatePipe,
TranslateDirective
]
}) // this defines the normal version of the module
export class TranslateModule {
static forRoot(): ModuleWithProviders { // this kinda tells "module + providers"
return {
ngModule: TranslateModule, // take the normal version
providers: [ // merge this to the providers array of the normal version
TranslateStore,
TranslateService
]
};
}
}
Maybe this resource could be useful as further explanation: https://www.youtube.com/watch?v=8VLYjt81-fE
回答3:
Let me try to summarize what I think to have learned:
@NgModule({
imports: [ CommonModule ],
providers: [
UserService,
UserServiceConfig
]
})
export class CoreModule {
}
static forRoot(config: UserServiceConfig): ModuleWithProviders {
return {
ngModule: CoreModule,
providers: [
{provide: UserServiceConfig, useValue: config }
]
};
}
}
When I import the module using the forRoot method:
- UserService is also provided since (as you guys explained) the forRoot creates an extended version of this module (it merges the services)
- UserServiceConfig is provided using the config argument of the forRoot method.
And regarding application-wide singleton:
In order to have application-wide singleton services (even for lazy-loaded modules) I can use the forRoot method:
static forRoot(): ModuleWithProviders {
return {
ngModule: MyModule,
providers: [
MySingletonService
]
};
- the forRoot must be called only once in the appModule (so why the method is called "forRoot" by convention)
- if the module is imported in multiple modules, MySingletonService is not been provided (because it's provided only through the forRoot method)
BUT
If I create a CoreModule with a following special constructor, it prevents the module to be loaded more than once, so the services provided are application-wide singleton:
constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error(
'CoreModule is already loaded. Import it in the AppModule only');
}
}
So it makes sense to use forRoot method in a SharedModule, not in a module with a special constructor above.
So If I want application-wide (even for lazy-modules) services I see two options:
- Shared Module with forRoot method called in the appModule
- Core Module with special constructor and services provided normally, without a forRoot method
Any comments?
来源:https://stackoverflow.com/questions/45234694/angular-4-are-services-provided-in-a-core-module-singleton-for-real