forRoot()的目的是在应用程序中使用单例服务。
forRoot()的意义是仅有一个由ModuleWithProviders导出的service实例。
如果没有forRoot()方法,如果你在模块的providers里面添加一个service并在多处用到这个module,那么你在这个应用的不同级都会实例化这个service。用forRoot(),它会创建这个service的新实例
forRoot(),我们最熟悉的莫过于路由的调用了
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ path: '', redirectTo: '/index', pathMatch: 'full' }
];
@NgModule({
imports: [
RouterModule.forRoot(routes)
],
...
})
export class AppModule { }
这种写法也出现在ngx-bootstrap中,并且以前在angular Material中也这么使用。
该调用必须注册在应用的根模块上。
对于初学者来说,一般的疑问是:forRoot()到底返回什么呢?
通常情况下,这个方法的返回类型是一个符合 ModuleWithProviders interface的一个object。
interface ModuleWithProviders {
ngModule: Type
providers?: Provider[]
}
描述
A wrapper around a module that also includes the providers.
一个包裹着module的wrapper,并且内含一个providers
这个接口有一个导入的ngModule。
简单地说,forRoot()方法返回一个NgModule和他的provider依赖项。
根NgModule的调用到底有什么用呢?也许没啥用。实际上,你也可以将它导入到非根NgModule中,它也起作用。
请看ngx-bootstrap中modalModule如何使用forRoot()
import { NgModule, ModuleWithProviders } from '@angular/core';
import { ModalBackdropComponent } from './modal-backdrop.component';
import { ModalDirective } from './modal.component';
import { PositioningService } from '../positioning';
import { ComponentLoaderFactory } from '../component-loader';
@NgModule({
declarations: [ModalBackdropComponent, ModalDirective],
exports: [ModalBackdropComponent, ModalDirective],
entryComponents: [ModalBackdropComponent]
})
export class ModalModule {
public static forRoot(): ModuleWithProviders {
return {ngModule: ModalModule, providers: [ComponentLoaderFactory, PositioningService]};
}
}
注意ModalModule没有在@NgModule装饰器中声明任何提供者,而是在static forRoot()方法中这样做。
根NgModule为什么重要?
尽管导入forRoot()方法的额外providers,理论上可用于子模块NgModules,但将其注册到应用程序的根目录中有许多帮助。
首先,考虑提供者的注入方式与组件和指令不同。
通常,在使用@Injectable装饰类并在NgModule中注册为提供者时,这个类创建一次,并且一个实例在整个应用程序中共享。当Angular引导根NgModule时,所有NgModule中的所有可用导入都会在当时注册,并可供整个应用程序使用 - 它们是全局的。
这就是为什么在子NgModule中被注册的providers在整个儿应用中都可见。
另一方面,组件和指令被多次实例化,每个实例在标记中被实例化一次。此外,这些项目的作用范围为导入它们的NgModule,以防止两个组件可能具有相同选择器的命名冲突,由于依赖注入(DI)行为有所不同,因此需要区分包含组件,指令和来自包含组件,指令和提供程序的ModuleWithProviders的NgModule,这对于forRoot()方法作出区分的位置很有帮助。
但是,依赖注入并不总是这么简单。在引导过程中,有时候所有应用程序的NgModule都不可用。延迟加载就是这样一个例子。在路由过程中延迟加载NgModule时,在启动过程中注册在延迟加载的NgModule及其子代中的提供程序不可用,并且Angular无法在此时注册它们。因此,只有在加载路由时它们才会作为提供者添加,而且它们的范围是从延迟加载的NgModule及其子节点开始注入。如果有多个延迟加载的NgModules尝试注册相同的提供程序,则NgModule树中的每个节点都会以不同的实例结束。通过在根中导入提供程序,它有助于确保所有延迟加载的NgModule都获得提供程序的相同实例,并且也是为什么forRoot()被这样命名的原因。
何时使用forRoot
作为消费者,当库依赖需要时使用它。在应用程序的根目录下导入模块,并使用forRoot()方法进行注册以全局导入提供程序。在其他NgModules中,必要时使用适当的非根形式的导入来导入组件和指令。
在Angular Routing和ngx-bootstrap的情况下,这个约定有助于在指令和组件的多个实例之间共享提供程序,以实现应用程序的全局关注。例如,ngx-bootstrap将这个约定用于模态对话框组件。虽然在应用程序的标记中可能定义了许多模式实例,但模式会接管整个UI,因此对话框的所有实例都应了解它们如何影响此全局关注点。
在路由的情况下,应用程序只有一个window.location,所以即使可能有子路由和router-outlet组件的各种实例,它们都需要窗口位置的全局依赖关系,以便它们可以一起工作。
在这两种情况下,模态对话框或路由器的使用者不需要知道这些项目如何沟通和管理共同关心的问题。通过这种方式,forRoot()方法抽象出必要的提供者注册。
简而言之,当您有多个自定义组件或指令时,请考虑使用约定,这些组件或指令都依赖于全局UI关注点,并且需要一起工作才能管理此全局状态。要明确的是,forRoot()约定是一种耦合形式,只能在仔细考虑设计后才能使用。
最好避免在第三方库中使用这个约定。例如,不要试图用这个约定来冒泡应用程序的所有NgModule依赖关系。由forRoot()方法返回的提供程序应该是内部依赖项,它们只与ModuleWithProviders中包含的其他组件一起工作。第三方依赖关系应该像npm peerDependencies一样对待,并直接在根NgModule中导入并注册,以便其他组件可以更轻松地共享相同的依赖关系,并帮助确保所有使用者在npm版本化解决时引用相同的程序包版本。
小贴士
总之,forRoot()约定表示一种使用ModuleWithProviders接口导入NgModule及其提供程序的方法。
当功能NgModule导出需要共享相同定制提供程序实例的组件和指令时,可考虑使用forRoot()方法在根NgModule中注册这些提供程序。这有助于确保所有子级NgModules都可以访问相同的提供程序实例,而无需消费者明确处理提供程序注册。
当您为应用程序编写共享模块时,该模块包含您在同一应用程序的其他模块中使用的内容,通常希望该模块中的所有服务仅共享一次。
如果您只是将providers中的services放在共享模块的NgModule()中,那么您可能会遇到奇怪的情况,特别是在延迟加载时,感觉该服务有多个实例。如果你想拥有共享状态,这很糟糕。
所以,有更好的方法来编写你的共享模块:
import {NgModule, ModuleWithProviders} from '@angular/core';
import {CommonModule} from '@angular/common';
// ... other imports
export const providers = [
// ... your shared services here
];
@NgModule({
declarations: [...],
imports: [
CommonModule,
SomeLibraryModule,
...],
exports: [
SomeLibraryModule.forRoot()
...
]
})
export class SharedModule {
static forRoot() : ModuleWithProviders {
return {
ngModule: SharedModule,
providers: [...providers]
};
}
}
forRoot()模式/约定在库中很常见,你会在ng-bootstrap和其他的东西中看到它。该名称对于编译器/框架并不特别,但它是一种常见模式。
使用这些时,可以在imports部分导入模块本身(它可以为您提供任何被需要declarations ,诸如指令等),并且在导出中使用模块的forRoot()版本,以便它可用于共享模块的使用者。
然后在其他应用程序模块中,除了AppModule,您可以将SharedModule添加到其正常导入的NgModule中。
AppModule将是您添加SharedModule.forRoot()的唯一位置,如下所示:
@NgModule({
declarations: [...],
imports: [BrowserModule, SharedModule.forRoot(), ...],
...
})
export class AppModule {
}
RouterModule.forRoot
第一个静态方法是RouterModule.forRoot,用来在我们应用的主模块上定义根config。
这使我们的主模块能够访问所有的路由器指令(更多关于这些指令的内容),以及定义主配置
// ...
import { Routes, RouterModule } from '@angular/router';
export const ROUTES: Routes = [];
@NgModule({
imports: [BrowserModule, RouterModule.forRoot(ROUTES)],
// ...
})
export class AppModule {}
我们只需传入一组路由作为我们的配置,这将是一组描述配置的对象。通常情况下,将常数/变量传递到forRoot方法而不是直接传递给数组是一种很好的模式,以保持模块更加可见,并控制上面模块的输入,或者在单独的文件中控制外部模块 - 这样const ROUTES就被导出供其他地方使用。
官方解释
forRoot creates a module that contains all the directives, the given routes, and the router service itself.
forChild creates a module that contains all the directives and the given routes, but does not include the router service.
导入模块时,通常使用对模块类的一个引用:
@NgModule({
providers: [AService]
})
export class A {}
-----------------------------------
@NgModule({
imports: [A]
})
export class B
用这种方法 所有的providers注册在模块A上,被添加到根注入器,并可用于整个儿应用程序。
但是有另一种方式来注册一个带有providers的模块。
@NgModule({
providers: [AService]
})
class A {}
export const moduleWithProviders = {
ngModule: A,
providers: [AService]
};
----------------------
@NgModule({
imports: [moduleWithProviders]
})
export class B
这与前一个有相同的含义。
你可能知道懒加载的模块有自己的注入器。因此,假设您想注册AService以供整个应用程序使用,但是一些BService只能用于懒加载的模块。你可以像这样重构你的模块:
@NgModule({
providers: [AService]
})
class A {}
export const moduleWithProvidersForRoot = {
ngModule: A,
providers: [AService]
};
export const moduleWithProvidersForChild = {
ngModule: A,
providers: [BService]
};
------------------------------------------
@NgModule({
imports: [moduleWithProvidersForRoot]
})
export class B
// lazy loaded module
@NgModule({
imports: [moduleWithProvidersForChild]
})
export class C
现在,BService只能用于延迟加载的子模块,并且AService将可用于整个应用程序您可以将上述内容重写为导出的模块,如下所示:
@NgModule({
providers: [AService]
})
class A {
forRoot() {
return {
ngModule: A,
providers: [AService]
}
}
forChild() {
return {
ngModule: A,
providers: [BService]
}
}
}
--------------------------------------
@NgModule({
imports: [A.forRoot()]
})
export class B
// lazy loaded module
@NgModule({
imports: [A.forChild()]
})
export class C
这与RouterModule有什么关系?
假设它们都使用相同的token进行访问:
export const moduleWithProvidersForRoot = {
ngModule: A,
providers: [{provide: token, useClass: AService}]
};
export const moduleWithProvidersForChild = {
ngModule: A,
providers: [{provide: token, useClass: BService}]
};
通过单独的配置,当你从一个懒惰的加载模块请求令牌时,你将获得BService。
RouterModule使用ROUTES token 来获取特定于模块的所有路由,由于它希望特定于延迟加载模块的路由在该模块内可用(类似于我们的BService),因此它为子加载延迟模块使用了不同的配置:
static forChild(routes: Routes): ModuleWithProviders {
return {
ngModule: RouterModule,
providers: [{provide: ROUTES, multi: true, useValue: routes}}]
}
}
它们返回的都是ModuleWithProviders,也就是说你可以根据业务逻辑,不同情况向同一个模块传进去不同的providers,灵活使用。