当可以使用 ngrx 在多个选项卡(在应用程序级别)中打开功能时,如何隔离功能状态?



我必须使用ngrx(要求)构建一个应用程序,并具有以下要求:

  • 多种功能,我们称它们为 A、B 和 C。
  • 应用程序级别的选项卡。选项卡包含功能,并且对可以打开的选项卡数量没有限制。
  • 可以根据用户需要在任意数量的选项卡中打开功能。
  • 选项卡
  • 状态必须与其他选项卡隔离,即使它包含相同的功能。

最后一点是我被困住了,我为此严重失眠,我想找回我的生活。有没有人知道如何做这样的事情?

到目前为止,我所做的是构建一个示例应用程序。经过很多天的混乱,我有一个解决方案可以工作,但我觉得维护起来会非常糟糕。我的解决方案是:

  • 将包含在选项卡中的功能必须具有状态(例如FeatureAState)
  • 此状态将存储在此功能的状态映射中(例如FeatureAStates),键是该状态所属选项卡的 ID。
  • 选项卡
  • 导航完成后,我必须在所有功能中侦听此操作以更新其状态以了解当前选项卡。
  • 然后,我们使用选择器获取当前活动选项卡的功能状态。

但是我对解决方案的问题是:

1)每个功能都必须意识到它们在选项卡中的事实,并且我必须在每个功能中复制选定的TabId(并且不要忘记在用户每次导航选项卡时更新它)。 2)所有的子特征必须包装在一个states: {[tabIdOfThisState: string]: SubFeatureState}中,这使得访问属性变得痛苦。

如何改进此解决方案?

您可以在下面找到代码的外观。

tabs.actions.ts

// Tab is an interface with an id, a matching url and a label.
export const addNewTab = createAction('[Tabs] Add New Tab', props<{ tab: Tab}>());
export const navigateToTab = createAction('[Tabs] Navigate To Tab', props<{ tab: Tab }>());
export const closeTab =  = createAction('[Tabs] Close Tab', props<{ tab: Tab }>());
export const all = union({ addNewTab, navigateToTab });
export type TabsActionsUnion = typeof all;

tabs.reducer.ts

export interface State extends EntityState<Tab> { 
selectedTabId: string;
}
export const adapter: EntityAdapter<Tab> = createEntityAdapter<Tab>({
selectId: (tab: Tab) => tab.linkOrId,
});
export const initialState: State = adapter.getInitialState({ selectedTabId: undefined });
export function reducer(state = initialState, action: TabsActionsUnion) {
switch (action.type) {
case addNewTab.type: {
return adapter.addOne(action.tab, state);
}
case navigateToTab.type: {
return { ...state, selectedTabId: action.tab.id };
}
case closeTab.type: {
// (todo): should handle the case where the tab being removed is the selected one
return adapter.removeOne(action.tab.id, state);
}
default: {
return state;
}
}
}
export const getSelectedTabId = (state: State) => state.selectedTabId;

然后我有我的功能 A。我的功能 A 处理多个子功能,在我的示例应用程序中,我有一个名为Counter的子功能。代码如下所示:

counter.actions.ts

export const increaseCounter = createAction('[Counter] Increase Counter', props<{ tabId }>());
export const decreaseCounter = createAction('[Counter] Decrease Counter');
const all = union({ initializeCounter, increaseCounter, decreaseCounter });
export type CounterActions = typeof all;

counter.reducer.ts

export interface CounterState {
value: number; 
}
// this is the list of the different states, one per tab where the counter feature is used... 
export interface CounterStates {
states: { [id: string]: CounterState };
activeTabId: string; // this is the selected tab id, this is always the same one as the selectedTabId in the Tabs feature 
}
const initialStates = { states: {}, activeTabId: undefined };
const initialState = { value: 0 };
export function reducer(state: CounterStates = initialStates, action: CounterActions | TabsActionsUnion) {
switch (action.type) {
// when we add a new tab we need to initialize a new counter state for this tab. 
// this also means that we cannot lazy load the store, because otherwise 
// this reducer would not be initialized on the first addNewTab
case addNewTab.type: {
return {
...state,
states: { ...state.states, [action.tab.id]: initialState },
activeTabId: action.tab.id
};
}
// we have to duplicate the activeTabId here because the reducer cannot access the
// state of another feature...
case navigateToTab.type: {
return {
...state,
activeTabId: action.tab.id
};
}
// updating the value is painful because we need to make sure we modify only the right counter state...
case increaseCounter.type: {
return {
...state,
states: {
...state.states,
[action.tabId]: { ...state.states[action.tabId], value: state.states[action.tabId].value + 1 }
}
};
}
case decreaseCounter.type: {
return {
...state,
states: {
...state.states,
[state.activeTabId]: { ...state.states[state.activeTabId], value: state.states[state.activeTabId].value - 1 }
}
};
}
default: {
return state;
}
}
}
// selectors are ok to work with, this one I'm happy with
export const getValue = (state: CounterStates) => state.states[state.activeTabId].value;

以及使用Counter的整个功能的化简器:

index.ts

export interface FeatureAState {
counter: fromCounter.CounterStates;
}
export interface State extends fromRoot.State {
featureA: FeatureAState;
}
export const reducers = combineReducers({ counter: fromCounter.reducer });
export const getFeatureAStateState = createFeatureSelector<State, FeatureAStateState>('featureAState');
export const getFeatureAStateCounterState = createSelector(
getFeatureAStateState,
state => state.counter
);
export const getCounterValue = createSelector(
getFeatureAStateState,
fromCounter.getValue
);

我相信您使用的是"the"正确的解决方案。您必须维护每个选项卡的状态,这只能通过保留对其 id 的引用来完成,这就是您正在做的事情。

我相信为活动选项卡 ID(通过 url)使用选择器是一个干净的解决方案 👍

最新更新