同一个 NgRx 特性模块的独立实例
Independent instances of the same NgRx feature module
我正在使用 NgRx 5 开发一个 Angular 5 项目。到目前为止,我已经实现了一个框架应用程序和一个名为 "Search" 的功能模块,它在一种封装的方式(通过使用 forFeature
语法)。
此模块有一个根组件 (search-container
),它呈现一整棵子组件树 - 它们一起构成了搜索 UI 和功能,它具有复杂的状态模型和良好的动作和减速器的数量。
有强烈的要求说:
特性模块应该相互隔离导入,
根据消费者应用程序的要求。
同一功能的多个实例应在同一父级中共存(例如,具有单独上下文的单独选项卡)
实例不应具有共享的内部状态,但它们应该能够对全局状态中的相同变化做出反应。
所以我的问题是:
如何将多个 <search-container></search-container>
放在一起并确保它们独立运行?例如,我想在小部件的一个实例中分派搜索操作,而不是在所有小部件中看到相同的搜索结果。
非常感谢任何建议。谢谢!
我遇到了和你类似的问题,想出了以下方法来解决它。
重申您的要求只是为了确保我理解正确:
- 你有一个模块 "Search" 和自己的 components/state/reducer/actions 等等
- 您想重用该模块以拥有许多搜索选项卡,这些选项卡的外观和行为都相同
解决方案:利用操作的元数据
有了动作,就有了元数据的概念。基本上,除了 payload-属性,您在操作对象的顶层还有一个 meta-属性。这与 "have the same actions, but in different contexts" 的概念相得益彰。元数据 属性 将变为 "id"(以及更多内容,如果您需要的话)以区分要素实例。您在根状态中有一个 reducer,一次定义所有操作,元数据帮助 reducer/effects 知道调用了哪个 "sub-state"。
状态看起来像这样:
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,然后使用元数据初始化服务。这当然会使代码更加复杂。
工作示例
我正在使用 NgRx 5 开发一个 Angular 5 项目。到目前为止,我已经实现了一个框架应用程序和一个名为 "Search" 的功能模块,它在一种封装的方式(通过使用 forFeature
语法)。
此模块有一个根组件 (search-container
),它呈现一整棵子组件树 - 它们一起构成了搜索 UI 和功能,它具有复杂的状态模型和良好的动作和减速器的数量。
有强烈的要求说:
特性模块应该相互隔离导入, 根据消费者应用程序的要求。
同一功能的多个实例应在同一父级中共存(例如,具有单独上下文的单独选项卡)
实例不应具有共享的内部状态,但它们应该能够对全局状态中的相同变化做出反应。
所以我的问题是:
如何将多个 <search-container></search-container>
放在一起并确保它们独立运行?例如,我想在小部件的一个实例中分派搜索操作,而不是在所有小部件中看到相同的搜索结果。
非常感谢任何建议。谢谢!
我遇到了和你类似的问题,想出了以下方法来解决它。
重申您的要求只是为了确保我理解正确:
- 你有一个模块 "Search" 和自己的 components/state/reducer/actions 等等
- 您想重用该模块以拥有许多搜索选项卡,这些选项卡的外观和行为都相同
解决方案:利用操作的元数据
有了动作,就有了元数据的概念。基本上,除了 payload-属性,您在操作对象的顶层还有一个 meta-属性。这与 "have the same actions, but in different contexts" 的概念相得益彰。元数据 属性 将变为 "id"(以及更多内容,如果您需要的话)以区分要素实例。您在根状态中有一个 reducer,一次定义所有操作,元数据帮助 reducer/effects 知道调用了哪个 "sub-state"。
状态看起来像这样:
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,然后使用元数据初始化服务。这当然会使代码更加复杂。