You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将NgRx Actions、Effects、Selectors与Reducer封装到类中是否合理?

问题:NgRx Store组件单类封装的可扩展性与维护性疑问

我正在学习NgRx,发现教程、文档和各类文章里,通常会把Store相关组件拆分到不同的ts文件中(比如Actions单独一个文件,Effects作为类放在独立文件等),和官方文档的做法一致。

但我担心这种拆分方式在大型应用里会产生过多文件夹,导致难以追踪所有Store组件,也不容易理清哪个Effect或Reducer对应哪个Action。我知道有DevTools可以可视化Store中的Action,但更关注Angular项目里Store组件的组织方式。

所以想请教:把所有Store组件封装到一个类中以便追踪,这种做法会不会随着应用规模增长,导致Store难以扩展和维护?以下是我的示例代码:

import { Actions, createEffect, ofType } from "@ngrx/effects";
import { createAction, Store, props, createReducer, on } from "@ngrx/store";
import { Person } from "./person.model";
import { PersonService } from "./person.service";
import { map, mergeMap } from "rxjs/operators";
import { Observable } from "rxjs";
import { Injectable } from "@angular/core";

@Injectable()
export class PersonStore {

  // Selectors
  get hasPeople$(): Observable<boolean> {
    return this.store.select(({ person }) => person.people.length > 0);
  }

  get people$(): Observable<Person[]> {
    return this.store.select(({ person }) => person.people);
  }

  constructor(
    private readonly store: Store<{ person: PersonState }>,
    private readonly actions$: Actions,
    private readonly personService: PersonService
  ) {
  }

  initializePeople(): void {
    this.store.dispatch(PersonStore.getPeopleAction());
  }

  deletePersonById(id: number): void {
    this.store.dispatch(PersonStore.deletePersonByIdAction({ id }));
  }

  //--- Actions, reducer, and effects
  private static readonly source: string = '[Person store]';

  // Actions
  private static readonly getPeopleAction = createAction(`${PersonStore.source} Get people`);
  private static readonly setPeopleAction = createAction(
    `${PersonStore.source} Set people`, 
    props<{ people: Person[] }>()
  );

  private static readonly deletePersonByIdAction = createAction(
    `${PersonStore.source} Delete person by ID`,
    props<{ id: number }>()
  );

  private static readonly removePersonByIdAction = createAction(
    `${PersonStore.source} Remove person from state by ID`,
    props<{ id: number }>()
  );

  // Reducer
  static get reducer() {
    return createReducer(
      initialPersonState,
      on(PersonStore.setPeopleAction, (_ , { people }) => ({ people })),
      on(PersonStore.removePersonByIdAction, (state, { id }) => ({ ...state, people: state.people.filter(x => x.id !== id) }))
    );
  }

  // Effects
  private readonly initializePeople$ = createEffect(() => this.actions$.pipe(
    ofType(PersonStore.getPeopleAction),
    mergeMap(() => this.personService.getPeople().pipe(
      map(people => PersonStore.setPeopleAction({ people }))
    ))
  ));

  private readonly deletePersonById$ = createEffect(() => this.actions$.pipe(
    ofType(PersonStore.deletePersonByIdAction),
    mergeMap(action => this.personService.deletePersonBy(action.id).pipe(
      map(id => PersonStore.removePersonByIdAction({ id }))
    ))
  ));
}

interface PersonState {
  people: Person[];
}

const initialPersonState: PersonState = {
  people: []
};

在组件中我只需注入PersonStore并这样使用:

// Set selector observable to my component property
ngOnInit(): void {
  this.people$ = this.personStore.people$.pipe(/* do some transformation here */);
}

// Delete a person
deletePersonById(id: number): void {
  this.personStore.deletePersonById(id);
}

回答

短期优势

这种单类封装的方式在小型模块或初期开发阶段确实有明显优势:

  • 所有相关逻辑集中在一个文件,不用在多个文件间切换,对单个实体的CRUD操作追踪非常直观
  • 对外暴露的API统一(比如PersonStore类的方法),组件使用时不用导入多个Action、Selector,降低了组件层的认知负担

长期隐患(应用规模增长后)

但当应用规模扩大、模块逻辑变得复杂时,这种方式会逐渐暴露出维护问题:

  1. 文件体积膨胀:当一个实体的业务逻辑变多(比如新增批量操作、状态校验、多数据源同步等),单个文件会变得非常冗长,几百甚至上千行代码,定位特定逻辑变得困难
  2. 职责模糊:Action、Reducer、Effect、Selector本是单一职责的模块,强行揉在一个类里会打破单一职责原则,导致类的职责过重,修改一处逻辑可能影响其他部分
  3. 复用性差:如果其他模块需要复用某个Action或Selector,必须导入整个PersonStore类,无法做到按需导入,增加不必要的依赖
  4. 协作冲突:多人协作开发同一模块时,单文件的合并冲突概率远高于拆分后的多文件,影响开发效率
  5. 难以遵循NgRx最佳实践:NgRx生态的很多工具(比如代码生成器、lint规则)都是基于官方推荐的拆分结构设计的,单类封装会导致这些工具无法正常使用

折中方案

如果你不想用官方的多文件拆分,但又担心单类的问题,可以尝试按逻辑拆分到同一目录下的多个文件,但通过barrel文件统一导出:

  • 比如在person/目录下创建actions.ts、reducer.ts、effects.ts、selectors.ts
  • 再创建index.ts作为barrel文件,统一导出这些模块
  • 组件层只需导入person/index.ts中的内容,既保持了逻辑拆分的清晰,又简化了导入路径

内容的提问来源于stack exchange,提问作者ponki.d.monkey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 19:40:29