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" 进行数据对比,当然自己也能写一个递归判断对象是否相同的方法。
如果有什么疑问或者建议,可以多多交流。文笔有限,才学疏浅,文中若有不正之处,万望告知。