ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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/x61.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表示不限制设置为其他数字表示最多只记录该数量的历史记录ignoreAddbooleanfalse否为true时添加元素不会被记录到历史记录ignoreRemovebooleanfalse否为true时删除元素不会被记录到历史记录ignoreChangebooleanfalse否为true时元素属性变化不会被记录到历史记录beforeAddCommand(event, args) any-否命令被加入 Undo 队列前调用若返回false该命令不会入队afterAddCommand(event, args, cmd) any-否命令被加入 Undo 队列后调用executeCommand(cmd, revert, options) any-否命令被撤销或重做时调用revert为true表示撤销否则为重做此外源码还暴露了几个文档未列入基础表的进阶选项定义见 type.tseventNames自定义需要监听的模型事件名数组默认监听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 / stopBatchgraph.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快照addCommandbatch: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.tsundo()从 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/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 验证器阻止非法状态Validatorvalidator.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),仅供参考
返回列表