前端快照实现方案

简介

snapshot 翻译为快照,用于直观获取页面在某个运行时的状态,将执行操作前后的快照进行存储,可以轻松实现页面状态的重做、撤销功能。

本文主要介绍 snapshot 工具实现的原理,以及其在项目中的使用。

设计

要实现页面状态的历史记录、重做、撤销,需要支持以下几个属性和方法

属性

  • 历史记录:存储历史的页面状态,包含页面初始化的状态 到 上一个页面状态
  • 撤销记录:存储重做的每一个操作记录,用于撤销后恢复
  • 当前记录:临时存储当前页面状态,主要用于下一次操作后,需要将其存储到历史记录
  • 上次插入数据时间:插入时间间隔太小时,需要额外处理
// 历史记录
recordList: string[] = []

// 撤销记录,用于重做
redoList: string[] = []

// 当前记录用 currentRecord 变量暂时存储,当用户修改时,再存放到 recordList
currentRecord = ''

// 上次插入数据时间
time = 0

方法

存储历史记录 push

当用户操作后,更新历史记录。需要考虑以下几点。

  • 当前操作时间距离上次插入时间小于 100 ms 时,则替换当前记录并取消执行添加
  • 如果当前记录有值,则说明上一次操作是手动插入,将之前缓存的当前记录推入 recordList,并且需要清空重做记录
  • 将当前状态存储到当前记录
  • 设置最大历史记录容量,当超过时,将最先插入的数据删除
push(record: PageData) {
  const nowTime = Date.now()
  // 防止添加重复的时间,当添加间隔小于 100ms 时,则替换当前记录并取消执行添加
  if (this.time + 100 > nowTime) {
    try {
      // 将 json 转成字符串存储
      this.currentRecord = JSON.stringify(record)
    } catch (error) {
      return false
    }

    return false
  }

  this.time = nowTime

  // 判断之前是否已经存在currentRecord记录,有则存储到recordList
  if (this.currentRecord) {
    this.recordList.push(this.currentRecord)
    // 增加记录后则应该清空重做记录
    this.redoList.splice(0, this.redoList.length)
  }

  // 存储当前记录
  this.currentRecord = JSON.stringify(record)

  // 最多存储 30 条记录,超过则删除之前的记录
  if (this.recordList.length > 30) {
    this.recordList.unshift()
  }
  return true
}
撤销操作 undo

当用户操作后,依赖 push 时存储的历史记录列表,将页面状态回退到上一次的状态,需要注意以下几点:

  • 当历史记录没有时,直接返回
  • 从历史记录中取出最后一次存储数据
  • 若当前记录存在,需要将其存放到重做记录列表
  • 需要清空当前记录,防止重复添加,因为撤销后,也会执行 push 存储历史记录方法
undo() {
  // 没有记录时,返回 false
  if (this.recordList.length === 0) {
    return null
  }

  const record = this.recordList.pop()

  // 将当前记录添加到重做记录里面
  if (this.currentRecord) {
    this.redoList.push(this.currentRecord)
  }

  // 丢弃当前记录,防止重复添加
  this.currentRecord = ''
  return JSON.parse(record as string) as PageData
}
重做操作 redo

当用户操作后,依赖 redoList 列表,将页面状态回退到撤销前的状态,需要注意以下几点:

  • 当重做记录没有时,直接返回
  • 从重做记录里取出最后一次存储数据
  • 如果当前记录有值,需要将其放到历史记录列表
  • 需要清空当前记录,防止重复添加,因为重做后,也会执行 push 存储历史记录方法
redo() {
  // 没有重做记录时,返回 false
  if (this.redoList.length === 0) {
    return null
  }

  const record = this.redoList.pop()
  // 添加到重做记录里面
  if (this.currentRecord) {
    this.recordList.push(this.currentRecord)
  }

  // 丢弃当前记录,防止重复添加
  this.currentRecord = ''
  return JSON.parse(record as string) as PageData
}

过程演示

假设数据列表为 [1, 2, 3, 4],当前属性值分别为:

recordList = [1, 2, 3]
redoList = []
currentRecord = 4

1、手动添加 5,则会执行 push 方法,执行后属性值分别为

recordList = [1, 2, 3, 4]
redoList = []
currentRecord = 5

2、执行1次撤销,则先会执行 undo,执行后属性值分别为

recordList = [1, 2, 3]
redoList = [5]
currentRecord = ''

然后执行 push,将 4 push 进去,执行后属性值分别为

recordList = [1, 2, 3]
redoList = [5]
currentRecord = 4

3、执行第2次撤销,则先会执行 undo,执行后属性值分别为

recordList = [1, 2]
redoList = [5, 4]
currentRecord = ''

然后执行 push,将 3 push 进去,执行后属性值分别为

recordList = [1, 2]
redoList = [5, 4]
currentRecord = 3

4、执行1次重做,则先会执行 redo,执行后属性值分别为

recordList = [1, 2, 3]
redoList = [5]
currentRecord = ''

然后执行 push,将 4 push 进去,执行后属性值分别为

recordList = [1, 2, 3]
redoList = [5]
currentRecord = 4

5、手动添加 6,则会执行 push 方法,执行后属性值分别为

recordList = [1, 2, 3, 4]
redoList = []
currentRecord = 6

完整代码

export default class Snapshot {
    // 历史记录
    recordList: string[] = []

    // 撤销记录,用于重做
    redoList: string[] = []

    // 当前记录用 currentRecord 变量暂时存储,当用户修改时,再存放到 recordList
    currentRecord = ''

    // 上次插入数据时间
    time = 0

    push(record: PageData) {
        const nowTime = Date.now()
        // 防止添加重复的时间,当添加间隔小于 100ms 时,则替换当前记录并取消执行添加
        if (this.time + 100 > nowTime) {
            try {
                // 将 json 转成字符串存储
                this.currentRecord = JSON.stringify(record)
            } catch (error) {
                return false
            }

            return false
        }

        this.time = nowTime

        // 判断之前是否已经存在currentRecord记录,有则存储到recordList
        if (this.currentRecord) {
            this.recordList.push(this.currentRecord)
            // 增加记录后则应该清空重做记录
            this.redoList.splice(0, this.redoList.length)
        }

        try {
            // 将 json 转成字符串存储
            this.currentRecord = JSON.stringify(record)
        } catch (error) {
            return
        }

        // 最多存储 30 条记录,超过则删除之前的记录
        if (this.recordList.length > 30) {
            this.recordList.unshift()
        }
        return true
    }

    undo() {
        // 没有记录时,返回 false
        if (this.recordList.length === 0) {
            return null
        }

        const record = this.recordList.pop()

        // 将当前记录添加到重做记录里面
        if (this.currentRecord) {
            this.redoList.push(this.currentRecord)
        }

        // 丢弃当前记录,防止重复添加
        this.currentRecord = ''
        return JSON.parse(record as string) as PageData
    }

    redo() {
        // 没有重做记录时,返回 false
        if (this.redoList.length === 0) {
            return null
        }

        const record = this.redoList.pop()
        // 添加到重做记录里面
        if (this.currentRecord) {
            this.recordList.push(this.currentRecord)
        }

        // 丢弃当前记录,防止重复添加
        this.currentRecord = ''
        return JSON.parse(record as string) as PageData
    }
}

在线演示

https://codepen.io/belle-peng/pen/xxMLzWK

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值