模块模式与命令模式配合是构建可维护、可扩展绘图软件撤销系统的关键组合:前者封装命令管理逻辑,后者将绘图操作抽象为可执行、可撤销对象;二者结合实现历史栈管理、状态一致性与工具解耦,无需画布快照,内存友好且逻辑清晰。

模块模式与命令模式配合,是构建可维护、可扩展绘图软件撤销系统的关键组合:模块模式负责封装和隔离命令管理逻辑,命令模式则将每个绘图操作抽象为可执行、可撤销的对象。二者结合后,历史栈管理、状态一致性、工具解耦都能自然落地,不依赖画布快照,内存友好且逻辑清晰。
模块模式封装命令管理器
用模块模式把命令历史、游标索引、撤销/重做逻辑收进一个独立作用域,避免全局污染,也便于复用和测试:
- 内部维护两个数组:一个存已执行的命令(history),一个存被撤销后暂存的命令(redoStack)
- 游标 cursor 指向当前有效状态位置(不是数组末尾),保证“撤销后新增操作”自动清空 redoStack
- 对外只暴露
execute(command)、undo()、redo()和canUndo()/canRedo()等干净接口 - 所有状态变更都在模块内部完成,外部调用者无需关心索引越界或数组截断细节
命令对象统一结构与具体实现
每个绘图动作(如画线、拖拽节点、填充颜色)都实现同一个命令接口,结构必须包含可逆性所需信息:
- 必须字段:
type(字符串标识)、before(操作前状态快照或关键属性)、after(操作后状态或参数) - 例如线条命令:
{ type: 'drawLine', before: null, after: { points: [[100,100],[200,150]], stroke: '#333', strokeWidth: 2 } } -
execute()负责应用after数据到画布(Konva.Stage 或 Canvas API);undo()则恢复before状态——若before是 null,说明是新增操作,undo 就是删除该图形 - 所有命令构造时即固化数据,不持有对画布或图层的引用,确保无内存泄漏
操作链路与状态一致性保障
用户每一次有效交互,都要走通“捕获 → 构建 → 执行 → 记录”闭环,同时拦截非法状态跃迁:
- 鼠标松开、键盘确认、工具切换完成等时机才生成并提交命令,避免中间态入栈
- 调用
execute()时,先调用命令自身的execute(),再将命令推入 history,并更新 cursor = history.length − 1,同时清空 redoStack - 按下 Ctrl+Z 触发
undo():若 cursor > 0,则 cursor--,执行对应命令的undo();按钮禁用状态由canUndo()实时返回 - 任何新增命令(非 redo)都必须调用
clearRedo(),这是保持线性历史不可绕过的约束
与 KonvaJS 或 Canvas 渲染层低耦合集成
命令本身不操作视图,而是通过接收者(Receiver)间接驱动渲染,真正实现关注点分离:
- 定义
Renderer类作为接收者,提供addShape()、removeShapeById()、updateShapeAttrs()等方法 - 具体命令(如
MoveNodeCommand)在构造时注入该 Renderer 实例,在execute()中调用其方法,而非直接操作layer.children - 模块导出的命令管理器只依赖接口,不依赖 Konva;未来切换到 Fabric.js 或纯 Canvas,只需重写 Renderer 即可
- 撤销重做时,Renderer 可触发自定义事件(如
stateChanged),供 UI 同步标题栏、历史面板等











