基于Fabric.js实现撤销与恢复功能的完整方案
本文详解如何在Fabric.js中实现撤销与恢复功能。核心思路是利用fabric导出导入JSON的能力,通过监听object:added、modified等事件捕获画布变更。重点介绍了使用toDatalessJSON减小序列化体积,并通过undoStack和redoStack管理状态历史。此外,还涵盖了防止重复保存、限制堆栈大小以节省内存,以及批量操作时的自定义保存时机控制策略。
介绍
在图形编辑器开发中,撤销和恢复是不可或缺的基础功能。然而,网络上关于此功能的详细教程相对较少,这可能与Canvas生态系统的资料稀缺性有关。
实际上,实现这一功能的核心在于Fabric.js原生支持将当前画布内容序列化为JSON格式,并支持从JSON导入还原画布。利用这两项基础能力,即可构建状态回溯机制。
在具备上述导出与导入能力后,关键问题转化为如何准确监听画布状态的变更,以及如何高效地存取这些状态数据。
本文采用一种直观且直接的方法:定义两个数组栈,即undoStack(撤销栈)和redoStack(恢复栈),分别用于记录可撤销的历史状态和可恢复的历史状态。
class CanvasStateManager {
protected canvas: canvas
protected editor: IEditor
private undoStack: string[] = []
private redoStack: string[] = []
constructor(canvas: Owl.ICanvas, editor: Owl.IEditor) {
this.canvas = canvas
this.editor = editor
}
}
export default CanvasStateManager
何时更新存储的状态?
需要监听的方法
回到监听画布状态变更的问题,处理方案相对直接:通过订阅Fabric.js提供的回调事件来捕获变化。通常情况下,监听以下四个事件已足以覆盖大多数画布变更场景。
'object:added'
'object:removed'
'object:modified'
'object:skewing'
这四个事件足以收集画布的变更,甚至object:skewing事件在多数场景下并非必需。考虑到某些情况下可能需要动态取消监听,代码中定义了两个辅助方法:initHistoryListener(初始化监听)和offHistoryListener(移除监听)。
class CanvasStateManager {
protected canvas: canvas
protected editor: IEditor
private undoStack: string[] = []
private redoStack: string[] = []
constructor(canvas: Owl.ICanvas, editor: Owl.IEditor) {
this.canvas = canvas
this.editor = editor
this.initHistoryListener()
}
initHistoryListener = async () => {
this.canvas.on({
[ICanvasEvent.OBJECT_ADDED]: this.saveStateIfNotRestoring,
[ICanvasEvent.OBJECT_MODIFIED]: this.saveStateIfNotRestoring,
[ICanvasEvent.OBJECT_REMOVED]: this.saveStateIfNotRestoring
})
}
offHistoryListener = () => {
this.canvas.off(ICanvasEvent.OBJECT_ADDED, this.saveStateIfNotRestoring)
this.canvas.off(ICanvasEvent.OBJECT_MODIFIED, this.saveStateIfNotRestoring)
this.canvas.off(ICanvasEvent.OBJECT_REMOVED, this.saveStateIfNotRestoring)
}
}
export default CanvasStateManager
如何保存画布变更的状态
将当前画布转换为JSON
// 获取当前画布的 JSON 描述
const canvasState = this.canvas.toDatalessJSON()
const currentStateString = JSON.stringify(canvasState)
在保存状态时,这里选用toDatalessJSON()方法而非普通的toJSON(),主要基于以下数据优化考量。
- toDatalessJSON主要用于在需要减小序列化后数据大小的情况下,特别是在处理复杂的SVG图形时。由于SVG图形载入后通常是以ObjectPaths来保存的,因此大的SVG图形会有很多的Path数据,直接序列化会导致JSON数据过长。
- toDatalessJSON方法可以将这些Path数据用路径来代替,以减小序列化后的数据量。但需要注意的是,这需要手动设置sourcePath以便在下次使用时能够找到对应的资源。
toDatalessJSON 和 toJSON 的主要区别
- toJSON方法会完整地将画布上的所有对象及其属性序列化为JSON数据,包括Path等详细数据。
- toDatalessJSON则会尝试优化这些数据,通过用路径代替详细数据来减小数据量。
判断当前状态和撤销堆栈中最后一个状态是否相同。这里需要进行边界处理:如果当前保存的状态与撤销栈中的最后一个状态完全一致,则无需再次保存。此举旨在避免冗余保存对撤销和恢复逻辑造成干扰。
// 判断当前状态和撤销堆栈中最后一个状态是否相同
if (this.undoStack.length > 0) {
const lastUndoStateString = this.undoStack[this.undoStack.length - 1]
if (currentStateString === lastUndoStateString) {
// 如果当前状态和最后一个撤销状态相同,则不保存
console.log('Current canvas state is identical to the last saved state. Skipping save.')
return
}
}
将画布状态保存到撤销堆栈
// 将画布状态保存到撤销堆栈
this.undoStack.push(currentStateString)
限制撤销堆栈的大小以节省内存
为防止堆栈无限增长占用过多内存,需对保存的状态数量进行限制。此处设定最大保存30步,当超出限制时,自动移除最旧的状态记录。
private readonly maxUndoStackSize: number = 30 // 最大撤销堆栈大小
...
// 限制撤销堆栈的大小以节省内存
if (this.undoStack.length > this.maxUndoStackSize) {
this.undoStack.shift() // 移除最旧的状态
}
如何自定义保存的状态或时机
在许多场景中,并非每一步操作都需要独立保存。例如在批量创建元素时,若将每一次插入操作都单独记录,撤销时只能逐个移除,这与用户“一次性创建N个元素”的操作意图不符。因此,需要自定义保存的触发时机。本文暂且定义两种控制方式。
- 忽略下一次画布变更的保存
- 自定义停止在当前流程中的状态保存,以及自定义开始保存
忽略下一次画布变更的保存
实现这一功能非常简单,只需定义一个布尔状态位来标记是否忽略下一次保存即可。
// 用于忽略下一次操作的保存
private ignoreNextSave: boolean = false
ignoreNextStateSave = () => {
this.ignoreNextSave = true
}
在每次执行保存逻辑时,将该状态位重置为false,确保后续操作正常记录。
private saveStateIfNotRestoring = () => {
if (!this.ignoreNextSave && this.hasListener) {
this.saveCustomState()
}
this.ignoreNextSave = false // 重置标志
}
自定义停止在当前流程中的状态保存,以及自定义开始保存
这一机制与前述方法类似,同样通过定义一个状态位来控制当前流程是否允许保存。
private hasListener: boolean = true
changeHistoryListenerStatus = (hasListener: boolean) => {
this.hasListener = hasListener
}
不同之处在于,该状态位的开关由用户代码直接控制,提供了更高的灵活性。
自定义撤销功能
在处理撤销操作时,需特别注意:恢复画布状态的过程中会多次触发Fabric的回调事件。因此,在执行恢复操作期间,必须暂时禁用事件监听,待状态恢复完成并渲染后,再重新注册监听器。
customUndo = () => {
if (this.undoStack.length > 1) {
// 取消事件监听器
this.offHistoryListener()
// 将当前状态弹出并保存到恢复堆栈
this.redoStack.push(this.undoStack.pop()!)
// 获取撤销后的状态
const previousState = this.undoStack[this.undoStack.length - 1]
this.canvas.clear()
// 临时禁用事件监听, 但是点击一次存在多次监听更新的情况下不管用,所以可以考虑手动去掉事件监听器
this.isRestoring = true
this.canvas.loadFromJSON(previousState, () => {
// 重新注册事件监听器
this.initHistoryListener()
this.canvas.renderAll()
this.isRestoring = false
})
}
}
自定义恢复功能
恢复功能的实现逻辑与撤销类似,同样需要在操作期间暂停监听,并在操作结束后恢复监听,以确保状态同步的准确性。
customRedo = () => {
if (this.redoStack.length > 0) {
// 取消事件监听器
this.offHistoryListener()
// 将最后的恢复状态弹出并保存到撤销堆栈
this.undoStack.push(this.redoStack.pop()!)
// 获取恢复的状态
const nextState = JSON.parse(this.undoStack[this.undoStack.length - 1])
// 临时禁用事件监听
this.isRestoring = true
this.canvas.clear()
this.canvas.loadFromJSON(nextState, () => {
// 重新注册事件监听器
this.initHistoryListener()
this.canvas.renderAll()
this.isRestoring = false
})
}
}
整体实现
class CanvasStateManager {
protected canvas: Owl.ICanvas
protected editor: IEditor
private undoStack: string[] = []
private redoStack: string[] = []
private isRestoring: boolean = false
// 用于忽略下一次操作的保存
private ignoreNextSave: boolean = false
private hasListener: boolean = true
private readonly maxUndoStackSize: number = 30 // 最大撤销堆栈大小
static apis = [
'clearCustomHistory',
'saveCustomState',
'customUndo',
'customRedo',
'ignoreNextStateSave',
'initHistoryListener',
'offHistoryListener',
'changeHistoryListenerStatus'
]
constructor(canvas: Owl.ICanvas, editor: Owl.IEditor) {
this.canvas = canvas
this.editor = editor
// 初始状态
this.saveCustomState()
this.initHistoryListener()
}
private saveStateIfNotRestoring = () => {
if (!this.isRestoring && !this.ignoreNextSave && this.hasListener) {
console.log('saveStateIfNotRestoring -> saveCustomState')
this.saveCustomState()
}
this.ignoreNextSave = false // 重置标志
}
clearCustomHistory = () => {
this.undoStack = []
this.redoStack = []
this.saveCustomState()
}
saveCustomState = () => {
// 获取当前画布的 JSON 描述
const canvasState = this.canvas.toDatalessJSON()
const currentStateString = JSON.stringify(canvasState)
// 判断当前状态和撤销堆栈中最后一个状态是否相同
if (this.undoStack.length > 0) {
const lastUndoStateString = this.undoStack[this.undoStack.length - 1]
if (currentStateString === lastUndoStateString) {
// 如果当前状态和最后一个撤销状态相同,则不保存
console.log('Current canvas state is identical to the last saved state. Skipping save.')
return
}
}
// 将画布状态保存到撤销堆栈
this.undoStack.push(currentStateString)
// 输出保存信息
console.log('saveCustomState', this.undoStack, this.redoStack)
// 限制撤销堆栈的大小以节省内存
if (this.undoStack.length > this.maxUndoStackSize) {
this.undoStack.shift() // 移除最旧的状态
}
}
customUndo = () => {
if (this.undoStack.length > 1) {
// 取消事件监听器
this.offHistoryListener()
// 将当前状态弹出并保存到恢复堆栈
this.redoStack.push(this.undoStack.pop()!)
// 获取撤销后的状态
const previousState = this.undoStack[this.undoStack.length - 1]
this.canvas.clear()
// 临时禁用事件监听, 但是点击一次存在多次监听更新的情况下不管用,所以可以考虑手动去掉事件监听器
this.isRestoring = true
this.canvas.loadFromJSON(previousState, () => {
// 重新注册事件监听器
this.initHistoryListener()
this.canvas.renderAll()
this.isRestoring = false
})
}
}
customRedo = () => {
if (this.redoStack.length > 0) {
// 取消事件监听器
this.offHistoryListener()
// 将最后的恢复状态弹出并保存到撤销堆栈
this.undoStack.push(this.redoStack.pop()!)
// 获取恢复的状态
const nextState = JSON.parse(this.undoStack[this.undoStack.length - 1])
// 临时禁用事件监听
this.isRestoring = true
this.canvas.clear()
this.canvas.loadFromJSON(nextState, () => {
// 重新注册事件监听器
this.initHistoryListener()
this.canvas.renderAll()
this.isRestoring = false
})
}
}
ignoreNextStateSave = () => {
this.ignoreNextSave = true
}
changeHistoryListenerStatus = (hasListener: boolean) => {
this.hasListener = hasListener
}
initHistoryListener = async () => {
this.canvas.on({
[ICanvasEvent.OBJECT_ADDED]: this.saveStateIfNotRestoring,
[ICanvasEvent.OBJECT_MODIFIED]: this.saveStateIfNotRestoring,
[ICanvasEvent.OBJECT_REMOVED]: this.saveStateIfNotRestoring
})
}
offHistoryListener = () => {
this.canvas.off(ICanvasEvent.OBJECT_ADDED, this.saveStateIfNotRestoring)
this.canvas.off(ICanvasEvent.OBJECT_MODIFIED, this.saveStateIfNotRestoring)
this.canvas.off(ICanvasEvent.OBJECT_REMOVED, this.saveStateIfNotRestoring)
}
}
export default CanvasStateManager
以上就是使用fabric实现恢复和撤销功能的实例详解的详细内容,更多关于fabric实现恢复和撤销的资料请关注本站其它相关文章!
适合JavaScript初学者的实战项目:制作简易待办事项列表
2026-11-05 16:17:07
JS打开新标签页时怎么通过URL传递参数
2026-11-05 12:21:08
sigma.js 自定义节点样式:设置颜色、大小及标签显示的技巧
2026-11-05 09:55:00
JS 生成的统计图怎么导出为 PNG 图片或 PDF 文件?
2026-11-05 09:37:58
JavaScript事件处理实例:点击、输入与表单验证代码
2026-11-05 09:06:19
