基于 Vuex 的时移操作(撤回/恢复)实现
最近做了一个 BI 平台的可视化看板编辑器,项目刚做完一期,各方面的功能都还能粗糙,但该有的也都有了,比如编辑器场景下最基本的两类时移操作-撤回(undo) 和恢复 (redo)。
用 vuex 实现的原理其实很简单,一句话就可以概括:维护一个 state快照 的历史记录数组和当前索引值, undo 和 redo 分别对应索引的回退(backward)的前移(forward)。
原理虽然简单,但代码实现还是要注意一些细节。
搭配源码@bugonly/vuex-undo-redo阅读口味更佳。
时间线不可逆
假设A为空白状态,依序进行以下操作:
- 新增一个组件1,进入状态B;
- 再次新增一个组件2,进入状态C;
- 执行undo操作,回退到状态B,组件2被清除,仅剩组件1;
- 新增一个组件3,进入状态D;
- 再次执行undo操作,回退到状态B,组件3被清除,仅剩一个组件1;
- 再次执行undo操作,组件1被清除,看板为空白状态,即状态A;
- 再次执行undo操作,提示无历史记录。
以上操作流程如下视频:
上述步骤中有争议的是步骤6,在测试过程中测试同事提出步骤6的表现应该是恢复到状态C,即组件2被恢复到看板中。如果是这样的话会发生以下问题:
- 状态B的 undo 操作结果会有两种:状态 A 和状态 C;
- 如何判断该什么时候回退到 A?什么时候回退到 C?
- 从状态B undo 回退到 C,再次 undo 应该回退到哪个状态?按时间线的话应该是回退到 B,那么再次 undo 呢?死循环?
之所以对步骤6的结果有争议,根本原因是混淆了编辑行为和时移行为。时移行为 undo/redo 恢复的是上一步/下一步的编辑行为,而时移行为本身是不被记录在操作历史栈中的,也就是说, undo 行为本身不能被 undo ,redo 行为本身不能被 redo。否则就会造成时间线混乱,难以管理。
时间线不可逆这条规则在所有类型的可视化编辑器中都是统一的,比如在线文档、IDE等等,大家有兴趣可以亲自去验证一下。
行为分类
并不是所有行为都是可以撤回的,理论上应该只有编辑行为可撤回,其他的比如页签之间的切换等简单交互的行为虽然也是状态机驱动(此处留个扣子,下文细聊),但并没有支持撤回的必要性,如果所有状态都能撤回反而令编辑器不好用。
所以在设计技术方案时,需要对用户行为进行归类,最基本要有三类:
- 支持撤回的行为;
- 不支持撤回的行为;
- 不支持撤回但是需要覆盖当前状态机快照的行为。
最后一种非常有必要,有些行为虽然本身不能撤回,但是在它之后的一些行为需要支持撤回,为了保持状态机的完整性,这类行为也必须记录下来,但是并不会作为一个独立的快照,而是覆盖当前快照。
举个例子。
- 页签1新增一个组件;
- 新增页签2;
- 页签2新增一个组件;
- 切换到页签1;
- 执行 undo,此时的表现是自动切换至页签2并且清除了页签2中的组件。
上述步骤中页签之间的切换行为就属于「不支持撤回但是需要覆盖当前状态机快照的行为」之一。在绝大多数交互场景中,页签之间的切换是没有必要使用 store 驱动的,往往是组件内部的状态机,上面示例之所以将它加入 store 就是为了实现视频中展示的 undo 自动切换页签效果。
这种方案比较简单有效,当然也有其他解决方案实现。
时移操作的作用域
这一点就很简单了,编辑器是应用的一个模块,在 vuex 中是 store 的一个 module,所以时移操作的插件函数在订阅 mutations 时需要判断 mutation-type,过滤非编辑器模块的 mutation。
const moduleFilterReg = new RegExp(`^${module}\/([a-zA-Z0-9\_]+)$`);
store.subscribe((mutation, state: Record<string, any>) => {
let mutationType = mutation.type;
if (moduleFilterReg){
const match = moduleFilterReg.exec(mutation.type);
// 过滤非指定模块的mutation
if (!match) {
return;
}
mutationType = match[1];
}
// ...其他逻辑
});
插件函数完整源码链接。
总结以上内容,时移操作插件的完整配置项如下:
interface IUndoRedoConfig {
/**
* 模块名称
* 如果指定模块则过滤此模块之外的所有 mutation
*/
module?: string;
/**
* 不跟踪的 mutation-type 清单
*/
noTraceMutationTypes?: string[];
/**
* 此列表中的 mutation-type 行为不跟踪,但是会覆盖当前历史记录
*/
needReplaceMutationTypes?: string[];
/**
* 过滤器,返回 false 时不执行后续逻辑
* 使用 filter 可以编写更复杂的过滤逻辑
* @param mutation
* @param state
*/
filter?: (mutation: MutationPayload, state:Record<string, any>) => boolean;
/**
* 历史记录容量,最小值1
*/
historyCapcity?: number;
}
页签域的时移操作如何实现?
最后留一个问题,这个问题我也暂时没想通最优解。目前市面上几乎所有的可视化编辑器都是这样的逻辑:时移操作的作用域的编辑器全局。
如何理解这句话呢?比如上文提到的报告编辑器,undo/redo 操作是针对报告 scope的,而不是页签 scope。报告编辑器可能有些人比较陌生,类比一种更普遍的编辑器:Excel。
Excel 的每个工作表(sheet)相当于报告中的页签,你试着在excel中执行以下步骤:
- 在 sheet 1 中任意编辑一次;
- 新建一个 sheet 2;
- 在 sheet 2 中任意编辑一次;
- 执行一次 undo,表现为 sheet 2中的编辑被还原;
- 再执行一次 undo,表现为 sheet 2 被整体清除;
- 再执行一次 undo,表现为 sheet 1中的编辑被还原。
以上步骤可以看出,excel 的 undo 行为是针对 excel 文档 scope 的,而不是每个 sheet 的 scope。
那么假如我想实现每个 sheet 域的时移操作呢?具体表现为:
- 每个 sheet 有单独的操作历史,互不影响;
- sheet 不能被时移操作删除,只能手动删除。
其实有很多种解决方案,最简单的就是每个 sheet 在 vuex store 对应一个 module,然后为每个 module 单独维护一个操作历史栈,这属于暴力解法,简单有效但很挫。也有更复杂的,比如基于图(Graph)数据结构做状态机发散,这属于自己牛逼同事看不懂的非工程解法,而且这个逻辑放在客户端会很重。所以这俩都不是最优解,更好的方案暂时不写了,因为我也没想出来...
基于 Vuex 的时移操作(撤回/恢复)实现的更多相关文章
- Elasticsearch--集群&时光之门和恢复控制
节点发现 启动一个Elasticsearch节点时,该节点会开始寻找具有相同集群名字并且可见的主节点.如果找到主节点,该节点加入一个已经组成了的集群:如果没有找到,该节点成为主节点(如果配置允许).形 ...
- 阿里云 Aliplayer高级功能介绍(四):直播时移
基本介绍 时移直播基于常规的HLS视频直播,直播推流被切分成TS分片,通过HLS协议向播放用户分发,用户请求的m3u8播放文件中包含不断刷新的TS分片地址:对于常规的HLS直播而言,TS分片地址及相应 ...
- VC++6.0和VS2005在编写MFC应用程序时,操作方面的差异
VC++6.0和VS2005在编写MFC应用程序时,操作方面的差异 一直用VC++6.0,对VS2005不太了解,下面简单的熟悉一下VS2005的一下功能,总结一下VS2005在编写MFC时候的应用. ...
- binlog2sql实现MySQL误操作的恢复
对于MySQL数据库中的误操作删除数据的恢复问题,可以使用基于MySQL中binlog做到类似于闪回或者生成反向操作的SQL语句来实现,是MySQL中一个非常实用的功能.原理不难理解,基于MySQL的 ...
- EasyDSS流媒体服务器实现RTMP直播同步HLS录像和时移回放
本文转自EasyDarwin团队成员Alex的博客:http://blog.csdn.net/cai6811376/article/details/74166337 "目前在市面上可以找到的 ...
- Mybatis基于代理Dao实现CRUD操作 及 Mybatis的参数深入
Mybatis基于代理Dao实现CRUD操作 使用要求: 1.持久层接口和持久层接口的映射配置必须在相同的包下 2.持久层映射配置中mapper标签的namespace属性取值必须是持久层接口的全限定 ...
- MySQL binlog2sql实现MySQL误操作的恢复
对于MySQL数据库中的误操作删除数据的恢复问题,可以使用基于MySQL中binlog做到类似于闪回或者生成反向操作的SQL语句来实现,是MySQL中一个非常实用的功能.原理不难理解,基于MySQL的 ...
- crontab误删操作的恢复与防范
1.crontab -r 误删操作的恢复 语句解析:crontab -e 编辑 与 crontab -r 删除,由于e, r在键盘上是紧邻的,一旦误操作 crontab -r 将会删除每个用户的定时任 ...
- 基于mykernel2.0编写一个操作系统内核
基于mykernel2.0编写一个操作系统内核 一. 实验准备 详细要求 基于mykernel 2.0编写一个操作系统内核 按照https://github.com/mengning/mykernel ...
随机推荐
- TortoiseGit冲突和解决冲突
产生冲突原因 产生:多个开发者同时使用或者操作git中的同一个文件,最后在依次提交commit和推送push的时候,第一个操作的是可以正常提交的,而之后的开发者想要执行pull(拉)和pull(推)操 ...
- 使用Hugo和GitHub搭建博客
折腾了几天博客的框架终于搭建起来了.研究了一番之后,最终还是选择使用Hugo和GitHub来搭建博客.本文介绍了如何使用Hugo来搭建静态博客网站,并将其部署在GitHub上.使用https://&l ...
- Go版本管理--依赖包存储
目录 1. 简介 2. GOPATH 依赖包存储 3.GOMODULE 依赖包存储 4.包名大小写敏感问题 1. 简介 GOPATH模式下,依赖包存储在$GOPATH/src,该目录下只保存特定依赖包 ...
- rabbitMQ重复消费(结合死循环重发那一篇看)
/** * 重复消费逻辑判断与处理 */ @Component public class RepeatMqConsumer { /** * 服务对象 */ private int count=1; @ ...
- Ubuntu 16.04LTS下eclipse连接mysql
第一部分:打开eclipse,新建一个web工程,新建一个类db_test.java(jdbc连接mysql的原理自行百度) import java.sql.*; public class db_te ...
- 关闭 Scroll Lock
通常,在电子表格中选择一个单元格并按箭头键时,所选内容会在各个单元格之间上下左右移动,具体取决于您按的箭头键.但是,如果在 Scroll Lock 处于开启状态时按箭头键,则向上或向下滚动一行.或者, ...
- rsync 服务搭建
rsync 服务搭建 服务端部署操作内容: 创建rsync用户和用户组 eg: useradd -s /sbin/nologin -M rsync 创建需要备份的指定目录,并修改权限 eg: mkdi ...
- Java最大栈深度有多大?-从一道面试题开始学习JVM
一.问题:Java最大支持栈深度有多大? 1.分析 有JVM的内存结构我们可知: 随着线程栈的大小越大,能够支持越多的方法调用,也即是能够存储更多的栈帧: 局部变量表内容越多,那么栈帧就越大,栈深度就 ...
- springMVC学习总结(三) --springMVC重定向
根据springMVC学习总结(一) --springMVC搭建搭建项目 在com.myl.controller包下创建一个java类WebController. 在jsp子文件夹下创建一个视图文件i ...
- 痞子衡嵌入式:MCUXpresso IDE下将关键函数重定向到RAM中执行的几种方法
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是MCUXpresso IDE下将关键函数重定向到RAM中执行的几种方法. 前段时间痞子衡写了一篇 <在IAR开发环境下将关键函数重 ...