☰
NgRx ESLint 规则 no-effects-in-providers 深入解析:Effect 类为何不能重复注册进 providers
2026/9/26 10:15:43 网站建设 项目流程
  • 前端
  • 状态管理

【免费下载链接】platform

Reactive State for Angular

项目地址:https://gitcode.com/gh_mirrors/pl/platform
点击查看免费下载

本文基于 NgRx(Angular Reactive State 库,本项目即其官方 platform 仓库)自带的@ngrx/eslint-plugin展开,聚焦其中no-effects-in-providers这一条 ESLint 规则。该规则用于拦截“同一个 Effect 类既通过EffectsModule.forRoot()/forFeature()注册,又出现在@NgModule的providers数组中”的重复注册问题。读完本文,你将掌握该规则的完整语义、自动修复行为、底层实现原理,以及如何在项目中正确注册 NgRx Effects。

规则概览

no-effects-in-providers是@ngrx/eslint-plugin中 effects 模块的一组规则之一,其核心含义是:

Effectshould not be listed as a provider if it is added to theEffectsModule.

即:如果一个 Effect 类已经通过EffectsModule.forRoot()或EffectsModule.forFeature()注册,就不应再把它写进 Angular 的providers数组。

规则属性如下(与文档头一致,并由源码 no-effects-in-providers.ts 中的meta定义相互印证):

属性值含义
Typeproblem属于“问题”类规则,报告的是可能引发运行时异常的代码缺陷
FixableYes支持 ESLint 自动修复(--fix),可安全移除冗余的 provider 声明
SuggestionNo不提供手动修复建议(suggestion),只走自动修复通道
Requires type checkingNo纯语法(AST)层面即可完成判断,无需 TypeScript 类型信息
ConfigurableNo不可配置,schema: [],无任何额外选项
所属模块effects源码中docs.ngrxModule: 'effects'

Rule Details:为什么 Effect 不能进 providers

文档明确给出了规则的设计意图:一个 Effect 类只应通过EffectsModule的forRoot()/forFeature()方法注册,不应通过把 Effect 类加入 Angularproviders数组的方式注册。

这一设计背后有清晰的源码依据。查看 effects_module.ts 可以发现,EffectsModule.forRoot()与forFeature()在内部已经把传入的 effect 类注册为了 providers:

static forFeature(...featureEffects): ModuleWithProviders<EffectsFeatureModule> { const effects = featureEffects.flat(); const effectsClasses = getClasses(effects); return { ngModule: EffectsFeatureModule, providers: [ effectsClasses, // ← 这里已经注册了 Effect 类 { provide: _FEATURE_EFFECTS, multi: true, useValue: effects }, { provide: USER_PROVIDED_EFFECTS, multi: true, useValue: [] }, { provide: _FEATURE_EFFECTS_INSTANCE_GROUPS, multi: true, useFactory: createEffectsInstances, deps: [_FEATURE_EFFECTS, USER_PROVIDED_EFFECTS], }, ], }; }

forRoot()的实现也完全对称(providers: [effectsClasses, ...])。也就是说:

  • 通过EffectsModule.forRoot([CustomersEffect])/forFeature([CustomersEffect])传入的 Effect 类,已经被 NgRx 作为 provider 注入,并交由createEffectsInstances工厂统一创建实例、接入 Effects 运行链路;
  • 如果开发者再次把同一个类放进@NgModule的providers,就会造成重复实例化:同一种行为会被注册两次、副作用可能被重复订阅,导致难以排查的运行时问题。

从源码结构可以推断,这正是规则将其归类为problem而非suggestion的原因——它拦截的是明确的反模式,而非风格偏好。

错误写法(incorrect)

以下写法都是该规则要报告的目标。核心特征:同一个 Effect 类同时出现在imports(EffectsModule 调用)与providers中。

与 forRoot 搭配的错误示例

@NgModule({ imports: [EffectsModule.forRoot([CustomersEffect])], providers: [CustomersEffect], // ❌ no-effects-in-providers }) export class AppModule {}

与 forFeature 搭配的错误示例

@NgModule({ imports: [EffectsModule.forFeature([CustomersEffect])], providers: [CustomersEffect], // ❌ no-effects-in-providers }) export class CustomersModule {}

正确写法(correct)

规则并不禁止 Effect 类出现在providers中——它只禁止已通过 EffectsModule 注册的类再次进入 providers。正确做法是把注册职责完全交给EffectsModule。

与 forRoot 搭配的正确示例

@NgModule({ imports: [EffectsModule.forRoot([CustomersEffect])], }) export class AppModule {}

与 forFeature 搭配的正确示例

@NgModule({ imports: [EffectsModule.forFeature([CustomersEffect])], }) export class CustomersModule {}

需要补充说明的是:未通过EffectsModule注册的 Effect 类放在providers里并不会触发此规则(详见下文测试用例),因为此时不存在重复注册问题。

自动修复行为:安全移除,连逗号一起清理

规则声明为Fixable: Yes,其修复逻辑位于源码的fix回调中,核心工具函数是 getNodeToCommaRemoveFix:

export function getNodeToCommaRemoveFix(sourceCode, fixer, node) { const nextToken = sourceCode.getTokenAfter(node); const isNextTokenComma = nextToken && ASTUtils.isCommaToken(nextToken); return [ fixer.remove(node), ...(isNextTokenComma ? [fixer.remove(nextToken)] : []), ]; }

修复策略非常直观且安全:

  1. 移除被报告的 Effect 标识符节点本身;
  2. 检查它后面的下一个 token是否为逗号,若是则一并移除,从而保证providers数组的语法与格式依旧合法。

因此,对下面的代码执行eslint --fix(或编辑器内“快速修复”):

@NgModule({ imports: [EffectsModule.forFeature([RegisteredEffect])], providers: [RegisteredEffect] }) export class AppModule {}

会被自动修复为:

@NgModule({ imports: [EffectsModule.forFeature([RegisteredEffect])], providers: [] }) export class AppModule {}

测试用例 no-effects-in-providers.spec.ts 中的第一条 invalid 用例即验证了该输出。同时,由于修复只操作被报告的节点及其后的逗号,与被移除元素相邻的注释会得到保留——例如行内注释// Let's see what happens with this comment?在修复后依然留在原位置(见该 spec 中第二条用例),块注释/* Deprecated effect */同样不被误删。

规则如何工作:基于 AST 选择器的实现原理

规则的实现非常轻巧,完全依赖@typescript-eslint/utils提供的类型化 AST 与选择器机制。完整逻辑见 no-effects-in-providers.ts,可拆解为三步:

第一步:两条路径分别收集

规则内部维护两个集合:

const effectsInProviders = new Set<TSESTree.Identifier>(); const effectsInImports = new Set<string>();
  • 命中effectsInNgModuleProviders选择器时,把providers数组里的每个标识符节点加入effectsInProviders;
  • 命中effectsInNgModuleImports选择器时,把EffectsModule.forRoot/forFeature([...])数组里的标识符名称加入effectsInImports。

第二步:选择器只匹配“Effect 注册调用”

这两个选择器定义在 selectors/index.ts:

export const ngModuleDecorator = `ClassDeclaration > Decorator > CallExpression[callee.name='NgModule']`; export const ngModuleImports = `${ngModuleDecorator} ObjectExpression ${metadataProperty('imports')} > ArrayExpression`; export const effectsInNgModuleImports = `${ngModuleImports} CallExpression[callee.object.name='EffectsModule'][callee.property.name=/^for(Root|Feature)$/] ArrayExpression > Identifier`; export const effectsInNgModuleProviders = `${ngModuleProviders} Identifier`;

要点在于effectsInNgModuleImports的正则/^for(Root|Feature)$/——它只匹配EffectsModule.forRoot(...)与EffectsModule.forFeature(...)这两种注册调用,而StoreModule.forFeature(...)等其他模块方法会被自动排除,不会干扰判断。这也是“Requires type checking: No”得以成立的原因:全部判断都能在 AST 结构层面完成。

第三步:在 NgModule 装饰器退出时比对并报告

[`${ngModuleDecorator}:exit`]() { for (const effectInProvider of effectsInProviders) { if (!effectsInImports.has(effectInProvider.name)) continue; context.report({ node: effectInProvider, messageId, fix: ... }); } effectsInImports.clear(); effectsInProviders.clear(); }

规则选择在NgModule装饰器解析结束时(:exit)统一比对:只有“既出现在 providers、又出现在 EffectsModule 注册列表”的标识符才会被报告。每次:exit后清空两个集合,保证不同@NgModule之间的状态互不污染。

边界情况与测试验证

测试文件 no-effects-in-providers.spec.ts 通过ruleTester覆盖了多条重要边界,可作为理解规则语义的权威依据:

  1. 未注册的 Effect 放 providers 是合法的(valid 用例):providers: [FeatEffectTwo, UnRegisteredEffect, FeatEffectThree, RootEffectTwo]中没有任何一个出现在imports的EffectsModule.forRoot/forFeature调用里,因此不报告。这与“规则只针对重复注册”的定位完全一致。

  2. 多种 providers 写法均可识别:测试覆盖了普通providers、计算属性形式['providers']、模板字符串形式[`providers`],选择器都能正确命中。

  3. 多个 Effect 同时命中:forRoot([RootEffectOne, RootEffectTwo])与多个forFeature([...])并存时,凡重复出现在 providers 中的类(如FeatEffectTwo、FeatEffectThree、RootEffectTwo)会被逐条报告并修复,未注册的UnRegisteredEffect则被保留。

  4. 多模块隔离:spec 中AppModule与SharedModule各自独立判断,AppModule 的修复不影响 SharedModule,反之亦然——这正是:exit后清空集合的设计收益。

  5. 修复保持注释完整:行内注释与块注释在自动修复后保留,避免破坏开发者写的说明性注释。

启用与使用方式

该规则不可配置(Configurable: No,schema: []),属于 effects 模块规则。启用方式为在 ESLint 配置中通过@ngrx/eslint-plugin的预设配置或直接引用规则名来开启,例如在.eslintrc或eslint.config中声明@ngrx/no-effects-in-providers。从源码结构看,configs 目录按模块提供预设,effects 相关规则会被统一聚合到该插件的 effects 配置集中。

在本地开发时,可以结合pnpm工作区运行该模块的测试以观察规则行为(对应 spec 文件位于 spec/rules/effects)。对存量代码,直接对@NgModule声明运行eslint --fix即可批量清理重复注册。

进一步阅读

  • EffectsModule 源码实现:查看forRoot/forFeature内部如何注册 effect 类并创建实例;
  • 规则源码:查看meta、选择器监听与修复回调的完整实现;
  • 规则测试用例:覆盖错误/正确/修复/多模块等全部边界场景;
  • 选择器工具定义:理解effectsInNgModuleImports、effectsInNgModuleProviders等选择器的匹配规则;
  • Effects 使用指南:了解 Effects 的标准注册与组织方式。
  • 前端
  • 状态管理

【免费下载链接】platform

Reactive State for Angular

项目地址:https://gitcode.com/gh_mirrors/pl/platform
点击查看免费下载
上一篇:终极指南:Deep-Live-Cam实时AI换脸实战,3步实现影视级视频处理
下一篇:macOS文件预览终极增强:QuickLook插件完全安装指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询