在 dva 应用中集成 redux-undo:基于 onReducer 增强器的撤销/重做实战

📅 发布时间:2026/9/21 16:41:39
在 dva 应用中集成 redux-undo:基于 onReducer 增强器的撤销/重做实战
在 dva 应用中集成 redux-undo基于 onReducer 增强器的撤销/重做实战【免费下载链接】dva React and redux based, lightweight and elm-style framework. (Inspired by elm and choo)项目地址: https://gitcode.com/gh_mirrors/dv/dva导读本文围绕仓库中 examples/with-redux-undo 示例讲解如何在 dva 应用中集成redux-undo实现撤销Undo与重做Redo能力。你将掌握 dvaonReducer配置项的正确用法、被undoable高阶 reducer 包裹后的状态树结构变化以及视图层如何通过ActionCreators.undo()/ActionCreators.redo()触发时间旅行并深入理解这一机制在 dva-core 中的底层实现。示例概览一个支持撤销重做的计数器with-redux-undo是一个麻雀虽小五脏俱全的 dva 示例它构建了一个经典的 Counter 计数器应用在原有/-增减操作之上额外提供了Undo与Redo两个按钮让每一次计数操作都能被回放或回退。示例的核心文件如下文件作用src/index.js应用入口dva 实例创建、onReducer配置、组件挂载src/models/counter.jscounter 模型定义add/minus两个 reducersrc/index.htmlHTML 入口包含#root挂载点并引入index.jspackage.json项目依赖与启动脚本从 package.json 可以看到示例的技术栈为dva^2.2.3、redux-undo^0.6.1、react^16.2.0并使用parcel-bundler^1.4.1作为零配置打包器。快速开始安装、启动与访问按照 README.md 的指引先安装依赖npm install或者使用 yarnyarn安装完成后启动开发服务器npm start或者yarn startpackage.json 中start脚本实际执行的是parcel ./src/index.html即由 Parcel 直接以src/index.html为入口进行打包与热更新。启动成功后打开浏览器访问http://localhost:1234即可看到 Counter 应用点击/-修改计数值点击Undo/Redo回退或重放历史操作。核心原理为什么需要onReducerdva 中一个 model 的 reducer 会被合并进 Redux 的 reducer 树状态以namespace为键组织。而redux-undo的核心机制是用一个高阶 reducer 包裹目标 reducer自动维护past历史、present当前、future未来三段状态从而实现时间旅行。在 dva 中接入redux-undo最简洁的入口就是实例化时传入的onReducer配置项——它是一个reducer 增强器在应用层拿到合并后的总 reducer之后、交给 Redux store 之前进行再加工。src/index.js 中正是这样做的const app dva({ onReducer: reducer (state, action) { const newState undoable(reducer, {})(state, action); return { ...newState }; }, });这里的reducer参数就是 Redux 的combineReducers合并结果包含 router、counter 等所有 namespace。undoable(reducer, {})将其整体包装为支持撤销/重做的高阶 reducer第二个参数{}为 redux-undo 的配置项示例使用空对象即默认配置。注意返回时{ ...newState }做了浅拷贝展开避免直接共享被包装后的引用。源码级剖析onReducer在 dva 内部如何生效理解onReducer的底层调用链有助于写出正确的增强器。以当前仓库 dva-core 的实现为准在start()中dva 通过plugin.get(onReducer)取出所有注册的 reducer 增强器packages/dva-core/src/index.js#L184createReducer()先通过combineReducers({ ...reducers, ...extraReducers, ...asyncReducers })合并出基础 reducerpackages/dva-core/src/index.js#L241-L249合并结果被reducerEnhancer(...)包裹后才交给createStore创建 storepackages/dva-core/src/index.js#L194-L201。而onReducer增强器本身是函数链式的在 packages/dva-core/src/Plugin.js#L89-L95 的getOnReducer中多个增强器按注册顺序依次包裹function getOnReducer(hook) { return function(reducer) { for (const reducerEnhancer of hook) { reducer reducerEnhancer(reducer); } return reducer; }; }这解释了示例中onReducer回调为什么是reducer (state, action) {...}的形态外层函数接收合并后的 reducer内层函数才是真正参与每次 dispatch 的 reducer。此外Plugin.js 中列出onReducer属于 dva 官方支持的 9 个 hook 之一onError、onStateChange、onAction、onHmr、onReducer、onEffect、extraReducers、extraEnhancers、_handleActions意味着它与 dva-loading 等插件的挂载机制完全兼容属于一等公民能力。模型与状态树present包装带来的结构变化示例的计数器模型定义在 src/models/counter.js非常标准export default { namespace: counter, state: 0, reducers: { add(state) { return state 1; }, minus(state) { return state - 1; }, }, };关键点在于由于onReducer用undoable包裹的是整棵合并后的 reducer 树而不是单个 namespace 的 reducer因此经过包装后Redux 的顶层 state 结构从{ router: ..., counter: 0 }变为{ past: [...], // 已发生的历史状态快照 present: { router: ..., counter: 0 }, // 当前状态 future: [...] // 被撤销后保留的未来快照 }这就是 src/index.js 中connect必须从present里取数据的原因const App connect(({ present: { counter } }) ({ counter, }))(props { ... });如果不经present解构而直接读取state.counter将得到undefined。这一点是集成redux-undo时最容易踩的坑务必牢记。视图层dispatch 与撤销/重做动作视图层同样是普通 React 组件通过connect拿到counter和dispatch。/-按钮照常派发 model 中定义的带命名空间的 actionprops.dispatch({ type: counter/minus }); props.dispatch({ type: counter/add });撤销与重做则依赖redux-undo导出的ActionCreatorsimport undoable, { ActionCreators } from redux-undo; props.dispatch(ActionCreators.undo()); // 回退一步 props.dispatch(ActionCreators.redo()); // 重放一步ActionCreators.undo()/ActionCreators.redo()返回对应的 action 对象由undoable高阶 reducer 内部拦截处理undo将present压入future、把past末尾弹出作为新的presentredo则反向操作。用户点击时dva 的dispatch与 redux 完全同构因此这些 action 会畅通无阻地到达被包裹的 reducer。从示例到实战集成要点与扩展方向基于本示例可以在实际 dva 项目中举一反三整树包装 vs 局部包装示例选择在onReducer中整体包裹 reducer 树实现全局可撤销。若只想让某个模块可撤销也可对单一 namespace 的 reducer 做undoable包装此时该 namespace 的状态会变成{ past, present, future }取数时需做相应调整。多增强器共存由于getOnReducer是链式包裹packages/dva-core/src/Plugin.js#L89-L95onReducer可以与 dva-loading 等其它 reducer 增强类插件同时配置顺序即包裹顺序。配置项留白示例传入{}使用 redux-undo 默认行为如需控制历史长度、按 action 过滤等可在该参数位传入对应配置如{ limit: 20 }但注意以所安装的redux-undo^0.6.1版本实际支持的选项为准。启动时序onReducer属于dva({...})初始化参数必须在app.start(#root)之前生效从 packages/dva/src/index.js#L51-L83 可见start内部才真正执行createStore因此初始化时的增强器配置会完整作用于最终 store。小结with-redux-undo示例展示了在 dva 中集成撤销/重做功能的最小完备路径通过onReducer配置项用undoable包裹合并后的 reducer 树src/index.js调整connect从present取数再用ActionCreators.undo()/ActionCreators.redo()驱动时间旅行。底层由 dva-core 的plugin.get(onReducer)与 Plugin.js 的链式增强机制保障这使得 redux-undo 与 dva 的模型体系、插件机制能够无缝协作。【免费下载链接】dva React and redux based, lightweight and elm-style framework. (Inspired by elm and choo)项目地址: https://gitcode.com/gh_mirrors/dv/dva创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考