基于Fabric.js实现撤销与恢复功能的完整方案

作者:像素猫

时间:2026-10-31

浏览:72

本文详解如何在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实现恢复和撤销的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章