发布于 2026-01-06 3 阅读
0

保护你的 Angular 模块 💂‍

保护你的 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 }],
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

上面的代码片段创建了一个Theme带有themeConfig参数的对象,其Theme外观如下所示。

@Injectable()
export class Theme {
  constructor(config: ThemeConfig) {}
}
Enter fullscreen mode Exit fullscreen mode

要使用它,ThemeModule我们可以使用forRoot()函数AppModule

@NgModule({
  imports: [ThemeModule.forRoot({ color: '#dd0031' })],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

我们的ThemeModule主题现在可以使用了,但如果第二次导入,就会创建该主题的第二个实例。
这会导致功能模块加载时,该模块将使用第二个实例主题,而不是根主题。

只需三个简单步骤即可预防这种情况

1. 创建一个InjectionToken

export const THEME_ROOT_GUARD = new InjectionToken<void>(
  'Internal Theme ForRoot Guard',
)
Enter fullscreen mode Exit fullscreen mode

首先,我们需要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'
}
Enter fullscreen mode Exit fullscreen mode

通过工厂函数createThemeRootGuard,我们可以为令牌提供一个值THEME_ROOT_GUARD
该工厂函数需要一个theme参数来检查主题是否已创建,如果已创建,则会抛出错误。
我们deps可以提供该参数Theme,但第一次调用该函数时,主题Theme尚未初始化,因此我们使用 ` Optional@Optional` 将该参数标记为可选,否则依赖注入容器会抛出错误,因为它找不到该参数的值Theme
为了避免实例化主题,Theme我们可以使用SkipSelf`@None`。如果不这样做,最终会直接得到两个主题实例Theme

更多信息请访问angular.ioOptional SkipSelf

3. 将守卫令牌注入模块

@NgModule()
export class ThemeModule {
  constructor(
    @Optional()
    @Inject(THEME_ROOT_GUARD)
    guard: any,
  ) {}
}
Enter fullscreen mode Exit fullscreen mode

我们在模块导入时注入该函数以创建实例。InjectionToken每次ThemeModule创建 实例时,都会调用工厂函数来创建实例。第二次调用时,参数将具有值,因此会导致异常。THEME_ROOT_GUARD
ThemeModulecreateThemeRootGuardTHEME_ROOT_GUARDtheme

这样做可以避免开发者ThemeModule通过函数意外地多次导入我们的模块forRoot()
我们明确地表明我们不希望这种情况发生,这样如果模块使用不当,也能为开发者节省时间和精力。

第二个解决方案,但有所不同

还有第二种方法,可以将模块视为单例,这种方法设置起来更简单,但行为略有不同。
如果我们在模块ThemeModule内部注入它,就可以简单地检查它ThemeModule是否已经初始化。

@NgModule()
export class ThemeModule {
  constructor(@Optional() @SkipSelf() themeModule: ThemeModule) {
    if (themeModule) {
      throw new TypeError(`ThemeModule is imported twice.`)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

有关此解决方案的更多信息,请参阅Angular 文档。

区别

ThemeModule第二种方法的缺点是,如果我们想添加第二次加载的功能,例如使用一个forChild函数,这将抛出同样的错误。

这是因为我们在模块的构造函数中设置了守卫。相比之下,第一个解决方案是在createThemeRootGuard()函数内部设置守卫,而该函数仅在函数内部提供forRoot(),这意味着只有当我们使用 `import` 语句导入模块时才会进行检查forRoot()

根据您的使用场景,您可以选择最适合您需求的解决方案。

例如,您可以查看Angular Router的实现。


请在推特上关注我:@tim_deschryver

文章来源:https://dev.to/angular/guarding-your-angular-modules-lio