X6 撤销重做插件 History 完全指南:配置、批处理与事件机制
【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6
导读
在基于 @antv/x6 构建的图编辑应用中,撤销(Undo)与重做(Redo)是用户操作体验的基石能力。本指南围绕 X6 官方插件History(撤销重做插件文档)展开,系统讲解如何为画布接入撤销/重做、如何通过ignoreAdd/ignoreRemove/ignoreChange控制记录范围、如何用batch将多个改变合并为一条历史记录,以及底层命令栈、验证器与事件回调的完整机制。读完本文,你将能够在自己的 X6 项目中落地一套可生产使用的历史记录方案。
一、快速上手:启用撤销重做
1.1 安装与引入
History是 X6 的内置插件,随@antv/x6主包一起发布,无需额外安装依赖。在代码中通过命名导入即可使用:
import { Graph, History } from '@antv/x6'1.2 创建画布并挂载插件
创建Graph实例后,通过graph.use()挂载History插件,并传入{ enabled: true }开启功能:
const graph = new Graph({ container: document.getElementById('container'), background: { color: '#F2F7FA', }, }) graph.use( new History({ enabled: true, }), )enabled的默认值即为true(见 getOptions 实现),因此new History()不传任何参数也会默认开启。上述写法与官方教程演示页(演示实现)保持一致。
1.3 最小可运行示例
启用插件后,对画布上元素的任何操作(移动、增删、属性修改)都会被自动记录:
graph.addNode({ x: 100, y: 60, width: 100, height: 40, label: 'Drag Me', attrs: { body: { stroke: '#8f8f8f', strokeWidth: 1, fill: '#fff', rx: 6, ry: 6 }, }, }) graph.undo() // 撤销,节点被移除 graph.redo() // 重做,节点重新出现官方演示的行为可简单概括为三句话:
- 随意移动节点后,Undo 按钮变得可用;
- 点击 Undo 按钮,节点位置被还原,随后 Redo 按钮变得可用;
- 点击 Redo 按钮,节点位置被更新。
演示页面(index.tsx)通过监听history:change事件同步刷新两个按钮的canUndo/canRedo状态,这是实现"按钮可用性联动"的推荐做法。
二、配置项详解
History插件构造函数接收一个配置对象,官方文档给出了完整的配置表。下表在原表基础上补充了源码确认的默认值与行为说明:
| 属性名 | 类型 | 默认值 | 必选 | 描述 |
|---|---|---|---|---|
stackSize | number | 0 | 否 | 历史记录栈最大长度,0表示不限制;设置为其他数字表示最多只记录该数量的历史记录 |
ignoreAdd | boolean | false | 否 | 为true时,添加元素不会被记录到历史记录 |
ignoreRemove | boolean | false | 否 | 为true时,删除元素不会被记录到历史记录 |
ignoreChange | boolean | false | 否 | 为true时,元素属性变化不会被记录到历史记录 |
beforeAddCommand | (event, args) => any | - | 否 | 命令被加入 Undo 队列前调用,若返回false,该命令不会入队 |
afterAddCommand | (event, args, cmd) => any | - | 否 | 命令被加入 Undo 队列后调用 |
executeCommand | (cmd, revert, options) => any | - | 否 | 命令被撤销或重做时调用,revert为true表示撤销,否则为重做 |
此外,源码还暴露了几个文档未列入基础表的进阶选项(定义见 type.ts):
eventNames:自定义需要监听的模型事件名数组,默认监听cell:added、cell:removed、cell:change:*(见 util.ts)。传入时会过滤掉保留事件与 batch 事件,因此它主要用于扩展监听范围,而非缩减范围。revertOptionsList/applyOptionsList:指定撤销/重做时透传到命令执行回调中的options属性名列表,默认均为['propertyPath']。cancelInvalid:校验不通过的命令是否自动取消(撤销并从重做栈删除),默认true,由 Validator 负责执行。
2.1 按场景选择忽略项
- 若画布中元素由后台数据驱动、不允许用户新建/删除,可设置
ignoreAdd: true、ignoreRemove: true,只保留属性变化的撤销能力; - 若希望"添加即提交",不想让用户撤销掉初始节点,可只设置
ignoreAdd: true; - 若只想记录结构变化(增删)而不关心样式微调,可设置
ignoreChange: true。
2.2 用 beforeAddCommand 做精细化拦截
beforeAddCommand是最灵活的控制点,可基于事件名与参数决定是否记录。例如忽略来自特定操作的命令:
graph.use( new History({ enabled: true, beforeAddCommand(event, args) { // 返回 false 则该命令不入队 if (args.options && args.options.ignoreHistory) { return false } return true }, }), )对应的源码逻辑位于 addCommand 方法:只有before回调存在且返回值严格等于false时,命令才被丢弃;返回其他任意值(含undefined)均正常入队。
2.3 命令过滤的另一入口:dryrun
除配置外,源码还支持在操作层面跳过记录:当模型事件携带options.dryrun时,命令同样不会入队(见 addCommand)。这在需要"执行但不记录"的临时操作(如实时预览)中很有用。
三、批处理:将多个改变合并为一条历史记录
在实际项目中,一次用户操作往往包含多个改变——例如"修改边框颜色并移动位置"、"批量设置 zIndex、文字与填充色"。若逐条记录,用户需要多次撤销才能回到操作前状态。X6 提供batch概念,将多个改变合并成一条历史记录,一次撤销/重做即可整体回退。
3.1 方式一:startBatch / stopBatch
graph.startBatch('custom-batch-name') // 这两个操作会合并成一条记录,可以一次性撤销 node.attr('body/stroke', 'red') node.position(30, 30) graph.stopBatch('custom-batch-name')注意:batch名称需前后一致。模型层使用计数器维护嵌套层级(见 model.ts),同名 batch 可以嵌套,stopBatch的次数需与startBatch匹配才能最终落栈。
3.2 方式二:batchUpdate 函数式写法
graph.batchUpdate(() => { node.prop('zIndex', 10) node.attr('label/text', 'hello') node.attr('label/fill', '#ff0000') })batchUpdate是对startBatch/stopBatch的封装(实现见 graph.ts),内部自动以'update'为默认名称包裹回调,并返回回调的执行结果,适用于"执行完毕后无需关心批名"的场景。它同样支持显式命名:graph.batchUpdate('my-batch', () => { ... })。
3.3 批处理的底层机制
从源码看,批处理的完整链路如下(index.ts):
- 插件监听模型的
batch:start与batch:stop事件; batch:start触发initBatchCommand():若当前无批处理上下文,则创建一个空的 batch 命令,之后所有模型改变都临时写入该命令(initBatchCommand);- 批处理期间对同一元素、同一类型的事件会合并
prev/next快照(addCommand); batch:stop触发storeBatchCommand():经filterBatchCommand()清洗(剔除"先加后删"等相互抵消的命令、比对prev与next是否相等,见 filterBatchCommand)后,整组命令作为一个单元压入 Undo 栈,并清空 Redo 栈(storeBatchCommand)。
也就是说,batch 模式下栈中的一条记录可能是一个HistoryCommand[]数组,撤销/重做时会按正确顺序批量执行其中的每条命令(sortBatchCommands保证增删顺序合理,见 util.ts)。
3.4 进阶:移动 + 嵌入合并
仓库还为"拖动节点使其嵌入父节点"这类高频交互提供了专门的合并逻辑:consolidateCommands()会检测 Undo 栈顶部是否同时包含cell:change:parent与cell:change:children(且均来自用户交互options.ui),并将其与前一条cell:change:position命令合并,从而实现"移动 + 嵌入"一步撤销(见 consolidateCommands)。
四、API 参考
插件挂载后,History的能力通过原型扩展直接注入Graph实例(注册见 api.ts),以下 API 均可在graph上直接调用。
4.1 撤销与重做
// 撤销,options 会传递到事件回调中 undo(options?: KeyValue): this // 撤销,且不加入重做队列,因此该命令不能被重做 undoAndCancel(options?: KeyValue): this // 重做 redo(options?: KeyValue): this从实现看(index.ts):
undo():从 Undo 栈弹出命令 → 逆序执行撤销逻辑 → 压入 Redo 栈 → 触发history:undo事件;redo():从 Redo 栈弹出命令 → 正序重新执行 → 压入 Undo 栈 → 触发history:redo事件;undoAndCancel():撤销后清空整个 Redo 栈,相当于"回退并放弃未来的所有重做可能"。
4.2 状态查询
canUndo(): boolean // 是否可以撤销 canRedo(): boolean // 是否可以重做 isHistoryEnabled(): boolean // 历史功能是否启用 getHistoryStackSize(): number // history 栈的尺寸(即配置的 stackSize) getUndoStackSize(): number // undo 栈当前命令数 getRedoStackSize(): number // redo 栈当前命令数 getUndoRemainSize(): number // undo 栈剩余可用空间(stackSize - undo 栈长度)canUndo()/canRedo()的判定条件为"插件未禁用且对应栈非空"(index.ts)。注意getHistoryStackSize()返回的是配置的上限值而非栈内实际命令数,实际数量应使用getUndoStackSize()。
4.3 启用状态控制
enableHistory(): this // 启用历史记录 disableHistory(): this // 禁用历史记录 toggleHistory(enabled?: boolean): this // 切换启用状态| 名称 | 类型 | 必选 | 默认值 | 描述 |
|---|---|---|---|---|
enabled | boolean | 否 | - | 是否启用历史状态;缺省时取反切换 |
对应的插件层实现为enable()/disable()/toggleEnabled()(index.ts)。disableHistory()只是暂停记录与执行,不会清空已有栈;再次启用后历史记录依然可用。
4.4 清理与栈管理
cleanHistory(options?: KeyValue): this清空 Undo 与 Redo 两个栈,并触发history:clean事件。源码对应clean()方法(index.ts)。当stackSize > 0且栈已满时,新命令入栈会从队首挤出最旧的记录(undoStackPush),保证栈长不超过上限。
五、事件机制
5.1 事件总表
事件既可以在graph上以history:*前缀监听,也可以在History插件实例上直接监听(事件类型注册见 api.ts):
| 事件名称 | 参数类型 | 描述 |
|---|---|---|
history:undo | { cmds: Command[], options: KeyValue } | 命令被撤销时触发 |
history:redo | { cmds: Command[], options: KeyValue } | 命令被重做时触发 |
history:cancel | { cmds: Command[], options: KeyValue } | 命令被取消(undoAndCancel)时触发 |
history:add | { cmds: Command[], options: KeyValue } | 命令被加入队列时触发 |
history:clean | { cmds: Command[] \| null, options: KeyValue } | 历史队列被清空时触发 |
history:change | { cmds: Command[] \| null, options: KeyValue } | 历史队列发生任何变化时触发 |
history:batch | { cmd: Command, options: KeyValue } | 接收到 batch 命令时触发 |
Command的结构定义于 type.ts:包含batch(是否批处理命令)、event(事件名)、data(prev/next或props快照)、options等字段。
5.2 监听方式
// 方式一:在 graph 上监听 graph.on('history:undo', ({ cmds }) => { console.log(cmds) }) // 方式二:在插件实例上监听(事件名不带 history: 前缀) const history = new History({ enabled: true }) graph.use(history) history.on('undo', ({ cmds }) => { console.log(cmds) })两种监听方式在内部是统一的:插件的notify()会同时触发插件自身的emit(event, ...)与graph.trigger('history:' + event, ...),并额外派发一个change事件(notify 实现)。因此history:change会在每次 undo/redo/add/clean 之后触发,非常适合用来驱动 UI 按钮的可用态刷新——官方演示正是这么做的。
5.3 事件与 UI 联动示例
graph.on('history:change', () => { undoBtn.disabled = !graph.canUndo() redoBtn.disabled = !graph.canRedo() })六、命令执行原理与验证器
6.1 撤销/重做如何真正改变画布
executeCommand()(index.ts)是命令落地的核心,它根据命令事件类型分派:
- 添加/删除类事件(
cell:added/cell:removed):撤销时删除对应 cell,重做时依据记录中的props快照重新model.addNode/model.addEdge; - 属性变化类事件(
cell:change:*):通过cell.prop(key, value)在prev[key]与next[key]之间来回写入。对于attrs变更,还会调用ensureUndefinedAttrs()递归补齐被置undefined的属性并标记dirty,保证 SVG 元素上的属性能被正确移除(ensureUndefinedAttrs); - 其他自定义事件:若配置了
executeCommand回调,则交给回调处理。
执行过程中插件会置freezed = true,避免撤销/重做本身又被递归记录进历史栈(见 revertCommand)。
6.2 验证器:阻止非法状态
Validator(validator.ts)允许为指定事件注册校验回调,用于拦截会导致画布进入非法状态的操作:
const history = new History({ enabled: true }) history.validator.validate('cell:change:position', (err, cmd, next) => { // 业务校验,如禁止节点移动到某区域 if (cmd.data.next.position && cmd.data.next.position.x < 0) { next(new Error('节点不能移动到负坐标区域')) return } next(null) }) graph.use(history)校验不通过时:若cancelInvalid为true(默认),会立即撤销该命令并从重做栈移除,同时触发invalid事件。此外,命令自带options.validation === false时可直接跳过校验。
七、测试佐证
仓库为 History 插件提供了覆盖全面的单元测试(history.spec.ts),可作行为契约参考:
- 默认启用、
enable/disable/toggleEnabled的开关逻辑; undo/redo/cancel的栈行为与事件触发;stackSize对 Undo 栈长度的限制;- batch 命令的执行、存储与
consolidateCommands的"移动 + 嵌入"合并; ignore选项与beforeAddCommand的拦截行为;executeCommand对 add/remove/change 的分派及自定义回调;- Validator 的校验与
invalid事件。
这些用例与本文第 2、3、6 节描述的行为一一对应,是验证"批处理合并""栈上限""拦截机制"等特性的最直接依据。
总结
History插件为 X6 图编辑应用提供了开箱即用的撤销/重做能力,其核心设计可以概括为三点:
- 命令栈模型:基于 Undo/Redo 双栈,配合
stackSize限制与ignore*过滤,天然支持增删改三类操作的记录; - 批处理机制:
startBatch/stopBatch/batchUpdate将多个改变折叠为一条历史记录,配合命令清洗与排序,保证复杂交互也能一步回退; - 扩展钩子:
beforeAddCommand、executeCommand、Validator 与完整的事件体系,让开发者可以精细控制记录范围、自定义命令语义并驱动 UI 状态。
无论你是在构建流程编辑器、拓扑图工具还是在线白板,将上述配置与 API 组合使用,都能获得可靠且可定制的历史记录体验。更深入的实现细节可继续阅读插件源码(src/plugin/history)与配套测试(history.spec.ts)。
【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考