在构建大型复杂应用程序时,Redux 提供了一种单向数据流机制来初始化、管理和响应应用程序状态的变更。Redux 的核心构建块,如 reducer 和组合 reducer,使得状态管理更加逻辑化,仿佛不同的 Flux 存储用于不同的状态。然而,当需要处理相同 reducer 的多个实例时,并没有一个标准的模式。例如,如果已经有了一个 Redux store,它与一个项目(域对象)的组合 reducer 一起工作,那么如何处理多个实例,比如一个条目或项目的数组呢?标准的分割/组合 reducer 在这里并没有帮助,因为状态会作为 props 数据传递给组件,而 reducer 本质上是一个处理动作以管理和变更状态的函数。
此外,由于当前的组合 reducer、动作和组件已经为单个实例工作并经过测试,最好在将它们放入集合时重用现有代码。通过两个步骤解决了这个问题。首先,将所有 React 组件与 store 形状解耦,使它们仅在“组件 props”的逻辑边界内操作。其次,在更高级别的 reducer 中管理一个状态集合,并应用相同的组合 reducer。
解耦步骤有助于在重新架构 store 形状时保持组件的完整性,而集合化步骤是利用现有的 reducer 和动作对特定选定实例的状态进行操作,而无需进行大量更改。这种两步方法的好处是,变更的范围被限制在更高级别的 reducer 中,所有组件都对 store 形状不敏感。当发展一个相当复杂和大型的应用程序架构时,这两个好处对于灵活性和可维护性至关重要,最终影响项目进度和质量。
背景:对多个实例的需求在项目的相当晚的阶段出现。应用程序是在没有同时打开/编辑多个域对象的概念下构建的。所有组件、动作、reducer 都是为单个域对象开发的,对象状态由组合 reducer 管理。现在需要在自己的标签页中打开不同的域对象,以便用户可以轻松地在它们之间切换。
发现 Redux Selector 模式是解耦步骤的一个很好的助手,它将关于状态形状的知识封装在与 reducer 共存的选择器中,这样组件就不依赖于状态结构,组件代码在重新塑造数据存储时不需要更改。
当当前选定的实例更改时,动作和组件将自动切换到选定的状态,无需运行重复且不断变化的逻辑来获取渲染状态,因为选择器本质上消除了对 store 形状的依赖。
有了选择器,可以简单地从根状态中移除单个实例的组合 reducer。当一个新的域对象被打开时,在创建一个新的标签页之后,组合 reducer 及其初始状态将与标签页状态关联。当动作被派发时,可以手动调用相同的组合 reducer 与当前选定的标签页状态。然后组件渲染,React 虚拟 DOM 更新 DOM,以标签页数组中选定对象的数据。
让看一些代码。使用状态选择器。假设 AEditor 是域对象组件,一个没有选择器的单个编辑器组件代码如下:
const AEditor = React.createClass({
componentWillMount() {
...
},
componentWillReceiveProps(nextProps) {
...
},
render() {
return (<lowerlevelcomponent {...this.props.editorstate} />);
}
});
export default connect(
state => ({editorState: state.editorContainer.editorState}),
dispatch => {actions: bindActionCreators(actions, dispatch)}
)(AEditor);
注意 editorState 直接从根状态检索,但组件只需要 editorState 来渲染。即使不需要多个 AEditor 实例,消除对状态形状的依赖也是更好的,这就是选择器发挥作用的地方。
这里是带有选择器的相同组件代码:
const AEditor = ...;
export default connect(
state => ({editorState: getEditorState(state)}),
dispatch => {actions: bindActionCreators(actions, dispatch)}
)(AEditor);
getEditorState 是选择器,它所做的就是将根状态作为输入,并返回 editorState。只有选择器知道存储形状,当当前选定的编辑器更改时,它将返回选定的编辑器状态。这就是可以在有一个对象数组时保持 AEditor 组件和相关动作不变的原因。
这里是根 reducer 中 getEditorState 的实现:
export const getEditorContainer = (state) => state.editorContainer;
export const getEditorState = (state) => fromEditor.getEditor(getEditorContainer(state));
这里没有什么特别的,这些选择器函数的工作是“选择”正确的状态传递给组件。对于多个状态实例,它们对于使当前动作与选定对象状态一起工作至关重要。
现在组件已经与存储形状解耦,可以继续创建编辑器状态的数组。
editorContainer 实际上是一个组合 reducer:
export const editorContainer = combineReducers({
editorState,
propertiesState,
outlineState,
categoryState,
dataPaneState
});
其中每个 xxxState 是一个 reducer。例如,这里是实现 editorState 的 reducer.editor.state.js 文件:
const initialState = {
actId: '',
actProps: undefined,
actSteps: undefined,
deleted: false
};
export const editorState = (state = initialState, action = {}) => {
switch (action.type) {
case types.NAVIGATE_WITH_ACTION_ID:
return onNavigateWithActionId(state, action);
...
default:
return state;
}
};
在可以将组合 reducer editorContainer 应用到集合之前,需要一个初始状态的选择器:
export const getInitialEditorContainerState = () => ({
editorState: initialEditor,
propertiesState: initialProperties,
outlineState: initialOutline,
categoryState: initialCategory,
dataPaneState: initialDataPane
});
所需要做的就是导出每个子 reducer 的每个 initialState,然后导入它们用于初始状态选择器:
export const initialState = {
actId: '',
...
// 其他初始属性
};
...
// seletor.initialStaet.js: 从每个 reducer 文件导入 initialState
import { initialState as initialEditor } from "../editor/reducer.editor.state";
export const getInitialEditorContainerState = () => ({
editorState: initialEditor,
...
// 其他导入的 initialState
});
一旦有了组合 reducer 的初始状态,其余的就相当简单了。
应用组合 reducer 目标是允许用户在自己的标签页中打开/编辑多个 AEditor 实例。由于 tabsState 已经有一个标签页数组,当创建一个新标签页时,可以将 initialEditorContainerState 设置为标签页的内容状态,也将 editorContainer 组合 reducer 设置为其内容 reducer。
这里是在 tabsState reducer 中创建新标签页后调用的代码:
function _setTabContent(newTab) {
if (newTab.tabType === AppConst.AEDITOR) {
if (!newTab.contentState || typeof (newTab.contentState) !== "object") {
newTab.contentState = getInitialEditorContainerState();
}
if (!newTab.contentReducer || typeof (newTab.contentReducer) !== "function") {
newTab.contentReducer = editorContainer;
}
}
}
接下来,需要在 tabsState reducer 中调用 contentReducer 来更新 contentState:
function onContentActions(state, action) {
let updatedState = state;
let curTab = state.tabs[state.selectedIndex];
if (curTab && curTab.contentReducer) {
let updatedTabs = state.tabs.slice();
updatedTabs.splice(state.selectedIndex, 1, {...curTab, contentState: curTab.contentReducer(curTab.contentState, action)});
updatedState = {...state, tabs: updatedTabs};
}
return updatedState;
}
export default function tabsState(state = _initialTabsState, action) {
switch (action.type) {
case AppConst.OPEN_OR_CREATE_TAB:
return onOpenOrCreateTab(state, action);
case AppConst.CLOSE_ONE_TAB:
return onCloseOneTab(state, action);
case AppConst.UPDATE_ONE_TAB:
return onUpdateOneTab(state, action);
default:
return onContentActions(state, action);
}
}
注意 onContentActions 通过调用 curTab.contentReducer(curTab.contentState, action) 更新当前标签页,这是确保所有当前动作和组件与当前选定的标签页内容一起工作的关键。