`
renzhen
  • 浏览: 253491 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

客户端JavaScript实战撤销恢复功能总结

阅读更多

      撤销和恢复功能,英文为:Undo和Redo,快捷键分别为Ctrl+Z和Ctrl+Y。它们是一组很常见的功能,在很多系统和软件中都有这两个功能。随着浏览器的功能越来越强,Web App将越来越占据重要位置,基于客户端JavaScript实现的撤销和恢复功能将越来越多,最近在工作中,完成了一个web系统的撤销和恢复功能,特写下一些心得以供分享。

      首先介绍一下我们系统基本内容:我们系统是一套建站系统,通过不同模块的拖拽和设置便可以简单的制作网站。其中的设置操作是可以撤销恢复的,如果你不小心误操作,也可以很方便的返回未设置前的状态。而我开发的就是这个功能。下面我就介绍开发的一些内容吧。

      1.不重新发明轮子,寻找合适的库帮忙

      撤销和恢复功能是一个常用的功能,也是一个实现起来比较繁琐的功能。如果自己重头实现,要自己设计类结构,设计底层的栈数据结构,维护状态,很不方便也很容易出错。所以一般不同的客户端技术都有对应的封装。客户端JS也不例外,它就是undo.js,https://github.com/jzaefferer/undo

      undo.js封装了一套比较通用的实现撤销和恢复功能架构,而它则帮你实现了内部的数据结构和状态的维护。使用undo.js主要有一下几步。

//初始化栈
var stack = new Undo.Stack();
//封装Command类
EditCommand = Undo.Command.extend({
				constructor: function(textarea, oldValue, newValue) {
					this.textarea = textarea;
					this.oldValue = oldValue;
					this.newValue = newValue;
				},
				execute: function() {
				},
				undo: function() {
					this.textarea.html(this.oldValue);
				},
				
				redo: function() {
					this.textarea.html(this.newValue);
				}
			});
//操作入栈
stack.execute(new EditCommand(text, startValue, newValue));
//undo
stack.undo();
//redo
stack.redo();
 

      2.轮子不利索,缝缝补补换新颜

      使用undo.js确实对于开发撤销和恢复功能提供了方便,原本可以结束了,那我也不写这篇文章了。不过在使用了undo.js几次后,我慢慢觉得代码有点别扭,于是我结合之前的开发重新回过头研究一下撤销和恢复的开发。在经过了一些研究后我发现其中Undo.Command的封装上过于简单了。虽然简单就是美,但是却导致了开发时一些代码大量的重复,介于此,我又在其上做了一层封装。

      首先说说Undo.Command的封装的一些问题:

      1.constructor、execute基本上是鸡肋,写法都一样。

      2.撤销和恢复要做的操作分为两个部分:逻辑和数据,很多操作undo、redo的逻辑是一样的,只不过是数据不同。比如:移动模块位置操作。

      3.缺少对于数据相关的函数,比如:setOldVal(val),setNewVal(val)

      4.缺少自动判断是否修改的过程,举个例子:以通过键盘移动为例,当模块在最右边界,实际并没有移动,就不需要入栈。

      5.缺少对于入栈的封装,比如:直接使用insert就可以,如果加一些insertWithNewVal(newval)、insertWithVals(oldval,newval)就更好了。

      于是我经过我的封装后,开发撤销恢复这可以这样写:

 EditCommand =Undo.Command.createGlobalCommand(function(val){
    val.textarea.html(val.content);
 },{returntype:'class'});
//入栈
new EditCommand().insertWithVals({textarea:text,content:oldvalue},{textarea:text,content:newvalue});

//你也可以这样直接入栈
Undo.Command.createGlobalCommand(function(val){
    val.textarea.html(val.content);
 }).insertWithVals({textarea:text,content:oldvalue},{textarea:text,content:newvalue});
 

 

     3. 高级应用,加入事务,多个Command合为一

      对于一些复杂的应用,一个大的操作需要由多个小操作组合而成,如果一个Undo Command也可以组合,那就完美了。在数据库中,有一个机制叫事务,可以让多步操作转为一个原子操作。我模仿了它也在undo.js中加入这个功能,保证了在事务中通过insert入栈的Command最终会统一成一个撤销恢复操作。示例代码如下:

Undo.Transaction.start();
//在这之间insert的Command会被转为一步撤销恢复操作


Undo.Transaction.end();
 

 

     4. 最高境界,彻底隐藏Undo Command  (未完成

       这部分每个系统都有不同做法,我提一下我的一些想法。

       在KindEditor等富文本编辑器中,它们也实现了撤销恢复操作,以KindEditor为例,它的API非常简单,addBookMark(),只要调用一下即可,为什么和我们前面看到的差那么多呢?关键在于我上面提到的两个东西:逻辑和数据。 对于KindEditor而言,其操作的这两个方面都是统一的,数据就是编辑器的html代码,逻辑就是把对应html代码塞入编辑器中。由此我们得出一个结论,只要逻辑和数据统一,我们就可以隐藏Undo Command。

       可是这个好像只对特殊的应用有效啊,不具备通用性啊,我不同操作的逻辑和数据都是不同的,如何统一?可以,我提供一个思路:关于数据的统一,我们可以构造一个数据容器,在这里统一管理数据,当容器中某数据更改了,就会自动生成一个Command。那逻辑呢,怎么统一,这个可以使用类似event的分发机制,统一函数入口,只要保证数据和逻辑的一致性就可以了。

        如果遇上了一个操作需要多个数据变化怎么办?这时可以使用事务机制(现在知道它的用途了吧)

        问题是怎么找到合适的切入点,这个我也没有好的办法,大家根据自己应用的特点加以寻找,基本上所有应用都有这样的切入点。

        以我公司的建站系统为例:

        我把每个模块加入一个容器管理数据,当一个操作改变模块,会想容器加入新的数据,比如:拖动位置后会修改pos字段,便会自动生成一个undo Command。当使用undo调用这个Command的函数时,它会向这个模块发送一个类似event中trigger的回调,并将对应的参数比如:数据key、value,undotype(是undo还是redo操作),由模块回调负责具体模块。这样就隐藏了Undo Command的底层实现,只专注逻辑了。

 

        最后,我把改进的undo.js发布在了iteye的代码收藏里了,其中的代码很粗糙,如果有兴趣就将就着看一下,你可以通过如下网址查看: http://renzhen.iteye.com/code  。

        作为一个决心成为优秀开发人员的我,竟然我怎么会画图,只能不停的打字,决定好好学学画流程图,一图胜万语啊。还有iteye的富文本编辑器排版有点烂,不好使,搞了我好久。

 

分享到:
评论
1 楼 星光1224 2014-03-12  
谢谢分享~~
有一个问题:
http://renzhen.iteye.com/code
请问上面链接里面的undo.js代码可以加注释吗?
后面的扩展代码有点看不懂!
加了扩展代码后应该如何调用?

相关推荐

    gridview+72绝技.rar

    17. 脚本控制:利用JavaScript和jQuery进行客户端操作,如排序、分页等。 18. 绑定复杂对象:处理包含多层嵌套的对象,如List, object>>。 19. 响应式设计:使GridView适应不同的屏幕尺寸和设备,提供良好的移动...

    board-draw:与您的朋友在线绘画

    - **撤销/重做功能**: 实现Crl + Z快捷键功能,让用户能够撤销最近的操作,增加编辑的灵活性。 总的来说,"board-draw"是一个利用现代Web技术实现的在线协作绘画应用,适合朋友间进行互动娱乐或教育场景下的远程...

    源码免费课程下载

    3. **消息传输**:学习如何在客户端与服务器之间建立稳定的消息传输通道,实现即时消息传递功能。 4. **安全性考虑**:考虑如何保护用户隐私,防止恶意攻击。 ### 方案:web程序中购物车的应用 购物车是电子商务...

    react-googlesheet-API:在这个项目中使用了react和Google Sheet API

    此外,你还可以实现保存、撤销和重做等高级功能,以增强用户体验。 在实际应用中,你可能还需要考虑错误处理和数据同步问题。例如,当网络连接不稳定或API请求失败时,应有适当的错误提示和恢复机制。此外,由于API...

    poc-oidc-react-generic2

    【标题】"poc-oidc-react-generic2" 指的是一个开源的...这个"poc-oidc-react-generic2"项目提供了一个实践OIDC身份验证的实战平台,对于想要在React应用中实现安全登录功能的开发者来说,是一个非常有价值的参考资料。

    django-google-login:我的 oauth2 简单应用程序通过谷歌或简单直接登录登录到 django

    "JavaScript" 这个标签可能表示在这个项目中,JavaScript 可能被用来实现某些前端功能,如按钮点击事件、表单提交等,尽管 Django 主要是后端框架,但在现代Web开发中,前端交互通常会用到 JavaScript。 【文件名称...

    Eclipse_Swt_Jface_核心应用_部分19

    3.3 常用的代码编辑功能 28 3.3.1 添加注释 28 3.3.2 自定义格式化代码 28 3.3.3 自动生成getter和setter代码 30 3.3.4 代码的重构 31 3.3.5 查看源代码 31 3.3.6 代码的展开和折叠 32 3.3.7 代码比较...

Global site tag (gtag.js) - Google Analytics