G6 History 历史记录插件完全指南:为图编辑实现撤销(Undo)与重做(Redo)
数据可视化前端图表库【免费下载链接】G6♾ A Graph Visualization Framework in JavaScript.项目地址https://gitcode.com/gh_mirrors/g6/G6点击查看免费下载导读History 是 G6 图可视化框架内置的官方插件专门为图编辑场景提供撤销Undo与重做Redo能力它监听渲染前后图数据的差异将其作为历史命令压入状态堆栈从而支持用户在一次或多次交互后一键回溯、恢复或取消操作。本文基于 G6 仓库中 History 插件官方文档结合 插件核心实现源码 与 单元测试完整讲解插件的配置项、Command 数据模型、全部 API、两种历史记录模式及批量操作的正确姿势。读完本文你将能够在自己构建的图编辑应用中快速接入撤销/重做能力并学会用BatchController将多步渲染合并为一次可撤销操作。概述与使用场景History 插件的核心职责是通过记录用户操作的历史状态堆栈支持在图交互过程中进行回溯或恢复操作并为用户提供完善的配置项与 API。从实现角度看插件在构造时监听了三个图生命周期事件见 index.tsGraphEvent.AFTER_DRAW每次渲染完成后触发用于把渲染前后产生的数据变更解析成一条命令入栈GraphEvent.BATCH_START批量操作开始时触发用于初始化批量变更缓存GraphEvent.BATCH_END批量操作结束时触发把整批变更作为一条命令入栈。这种基于渲染前后数据差异的设计使得插件天然适用于所有涉及到图编辑的场景增删节点/边/Combo、拖拽移动元素、折叠/展开 Combo、切换元素状态与可见性、调整 zIndex、连边交互等。测试用例 plugin-history.spec.ts 中依次验证了addData、updateData、removeData、collapse/expand、setElementState、setElementVisibility、setElementZIndex、create-edge 等十余种操作在 undo/redo 前后的表现足见其覆盖面之广。基本用法在图配置中添加plugins数组即可启用插件共有两种声明方式。1. 快速配置静态字符串形式直接使用字符串history声明简洁但仅支持默认配置且配置后不可动态修改const graph new Graph({ // 其他配置... plugins: [history], });2. 对象配置推荐可动态更新使用对象形式配置支持自定义参数且可以在运行时通过graph.updatePlugin动态更新配置const graph new Graph({ // 其他配置... plugins: [ { type: history, key: history-1, // 插件实例标识用于 getPluginInstance 获取实例 stackSize: 10, // 最多记录 10 条历史 }, ], });设置key后即可通过graph.getPluginInstance(history-1)拿到插件实例并调用其 API官方在线体验示例 common/api/plugins/history.md 中演示了用graph.updatePlugin({ key: history, stackSize: value })实时调整stackSize的用法。插件实例的获取与调用方式请参考 插件总览文档。配置项详解属性描述类型默认值必选afterAddCommand当一个命令被添加到Undo/Redo队列后被调用。revert为true时表示撤销操作为false时表示重做操作(cmd: Command, revert: boolean) void-否beforeAddCommand当一个命令被添加到Undo/Redo队列前被调用如果该方法返回false那么这个命令将不会被添加到队列中。revert为true时表示撤销操作为false时表示重做操作(cmd: Command, revert: boolean) boolean \| void-否executeCommand执行命令时的回调函数(cmd: Command) void-否stackSize最多记录该数据长度的历史记录number0不限制否配置项类型定义可参见源码 HistoryOptions下面结合源码说明各配置项的真实作用stackSize默认值0表示不做限制。当stackSize ! 0且撤销栈长度已达到上限时新命令入栈前会从栈底shift()掉最旧的记录实现环形滑动窗口式的历史容量控制见 undoStackPush。beforeAddCommand在命令真正入栈前执行返回false可拦截该命令。该钩子在撤销、重做、渲染入栈三条路径上都会被调用revert参数区分方向true表示此次是撤销命令进入撤销栈false表示重做命令进入重做栈。afterAddCommand命令成功入栈后触发适合做记录数统计、日志上报等旁路逻辑。executeCommand执行命令时的回调。注意它区别于beforeAddCommand它发生在命令执行即调用addData/updateData/removeData还原图数据之前可用于自定义执行行为。测试用例beforeAddCommand一节plugin-history.spec.ts验证了拦截逻辑设置beforeAddCommand: () false后执行隐藏元素操作撤销栈长度不变改为返回true后撤销栈长度 1。核心数据模型CommandCommand是历史记录的最小单元其类型定义见 types/history.ts// 单条历史记录命令 interface Command { current: CommandData; // 当前数据 original: CommandData; // 原始数据 animation: boolean; // 是否开启动画 } // 单条历史记录命令数据 interface CommandData { add: GraphData; // 新增的数据 update: GraphData; // 更新的数据 remove: GraphData; // 移除的数据 } // 图数据 interface GraphData { nodes?: NodeData[]; // 节点数据 edges?: EdgeData[]; // 边数据 combos?: ComboData[]; // Combo 数据 }从 util.ts 的parseCommand实现可以理解 Command 的语义新增current.add记录新增的元素同时其镜像数据会被放入original.remove因此撤销新增 移除该元素移除current.remove记录被移除的元素同时镜像放入original.add因此撤销移除 重新添加该元素更新current.update保存新值original.update保存旧值。为了撤销/重做后样式完全还原源码对更新类命令做了特殊处理根据元素类型边取stroke、其余取fill读取元素计算后的颜色样式并入原始值并通过alignFields递归补齐缺失字段否则缺少的字段在还原时会被默认值覆盖。该逻辑正是 bug 修复用例 plugin-history-align-fields.spec.ts 所守护的行为节点在平移后其data中的嵌套字段如data.aaa必须保持不变。执行命令时见 executeCommand插件根据revert方向选择original或current依次调用graph.addData、graph.updateData、graph.removeData还原数据再调用element.draw({ silence: true, animation: cmd.animation })静默渲染——整个过程用freezed标志避免还原渲染再次触发入栈形成死循环。API 一览history 插件通过graph.getPluginInstance(key)获取实例后可调用以下 API全部方法定义见 index.tsHistory.canRedo()判断是否可以进行重做操作。如果重做堆栈中有记录则返回true否则返回false。canRedo(): boolean;示例const canRedo historyInstance.canRedo(); if (canRedo) { console.log(可以进行重做操作); } else { console.log(重做堆栈为空无法重做); }History.canUndo()判断是否可以进行撤销操作。如果撤销堆栈中有记录则返回true否则返回false。canUndo(): boolean;示例const canUndo historyInstance.canUndo(); if (canUndo) { console.log(可以进行撤销操作); } else { console.log(撤销堆栈为空无法撤销); }History.clear()清空历史记录包括撤销和重做堆栈。源码实现会同时重置undoStack、redoStack与批量变更缓存并触发CLEAR、CHANGE事件clear。clear(): void;示例historyInstance.clear(); console.log(历史记录已清空);History.on()监听历史记录事件允许用户在特定事件发生时执行自定义逻辑。on(event: LoosenHistoryEvent, handler: (e: { cmd?: Command | null }) void): void;HistoryEvent枚举定义见 constants/events/history.tsenum HistoryEvent { UNDO undo, // 当命令被撤销时 REDO redo, // 当命令被重做时 CANCEL cancel,// 当命令被取消时 ADD add, // 当命令被添加到队列时 CLEAR clear, // 当历史队列被清空时 CHANGE change,// 当历史队列发生变化时 }其中Command类型请参考上文核心数据模型。需要特别说明的是CHANGE事件是总闸——源码 notify 中任何事件触发时都会同时再派发一次CHANGE因此监听CHANGE即可覆盖撤销、重做、取消、入栈、清空等全部状态变化。示例historyInstance.on(HistoryEvent.UNDO, () { console.log(执行了撤销操作); });History.redo()执行重做操作并返回插件实例。如果重做堆栈为空则不执行任何操作。源码中重做会先执行命令再经beforeAddCommand/afterAddCommand钩子后压回撤销栈并派发REDO事件redo。redo(): History;示例historyInstance.redo(); console.log(执行了重做操作);History.undo()执行撤销操作并返回插件实例。如果撤销堆栈为空则不执行任何操作。undo(): History;示例historyInstance.undo(); console.log(执行了撤销操作);History.undoAndCancel()执行撤销操作且不计入历史记录并返回插件实例。注意执行该操作会清空重做栈源码中this.redoStack []见 undoAndCancel并派发CANCEL事件。典型用途是取消上一步但不想留下回退痕迹的场景。undoAndCancel(): History;示例historyInstance.undoAndCancel(); console.log(执行了撤销并取消操作);历史记录模式该插件支持两种历史记录模式理解二者的差异是正确使用 History 的关键。默认模式一次渲染一条记录默认模式下每一次触发渲染后比如更新元素数据后用户主动执行graph.draw()方法触发渲染插件会把渲染前和渲染后的数据记录下来并作为一次操作记录入栈。监听AFTER_DRAW事件的 addCommand 正是该模式的实现入口。自定义模式批量合并场景描述实际需求中用户的一次图编辑操作可能涉及到多次渲染。比如一次编辑操作中首先把节点 A、B 展示出来然后展示 A-B 的连线这里就涉及到两次渲染即用户需要进行两次graph.draw()。这种场景下默认模式会入栈两次历史记录分别是展示节点 A 和 B展示 A-B 的连线显然实际业务中一次操作应该只需一次撤销。但默认模式下撤销本次操作时用户需要调用两次undo方法也就是需要进行两次撤销。场景支持为了支持这样的场景G6 提供了一个批量控制器BatchController源码在图实例上下文中提供了这个批量控制器实例。该控制器通过内部计数器batchCount管理嵌套的批量操作startBatch使计数 1endBatch使计数 -1只有当计数回到 0 时才派发BATCH_END事件batch.ts因此支持批量操作的嵌套调用。历史记录插件则基于这个批量控制器实现自定义操作记录代码示例如下const graph new Graph({ // 其他配置... plugins: [ { type: history, key: history, }, ], }); graph.context.batch.startBatch(); // 开始批量操作 graph.addNodeData(...); // 把节点 A、B 展示出来 graph.draw(); // 第一次触发渲染 graph.addEdgeData(...); // 把 A-B 连线展示出来 graph.draw(); // 第二次触发渲染 graph.context.batch.endBatch(); // 结束批量操作示例中通过调用批量控制器实例的startBatch方法告诉历史记录插件现在开始进行批量操作在批量操作没有结束前不管触发多少次渲染都不能进行历史记录入栈历史记录插件会把每次触发渲染的变更数据记录下来在完成最后一次数据变更后调用endBatch()方法历史记录插件监听到批量操作完成BATCH_END则把本次批量操作作为一次历史记录入栈。最终用户只需要进行一次undo即可撤销。从源码可以印证这一流程批量期间每次AFTER_DRAW产生的变更数据都被push进batchChanges暂存且各次动画开关做与运算直到BATCH_END时才统一parseCommand合并入栈addCommand。实战代码示例下面列举一些常见的业务场景并给出相应的代码参考。完整可运行示例可见 demos/plugin-history.ts其中封装了增删改、折叠/展开、状态切换、zIndex 调整、undo/redo/clear 等全部操作的调试面板。场景一撤销、重做按钮状态实际业务场景中可能需要自定义画布的工具栏也就涉及到撤销和重做按钮的启禁用状态const canUndo false; const canRedo false; const graph new Graph({ // 其他配置... plugins: [ { type: history, key: history, }, ], }); const historyInstance graph.getPluginInstance(history); historyInstance.on(HistoryEvent.CHANGE, () { canUndo historyInstance.canUndo(); canRedo historyInstance.canRedo(); });示例中通过监听HistoryEvent.CHANGE事件——该事件在历史队列任何变化撤销、重做、取消、入栈、清空时都会触发——每次发生变化后实时判断当前是否可以进行撤销和重做操作从而同步工具栏按钮的启禁用状态。场景二判断是否允许命令进入队列这里实现一个简单的场景只有移除元素的操作才允许进入历史记录队列const graph new Graph({ // 其他配置... plugins: [ { type: history, key: history, beforeAddCommand: (cmd) { return ( cmd.current.remove?.nodes?.length 0 || cmd.current.remove?.combos?.length 0 || cmd.current.remove?.edges?.length 0 ); }, }, ], });示例中通过配置项beforeAddCommand来实现判断cmd.current.remove里面是否存在被移除的元素有则返回true放行入栈无则返回false拦截。同理也可以基于cmd.current.add只放行新增操作或结合revert参数对撤销/重做方向做差异化过滤。实现原理小结与扩展阅读回顾 History 插件的核心机制可以用三条主线概括事件驱动的入栈插件监听AFTER_DRAW与BATCH_END把渲染前后数据差异经 parseCommand 解析为Command入栈双栈模型undoStack与redoStack相互配合——undo弹出撤销栈压入重做栈redo反向操作undoAndCancel则只弹栈并清空重做栈防抖与冻结执行命令时通过freezed标志屏蔽还原渲染引发的二次入栈保证状态一致性。若需进一步了解可在仓库中继续阅读插件完整实现plugins/history/index.ts命令解析与字段对齐plugins/history/util.ts批量控制器runtime/batch.ts类型定义types/history.ts 与 constants/events/history.ts单元测试unit/plugins/history/plugin-history.spec.ts调试示例demos/plugin-history.ts借助 History 插件与BatchController你可以在自己的图编辑应用中低成本地获得完整、可靠的撤销/重做体验并灵活控制历史记录的粒度与容量。赞分享数据可视化前端图表库【免费下载链接】G6♾ A Graph Visualization Framework in JavaScript.项目地址https://gitcode.com/gh_mirrors/g6/G6点击查看免费下载相关推荐G6 History 历史记录插件撤销 / 重做机制与实战指南G6 History 历史记录插件撤销 / 重做机制与实战指南 导读 本文围绕 AntV G6 antv/g6 内置的 History 历史记录插件展开数据可视化前端图表库Slate History 指南为 Slate 编辑器接入可撤销Undo与重做Redo能力Slate History 指南为 Slate 编辑器接入可撤销Undo与重做Redo能力 本指南围绕 Slate 官方子库 slate histor前端富文本UI组件Elementor 编辑器历史记录组件解析document/history 命令体系与撤销/重做实现Elementor 编辑器历史记录组件解析document/history 命令体系与撤销/重做实现 本指南以 Elementor 开源仓库中 docs/asCMS前端后端低代码上一篇vanilla-extract的测试覆盖率自动化工具自动化工具下一篇serve代码质量保障ESLint配置与Prettier格式化规则创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考