news 2026/9/17 9:53:54

X6 撤销重做插件 History 完全指南:配置、批处理与事件机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
X6 撤销重做插件 History 完全指南:配置、批处理与事件机制

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插件构造函数接收一个配置对象,官方文档给出了完整的配置表。下表在原表基础上补充了源码确认的默认值与行为说明:

属性名类型默认值必选描述
stackSizenumber0历史记录栈最大长度,0表示不限制;设置为其他数字表示最多只记录该数量的历史记录
ignoreAddbooleanfalsetrue时,添加元素不会被记录到历史记录
ignoreRemovebooleanfalsetrue时,删除元素不会被记录到历史记录
ignoreChangebooleanfalsetrue时,元素属性变化不会被记录到历史记录
beforeAddCommand(event, args) => any-命令被加入 Undo 队列前调用,若返回false,该命令不会入队
afterAddCommand(event, args, cmd) => any-命令被加入 Undo 队列后调用
executeCommand(cmd, revert, options) => any-命令被撤销或重做时调用,reverttrue表示撤销,否则为重做

此外,源码还暴露了几个文档未列入基础表的进阶选项(定义见 type.ts):

  • eventNames:自定义需要监听的模型事件名数组,默认监听cell:addedcell:removedcell:change:*(见 util.ts)。传入时会过滤掉保留事件与 batch 事件,因此它主要用于扩展监听范围,而非缩减范围。
  • revertOptionsList/applyOptionsList:指定撤销/重做时透传到命令执行回调中的options属性名列表,默认均为['propertyPath']
  • cancelInvalid:校验不通过的命令是否自动取消(撤销并从重做栈删除),默认true,由 Validator 负责执行。

2.1 按场景选择忽略项

  • 若画布中元素由后台数据驱动、不允许用户新建/删除,可设置ignoreAdd: trueignoreRemove: 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):

  1. 插件监听模型的batch:startbatch:stop事件;
  2. batch:start触发initBatchCommand():若当前无批处理上下文,则创建一个空的 batch 命令,之后所有模型改变都临时写入该命令(initBatchCommand);
  3. 批处理期间对同一元素、同一类型的事件会合并prev/next快照(addCommand);
  4. batch:stop触发storeBatchCommand():经filterBatchCommand()清洗(剔除"先加后删"等相互抵消的命令、比对prevnext是否相等,见 filterBatchCommand)后,整组命令作为一个单元压入 Undo 栈,并清空 Redo 栈(storeBatchCommand)。

也就是说,batch 模式下栈中的一条记录可能是一个HistoryCommand[]数组,撤销/重做时会按正确顺序批量执行其中的每条命令(sortBatchCommands保证增删顺序合理,见 util.ts)。

3.4 进阶:移动 + 嵌入合并

仓库还为"拖动节点使其嵌入父节点"这类高频交互提供了专门的合并逻辑:consolidateCommands()会检测 Undo 栈顶部是否同时包含cell:change:parentcell: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 // 切换启用状态
名称类型必选默认值描述
enabledboolean-是否启用历史状态;缺省时取反切换

对应的插件层实现为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(事件名)、dataprev/nextprops快照)、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)

校验不通过时:若cancelInvalidtrue(默认),会立即撤销该命令并从重做栈移除,同时触发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 图编辑应用提供了开箱即用的撤销/重做能力,其核心设计可以概括为三点:

  1. 命令栈模型:基于 Undo/Redo 双栈,配合stackSize限制与ignore*过滤,天然支持增删改三类操作的记录;
  2. 批处理机制startBatch/stopBatch/batchUpdate将多个改变折叠为一条历史记录,配合命令清洗与排序,保证复杂交互也能一步回退;
  3. 扩展钩子beforeAddCommandexecuteCommand、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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 9:48:49

APM飞控硬件调试与Mission Planner深度实践指南

简介&#xff1a;本资源为《APM中文使用手册》Word版PDF文档&#xff0c;面向无人机爱好者、嵌入式开发者及高校航模实践者&#xff0c;系统解决ArduPilotMega&#xff08;APM&#xff09;自驾仪的硬件选型、安装接线、传感器校准、地面站配置与自动化飞行任务部署等核心问题。…

作者头像 李华
网站建设 2026/9/17 9:46:36

CPU为什么不认识main函数?STM32启动流程深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 9:43:36

基于DeepSeek的酒店客诉知识库构建:从文本向量化到知识图谱实战

简介&#xff1a;以酒店业智能升级为切入点&#xff0c;围绕DeepSeek构建服务知识库、将客户投诉处理时长缩短75%的方案&#xff0c;面向酒店数字化管理者、AI技术实践者及关注大模型落地的学习者。资源包为一份PDF文档&#xff0c;共19页&#xff0c;大小约1.69MB&#xff0c;…

作者头像 李华
网站建设 2026/9/17 9:42:37

Claude Code 被官方登录拦下?TaoToken 的 Key 和 Base URL 这样填

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华