相同 NgRx 功能模块的独立实例

Posted

技术标签:

【中文标题】相同 NgRx 功能模块的独立实例【英文标题】:Independent instances of the same NgRx feature module 【发布时间】:2018-09-07 07:40:20 【问题描述】:

我正在使用 NgRx 5 开发一个 Angular 5 项目。到目前为止,我已经实现了一个骨架应用程序和一个名为“搜索”的功能模块,它以封装的方式处理自己的状态、动作和减速器(通过使用 @ 987654321@语法)。

该模块有一个根组件 (search-container),它呈现一整棵子组件树 - 它们共同构成了搜索 UI 和功能,具有复杂的状态模型以及大量的动作和减速器。

有强烈的要求说:

    功能模块应该相互隔离导入, 根据消费者应用程序的要求。

    同一功能的多个实例应共存于同一父项中(例如,具有单独上下文的单独选项卡)

    实例不应具有共享的内部状态,但它们应该能够对全局状态中的相同更改做出反应。

所以我的问题是:

如何将多个<search-container></search-container> 放在一起并确保它们独立运行?例如,我想在一个小部件实例中调度搜索操作,而不是在所有小部件中看到相同的搜索结果。

非常感谢任何建议。谢谢!

【问题讨论】:

找到解决办法了吗? @ParthGhiya 不幸的是没有。我所做的是在创建时为每个容器分配 ID。因此,一个特性的状态看起来就像一个 id -> containerState 的映射。处理这些会增加很多额外的复杂性,例如为每个容器的子组件集提供正确的 id、调度 id 感知操作、装饰 reducer 以修改容器状态以及使用动态生成的选择器,因为您不能将容器 id 作为参数传递到 ngrx 选择器。我最终围绕容器管理编写了一个完整的元框架:( 【参考方案1】:

我遇到了与您类似的问题,并想出了以下方法来解决它。

重申您的要求只是为了确保我正确理解它们:

您有一个“搜索”模块,其中包含自己的组件/状态/reducer/动作等。 您希望重用该模块以拥有许多外观和行为相同的搜索选项卡

解决方案:利用动作的元数据

对于动作,有元数据的概念。基本上,除了有效负载属性之外,您在操作对象的顶层还有一个元属性。这与“具有相同的动作,但在不同的上下文中”的概念非常吻合。然后元数据属性将是“id”(如果需要,还有更多内容)以区分功能实例。你的根状态中有一个 reducer,定义所有动作一次,元数据帮助 reducer/effects 知道调用了哪个“子状态”。

状态如下:

export interface SearchStates 
  [searchStateId: string]: SearchState;


export interface SearchState 
  results: string;

一个动作如下所示:

export interface SearchMetadata 
  id: string;


export const search = (params: string, meta: SearchMetadata) => (
  type: 'SEARCH',
  payload: params,
  meta
);

reducer 是这样处理的:

export const searchReducer = (state: SearchStates = , action: any) => 
  switch (action.type) 
    case 'SEARCH':
      const id = action.meta.id;
      state = createStateIfDoesntExist(state, id);
      return 
        ...state,
        [id]: 
          ...state[id],
          results: action.payload
        
      ;
  
  return state;
;

您的模块为 root 提供了一次 reducer 和可能的效果,并且您为每个功能(也称为搜索)提供了带有元数据的配置:

// provide this inside your root module
@NgModule(
  imports: [StoreModule.forFeature('searches', searchReducer)]
)
export class SearchModuleForRoot 


// use forFeature to provide this to your search modules
@NgModule(
  // ...
  declarations: [SearchContainerComponent]
)
export class SearchModule 
  static forFeature(config: SearchMetadata): ModuleWithProviders 
    return 
      ngModule: SearchModule,
      providers: [ provide: SEARCH_METADATA, useValue: config ]
    ;
  




@Component(
  // ...
)
export class SearchContainerComponent 

  constructor(@Inject(SEARCH_METADATA) private meta: SearchMetadata, private store: Store<any>) 

  search(params: string) 
    this.store.dispatch(search(params, this.meta);
  

如果您想从组件中隐藏元数据的复杂性,您可以将该逻辑移到服务中,然后在组件中使用该服务。您还可以在那里定义您的选择器。将服务添加到 forFeature 中的提供程序。

@Injectable()
export class SearchService 
  private selectSearchState = (state: RootState) =>
    state.searches[this.meta.id] || initialState;
  private selectSearchResults = createSelector(
    this.selectSearchState,
    selectResults
  );

  constructor(
    @Inject(SEARCH_METADATA) private meta: SearchMetadata,
    private store: Store<RootState>
  ) 

  getResults$() 
    return this.store.select(this.selectSearchResults);
  

  search(params: string) 
    this.store.dispatch(search(params, this.meta));
  

在您的搜索标签模块中使用:

@NgModule(
  imports: [CommonModule, SearchModule.forFeature( id: 'searchTab1' )],
  declarations: []
)
export class SearchTab1Module 
// Now use <search-container></search-container> (once) where you need it

如果您的搜索选项卡看起来完全一样并且没有任何自定义,您甚至可以更改 SearchModule 以提供 searchContainer 作为路由:

export const routes: Route[] = [path: "", component: SearchContainerComponent];

@NgModule(
    imports: [
        RouterModule.forChild(routes)
    ]
    // rest stays the same
)
export class SearchModule 
 // ...



// and wire the tab to the root routes:

export const rootRoutes: Route[] = [
    // ...
    path: "searchTab1", loadChildren: "./path/to/searchtab1.module#SearchTab1Module"
]

然后,当您导航到 searchTab1 时,将呈现 SearchContainerComponent。

...但我想在单个模块中使用多个 SearchContainerComponents

您可以在组件级别应用相同的模式:

在 SearchService 启动时随机创建元数据 ID。 在 SearchContainerComponent 中提供 SearchService。 服务销毁时别忘了清理状态。

@Injectable()
export class SearchService implements OnDestroy 
  private meta: SearchMetadata = id: "search-" + Math.random()
// ....



@Component(
  // ...
  providers: [SearchService]
)
export class SearchContainerComponent implements OnInit 
// ...

如果您希望 ID 具有确定性,则必须在某处对其进行硬编码,然后例如将它们作为输入传递给 SearchContainerComponent,然后使用元数据初始化服务。这当然会使代码更复杂一些。

工作示例

每个模块: https://stackblitz.com/edit/angular-rs3rt8

每个组件: https://stackblitz.com/edit/angular-iepg5n

【讨论】:

感谢您提供详细的答案 - 这看起来与我最终实现的非常相似,但这里和那里有一些差异,但核心思想是相同的。因此,我很高兴将此答案标记为可行的解决方案。在调度动作并在 reducer 和效果中处理它们时,我已经围绕封装搜索元数据逻辑做了更多的工作,但它仍然感觉像很多样板。我希望在某个时候,ngrx 团队会为这个问题提供更简化的解决方案。 谢谢,这对我在项目中实现虚拟标签很有帮助

以上是关于相同 NgRx 功能模块的独立实例的主要内容,如果未能解决你的问题,请参考以下文章

Angular 7、Ngrx、Rxjs 6 - 访问延迟加载模块之间的状态

带有分页功能的 NgRx 存储

在 @ngrx/store 4.0 中提供 root reducer

[Bug]:版本更新后找不到模块“@ngrx/effects/testing”

NgRx 效果加载两​​次

如何从 ngrx 存储中获取数据并将其更新到 Observable 变量中?