保护你的 Angular 模块 💂
许多 Angular 模块需要通过静态forRoot()函数导入,通过该函数可以配置模块。
一个最广为人知的例子是 Angular Router 模块,它需要一组路由;另一个例子是 NgRx Store,它需要应用程序的根 reducer。
在内部,这些模块会初始化所需的服务,并设置这些服务之间的编排,从而完成其工作。
forRoot()但有时我们会犯这样的错误:在应用程序中多次使用同一个函数。我们可能不会注意到这种情况,但这往往会导致意想不到的行为,而这些行为往往难以调试。
之所以会出现这个问题,是因为这些模块必须被视为单例。
当一个模块被延迟加载并同时使用某个forRoot()函数时,延迟加载的模块会创建模块服务的第二个实例。这些实例会在它们被创建的上下文中使用。延迟加载的模块会使用它自己创建的实例,而不是使用根实例。
如果我们以forRoot()NgRx 的函数StoreModule为例来设置 NgRx Store,这意味着最终会生成多个 store 实例,每个实例的配置都不同。如果一个特性模块分发一个 action,它不会到达“真正的”根 reducer,而只会到达由该特性模块配置的 reducer。但如果同一个 action 是从根模块内部分发的,它就能到达“真正的”根 reducer。
第二个问题是,当 Angular 在启动应用程序时,同一个模块可能会被多次预加载。最终只会注册并使用一个模块,具体取决于模块的加载方式。如果您没有意识到这种情况,就可能导致模块配置错误。
就我个人而言,我会浪费很多时间来追踪和解决由此产生的问题,在这篇文章中,我们将探讨一种防止这种情况发生的解决方案。
一个例子
例如,假设我们有一个ThemeModule用于设置应用程序主题的模块。
应用程序的主题可以通过该模块设置ThemeModule.forRoot(color),并在项目启动时进行配置。
一段时间后,该模块ThemeModule被提取到一个库中,以便在多个应用程序中重用。该模块ThemeModule非常成功,被广泛用于多个代码库。突然,当我们导入另一个模块时,应用程序的主题颜色神奇地发生了变化,我们陷入了一场色彩混乱的狂欢,这一切都是因为我们没有保护该模块ThemeModule。
那么,我们该如何避免这种情况呢?
答案是根保护机制,在接下来的代码片段中,我们将看到如何设置它们。
请把代码给我看看。
让我们实现第ThemeModule一个目标,添加一个静态forRoot()函数来设置ThemeModule。
@NgModule()
export class ThemeModule {
static forRoot(themeConfig: ThemeConfig): ModuleWithProviders<ThemeModule> {
return {
ngModule: ThemeModule,
providers: [{ provide: Theme, useValue: themeConfig }],
}
}
}
上面的代码片段创建了一个Theme带有themeConfig参数的对象,其Theme外观如下所示。
@Injectable()
export class Theme {
constructor(config: ThemeConfig) {}
}
要使用它,ThemeModule我们可以使用forRoot()函数AppModule。
@NgModule({
imports: [ThemeModule.forRoot({ color: '#dd0031' })],
})
export class AppModule {}
我们的ThemeModule主题现在可以使用了,但如果第二次导入,就会创建该主题的第二个实例。
这会导致功能模块加载时,该模块将使用第二个实例主题,而不是根主题。
只需三个简单步骤即可预防这种情况
1. 创建一个InjectionToken
export const THEME_ROOT_GUARD = new InjectionToken<void>(
'Internal Theme ForRoot Guard',
)
首先,我们需要InjectionToken为我们的保安人员创建一个。
ROOT_MODULE_GUARD2.在模块提供者中提供
@NgModule()
export class ThemeModule {
static forRoot(themeOptions): ModuleWithProviders<ThemeRootModule> {
return {
ngModule: ThemeRootModule,
provides: [
{
provide: Theme,
useFactory: createTheme,
deps: [themeOptions],
},
{
provide: THEME_ROOT_GUARD,
useFactory: createThemeRootGuard,
deps: [[Theme, new Optional(), new SkipSelf()]],
},
],
}
}
}
export function createThemeRootGuard(theme) {
if (theme) {
throw new TypeError(
`ThemeModule.forRoot() called twice. Feature modules should use ThemeModule.forFeature() instead.`,
)
}
return 'guarded'
}
通过工厂函数createThemeRootGuard,我们可以为令牌提供一个值THEME_ROOT_GUARD。
该工厂函数需要一个theme参数来检查主题是否已创建,如果已创建,则会抛出错误。
我们deps可以提供该参数Theme,但第一次调用该函数时,主题Theme尚未初始化,因此我们使用 ` Optional@Optional` 将该参数标记为可选,否则依赖注入容器会抛出错误,因为它找不到该参数的值Theme。
为了避免实例化主题,Theme我们可以使用SkipSelf`@None`。如果不这样做,最终会直接得到两个主题实例Theme。
更多信息请访问angular.io。Optional SkipSelf
3. 将守卫令牌注入模块
@NgModule()
export class ThemeModule {
constructor(
@Optional()
@Inject(THEME_ROOT_GUARD)
guard: any,
) {}
}
我们在模块导入时注入该函数以创建实例。InjectionToken每次ThemeModule创建新 实例时,都会调用工厂函数来创建实例。第二次调用时,参数将具有值,因此会导致异常。THEME_ROOT_GUARDThemeModulecreateThemeRootGuardTHEME_ROOT_GUARDtheme
这样做可以避免开发者ThemeModule通过函数意外地多次导入我们的模块forRoot()。
我们明确地表明我们不希望这种情况发生,这样如果模块使用不当,也能为开发者节省时间和精力。
第二个解决方案,但有所不同
还有第二种方法,可以将模块视为单例,这种方法设置起来更简单,但行为略有不同。
如果我们在模块ThemeModule内部注入它,就可以简单地检查它ThemeModule是否已经初始化。
@NgModule()
export class ThemeModule {
constructor(@Optional() @SkipSelf() themeModule: ThemeModule) {
if (themeModule) {
throw new TypeError(`ThemeModule is imported twice.`)
}
}
}
有关此解决方案的更多信息,请参阅Angular 文档。
区别
ThemeModule第二种方法的缺点是,如果我们想添加第二次加载的功能,例如使用一个forChild函数,这将抛出同样的错误。
这是因为我们在模块的构造函数中设置了守卫。相比之下,第一个解决方案是在createThemeRootGuard()函数内部设置守卫,而该函数仅在函数内部提供forRoot(),这意味着只有当我们使用 `import` 语句导入模块时才会进行检查forRoot()。
根据您的使用场景,您可以选择最适合您需求的解决方案。
例如,您可以查看Angular Router的实现。
请在推特上关注我:@tim_deschryver。
文章来源:https://dev.to/angular/guarding-your-angular-modules-lio