vue 低代码平台 实现 ctrl+z 撤销, ctrl+y 重做功能

3,778 阅读3分钟

1.背景

最近公司有个低代码平台的项目,需要用户进行拖拉拽操作每个组件属性生成json,然后经过编译器生成页面,产品要求加上撤销和重做的功能。

加!都可以加!随便加!

整理了一下思绪,得出需求:

  • 不用做持久化,刷新后自动清空历史
  • 限制操作历史最大储存条数
  • 撤销到任意N步之后,如果有任何的修改,删除N步之前所有记录
  • 需要记录当前操作历史下标

2.需求实现

根据以上需求列表,采用vuex来实现。

2.1 创建仓库(Store)

export const operateRecord = {
    namespaced: true,
    state:{
        rollbackList:[],    // 存储操作记录的数组
        maxStep:20,         // 最大储存条数
        currIndex:0         // 当前操作历史下标
    },
    mutations:{
    },
    actions:{
    }
};
import Vue from 'vue';
import Vuex from 'vuex';
import { operateRecord } from '@/store/modules/operate-record';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    operateRecord
  }
});

到此仓库模块化创建完毕,并设置命名空间。

2.2 创建修改stata的mutation,为后续action准备

首先需要一个往记录数组添加新数据的方法,同时更新下标

PUSH_RECORD (state, data) {
  state.rollbackList.push(data);
  state.currIndex = state.rollbackList.length - 1;
}

当添加的操作历史超出了设定的最大存储条数时,需要删除最旧的一条

LIMIT_RECORD (state) {
  if (state.rollbackList.length > state.maxStep) {
    state.rollbackList.shift();
  }
}

当用户撤销的时候,在某个节点停住,重新开始修改,新的操作历史进来之前,需要将当前撤销步骤后面所有的历史清除

REMOVE_RECORD (state) {
  state.rollbackList.splice(state.currIndex + 1, state.rollbackList.length);
}

最基本的撤销与重做

NEXT_INDEX (state) {
  state.currIndex += 1;
},

PREV_INDEX (state) {
  state.currIndex -= 1;
}

所有mutation准备完毕

2.3 actions操作

actions里面的方法,就是与实际业务接轨的地方了。
暴露一个追加记录的action,每次追加的时候,检测是否超出最大存储条数。

pushRecord ({ commit, state }, data) {
    commit('PUSH_RECORD', data);
    commit('LIMIT_RECORD');
}

暴露一个异步的重做action
这里需要判断,如果操作记录已经处于最新节点,就不要再前进了,但是值还是要照样返回,如果不返回,调用的地方拿到的就是undefined,赋值的时候就会出大问题

redoRecord ({ commit, state }) {
  return new Promise((resolve) => {
    if (state.currIndex < state.rollbackList.length - 1) {
      commit('NEXT_INDEX');
    }
    // deepClone是封装好了深度复制对象的方法,可以用JSON.parse(JSON.stringify())替换
    resolve(deepClone(state.rollbackList[state.currIndex]));
  });
}

同理,暴露一个异步的撤销action
判断一下如果已经处于最旧的节点,就不要再后退了

undoRecord ({ commit, state }) {
  return new Promise((resolve) => {
    if (state.currIndex > 0) {
      commit('PREV_INDEX');
    }
    resolve(deepClone(state.rollbackList[state.currIndex]));
  });
}

OK,目前actions里面的逻辑暂时写完

3.实际业务

在业务层中,监听了数据的变化,每次数据变化,就会进行一次记录,同时缓存最新操作,保证浏览器刷新后能够数据还原,同时监听键盘事件,判断常用的 ctrl+z 和 ctrl+y 快捷键。

watch: {
  editorData: {
    handler (val) {
      this.stepRecord(val);
    },
    deep: true
  }
},

mounted () {
  window.addEventListener('keydown', this.onKeydown);
  // 利用hook,在监听的地方解绑监听
  this.$once('hook:beforeDestroy', () => window.removeEventListener('keydown', this.onKeydown));
}

methonds: {
    onKeydown (e) {
      if (e.ctrlKey || e.metaKey) {
        if (e.key === 'z') {
          // 这就是为什么要始终返回值,应为如果不返回,Promise会自动返回一个undefined,data就会被赋值,然后页面出问题
          this.$store.dispatch('operateRecord/undoRecord').then(this.setEditorData);
        }
        if (e.key === 'y') {
          this.$store.dispatch('operateRecord/redoRecord').then(this.setEditorData);
        }
      }
    },
    // 记录操作步骤
    stepRecord (data) {
      // 这里只是我封装好了的一个防抖方法
      if (!this.record) {
        this.record = debounce((val) => {
          this.$store.dispatch('operateRecord/pushRecord', val);
          // local-storage本地缓存
          set('editorData', val);
        }, 500);
      }
      return this.record(data);
    }
}

事情到这里貌似已经结束了,但是细心的同学,肯定发现了,在actions中的pushRecord这个方法,没有判断什么时候 改记录,什么时候删除记录。

照成的后果就是,每次撤销或者重做的时候,会通过watch,将数据又重新添加到记录数组中。

所以我们需要在撤销和重做的时候禁止添加新记录,判断依据,则是靠一个第三方库:json-diff
通过判断进栈数据与数组中最新历史对比,不一致则放行添加。

同时,如果用户在撤销的途中,进行了修改,则需要删除之前当前下标之后所有的历史数据,判断依据则是根据下标与数据长度比对。

修改后的pushRecord

import { diff } from 'json-diff';

pushRecord ({ commit, state }, data) {
  if (diff(data, state.rollbackList[state.currIndex])) {
    if (state.currIndex !== state.rollbackList.length - 1) {
      commit('REMOVE_RECORD');
    }
    commit('PUSH_RECORD', data);
    commit('LIMIT_RECORD');
  }
}

现在,就可以愉快的ctrl+z,ctrl+y 了

4.结语

以上就是vue中撤销重做功能的所有代码了,最重要的一点就是使用 "json-diff" 进行数据对比,当然自己也能写一个递归判断对象是否相同的方法。

如果有什么疑问或者建议,可以多多交流。文笔有限,才学疏浅,文中若有不正之处,万望告知。