代码之家  ›  专栏  ›  技术社区  ›  Stefan Orzu

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

  •  18
  • Stefan Orzu  · 技术社区  · 6 年前

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

    此模块有一个根组件( search-container )它呈现了整个子组件树,它们一起构成了搜索UI和功能,它有一个复杂的状态模型和大量的操作和简化。

    强烈要求如下:

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

    2. 同一功能的多个实例应在同一父级内共存(例如,具有单独上下文的单独选项卡)

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

    所以我的问题是:

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

    非常感谢您的任何建议。谢谢

    1 回复  |  直到 6 年前
        1
  •  16
  •   dummdidumm    6 年前

    我遇到了一个与你类似的问题,并想出了以下解决方法。

    重申您的要求,以确保我正确理解:

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

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

    对于操作,有元数据的概念。基本上,除了payload属性之外,在动作对象的顶层还有一个meta属性。这很好地体现了“有相同的动作,但在不同的环境中”的概念。然后,元数据属性将是“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
    });
    

    减速器处理方式如下:

    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提供了一次缩减器和可能的效果,并为每个功能(也称为搜索)提供了元数据配置:

    // 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