最近在做一个网页版的 svg 编辑器,为此学习了编辑器相关方面的知识。本文是我的一些粗浅学习总结,希望可以给初学者一些思路。

前面的话

随着近几年前端技术的快速发展,人们更倾向于将应用开发放到网页浏览器上,即 B/S 架构 。相比与传统的 C/S 模式,它的兼容性更好,开发成本更低,且不需要安装,只要打开浏览器的一个页面即可。

Web 的图形编辑器主要使用到了 HTML5 的 Canvas 技术和 SVG 技术。Canvas 是使用 JavaScript 程序绘图,SVG是使用XML文档描述来绘图。SVG 是基于矢量的,放大缩小不失真。而 Canvas 是基于位图的,适合做像素处理,也很适合做 HTML5 小游戏。它们各有优劣,开发时具体使用哪种方案,需要根据自己的需求进行选择。

而我要做的是一个 SVG 编辑器,所以毫无疑问选择了 SVG 技术方案。此外,为更方便的操作 SVG,且使代码有更好的的可读性,而使用了 svg.js 库。svg.js 提供了可读性很好的链式写法,另外这个对学习 svg 也有很大帮助(通过简单的代码就可以生成一个svg )。我会在代码中和 svg.js 相关的代码旁边写上注释,所以你不会 svg.js 也能看懂我的代码。

功能描述

撤销(undo):返回到最后一个操作前的状态。

重做(redo):如果撤销过程中,发现过度撤销,可以通过 “重做”,进入某一个操作后的状态。

一般来说,稍微复杂点的编辑器都是有 撤销/重做 功能的。撤销重做 是一款编辑器的基础功能,它让用户在进行错误操作后,可以让编辑器回滚到错误操作前的状态。

选择实现方案

基于对象序列化的Undo/Redo

实现undo/redo 功能,其中一个方法是 基于 对象序列化 的Undo/Redo 。

每进行一个操作,就 将之前的所有对象序列化(即存储当前视图状态到一个变量中) ,将其推入到名为 undoStack 的栈中。当需要撤销时,undoStack 出栈,将出栈的数据进行解析,还原到 UI 层,此时还要将出栈的序列化数据推入到 redoStack 栈内。

这种模式,优点是代码容易实现,复杂度较低,缺点是当对象数量越多,每次保存状态都要使用的内存也就越大,所以并不是编辑器的首选解决方案。

基于命令模式的 Undo/Redo

命令模式则是 给每一个操作创建一个 command 对象,该对象记录了具体的执行方法(execute)和一个逆执行方法(undo) 。编辑器每进行一次操作,对应的 command 对象会被创建,并执行该命令对象的 execute 方法,然后将这个对象 推入到 undo 栈中。

当用户撤销(undo)时,如果 undo 栈中不为空,弹出 undo 栈顶的 command 对象,执行它的 execute 方法,然后将这个对象推入到 redo 栈中。

重做(redo)的操作和上面类似。如果 redo 栈不为空,弹出栈顶对象,执行 execute 方法,并把这个对象推入到 undo 栈中。

每次进行一个操作时,而创建一个新的 command 时,如果 redo 栈 不为空,将其清空。

有些操作可能是多个操作的组合,这时候需要用到设计模式中的 “组合模式”,将多个操作包装成一个组合操作。每次 execute 和 redo 都遍历组合操作下的子操作。

这种模式因为记录的只是 正向操作 和 逆向操作,自然占用的内存和对象的多少无关。但因为需要推导出每个操作的逆向操作,代码实现比前一种模式复杂,且不能复用。

示例编辑器的撤销重做功能使用了这种模式。

实现

教程示例源代码地址:github.com/F-star/web-…

演示地址:f-star.github.io/web-editor-…

代码部分参考了 svg-edit (一款开源基于web的,Javascript驱动的 svg 绘制编辑器) 的实现。

准备工作

首先我们创建一个 index.html 文件,里面用一个 div#drawing 元素来放 我们的 svg 元素。

为了让代码可读性更好,我使用了 ES6 的模块化,写好后用 babel 编译下就好。

如果要开发比较复杂的编辑器,模块化还是必要的,模块化可以降低代码的耦合度,也更方便进行单元测试。此外还可以考虑引入 typescript 来提供静态类型化,因为开发一个编辑器,无疑要使用到非常多的方法,传入的参数如果不能保证类型的正确,可能会导致意想不到的错误。

下面正式开始编写代码。

首先我们引入 svg.js 库,接着引入我们的入口文件 index.js,并给这个 script 的 type 设置为 module,以获得原生的 ES6 模块化支持。所以你要保证运行下面 html 的浏览器可以支持 ES6 模块化。

<body><div id="drawing"></div><script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.6/svg.js"></script><script src="./index.js" type="module"></script>
</body>
复制代码

然后我们开始编写 history.js 文件的相关代码。这里我使用了 ES6 的 class 语法,因为这种写法相比 “原型继承” 的写法,明显可读性更好。当然你也可以用 “原型继承” 的写法,class 只是它的语法糖。

命令类

首先我们创建一个命令基类。

// history.js
// 命令基类
class Command {constructor() {}execute() {throw new Error('未重写execute方法!');     // 继承时如果没有覆盖此方法,会报错。通过这种方式,保证继承的子命令类重写此方法。}undo() {console.error('未重写undo方法!');        // 同上}
}
复制代码

然后我们就可以根据业务逻辑,包装成一个个子命令类,在需要的时候实例化。下面的 InsertElementCommand 类的作用是创建新元素。

// history.js
// 创建不同元素的方法集合
const InsertElement = {// 在 svg 元素下,创建了一个宽高为 size,位于 [x, y],内容为 content 的 text 元素,// 并返回了这个节点对象的引用(svgjs包装后的对象)。text(x, y, size, content='') {return draw.text(content).move(x, y).size(size);}// 这里还可以写 rect, circle 等方法。
}// 插入元素命令类
export class InsertElementCommand extends Command {// 指定 元素类型 和 需要保存的状态。constructor(type, ...args) {super();this.el = null;this.type = type;this.args = args;}execute() {// 这里写创建的方法console.log('exec')this.el = InsertElement[this.type](...this.args);}undo() {console.log('undo')// 移除元素this.el.remove();}
}
复制代码

这里为了更好的通用性,我们创建了一个 InsertElement 对象,里面保存了创建不同类型的各种方法。这个对象其实就是设计模式中 “策略模式” 中 的策略对象。这里,我们对 text 类型的创建代码写在了 InsertElement 对象的 text 方法中了。

CommandManager 对象

这样,我们就写好一个具体的命令类了。接下来,我们需要写一个命令管理对象(CommandManager)来管理我们的创建的所有命令。

// history.js// 命令管理对象
export const cmdManager = (() => {let redoStack = [];        // 重做栈let undoStack = [];        // 撤销栈return {execute(cmd) {cmd.execute();                  // 执行executeundoStack.push(cmd);       // 入栈 redoStack = [];            // 清空 redoStack},undo() {if (undoStack.length == 0) {alert('can not undo more')return;}const cmd = undoStack.pop();cmd.undo();redoStack.push(cmd);}, redo() {if (redoStack.length == 0) {alert('can not redo more')return;}const cmd = redoStack.pop();cmd.execute();undoStack.push(cmd);},}
})();
复制代码

每当我们创建一个 Command 对象后,就要调用 cmdManager.execute(cmd) 方法后,它会执行 Command 对象的 execute 方法,并将这个 Command 对象推入 undoStack 中。

redo/undo 栈的实现方式有很多种,这里为了让代码更直观简单,直接用两个数组来保存两个栈。

而在 svg-edit 中,则使用了双向链表的方式:使用了一个数组,并给了一个指针,指向一个 Command 对象。指针左边是 undoStack,右边为 redoStack。这样每次撤销重做时,只要修改指针位置,而不需要修改对数组进行操作,时间复杂度更低。

进一步包装

通过下面这样的代码,我们就可以执行并保存每一步操作了。

let cmd = new InsertElementCommand('text', x, y, 20, '好');
cmdManager.execute(cmd);
复制代码

但如果每个操作都要写下面这样的代码,无疑有些累赘。于是我从 js 原生的方法 document.execCommand 获得了灵感,在全局添加了一个 executeCommand 方法。

// commondAction.jsimport {InsertElementCommand,cmdManager,
} from './history.js'const commondAction = {drawText(...args) {let cmd = new InsertElementCommand('text', ...args);cmdManager.execute(cmd);},undo() {cmdManager.undo();},redo() {cmdManager.redo();}
}// executeCommond 设置为全局方法
window.executeCommond = (cmdName, ...args) => {commondAction[cmdName](...args);
}
复制代码

然后我们通过下面这种方式,就能在任何位置创建 command 对象,并执行它的 execute 命令。

executeCommond('drawText', x, y, 20, '好');
executeCommond('undo');
executeCommond('redo');
复制代码

随着命令的扩展,我们可以在对第一参数 cmdName 进行解析,判断是创建一个元素,还是修改一个元素的一些参数等(如'create rect', 'update text'),然后调用对应的各种方法。

最后我们在入口 index.js 文件内,将这些命令绑定到事件响应事件上就完事了。

课后练习

你可以下载我在 github 上提供的源码,试着添加 “创建 rect 的功能。

如果你想挑战一下的话,还可以写一个移动元素的功能。如果还要考虑交互的话,会涉及到 mousedown, mousemove, mouseup 三个事件,会有点复杂,可以先不考虑考虑交互,通过传入元素id和坐标的方式来移动元素。

关联文章

  1. 基于Web的svg编辑器(2)——层次结构设计(DOM结构)

参考文献

  • 三种undo/Redo的实现
  • 从Undo,Redo谈命令模式
  • 无操作次数限制的 Undo/Redo 实现方案
  • SVG 与 HTML5 的 canvas 各有什么优点,哪个更有前途?
  • Use execCommands to edit HTML content in your browser
  • 《JavaScript设计模式与开发实践》命令模式、组合模式
  • blog.csdn.net/lhrhi/artic…
  • www.haorooms.com/post/js_fwb…

基于Web的svg编辑器(1)——撤销重做功能相关推荐

  1. N个富文本编辑器/基于Web的HTML编辑器

    转自:http://www.cnblogs.com/lingyuan/archive/2010/11/15/1877447.html 基于WEB的HTML 编辑器,WYSIWYG所见即所得的编辑器,或 ...

  2. Tridiv:基于 Web 的 CSS 编辑器,创建炫丽 3D 图形

    Tridiv 是一个基于 Web 的编辑器,使用 CSS 创建 3D 形状.它提供了一个传统的四个面板的操作界面,给出了从每个平面的视图,以及一个预览窗格中示出的最终的效果.使用 Tridiv 可以创 ...

  3. 在线富文本编辑器-基于Web的HTML编辑器大全(一)

    基于WEB的HTML 编辑器,WYSIWYG所见即所得的编辑器,或是一个富文本的编辑器,是我们在开发WEB应用和内容管理系统时接收用户输入时必需要考虑的问题.下面是网上牛人收集的一些开源的WEB在线的 ...

  4. C# wpf 实现自定义撤销重做功能

    文章目录 前言 一.具体实现 1.完整代码 二.使用示例 1.拖动控件 (1)MainWindow.xaml (2)MainWindow.xaml.cs (3)效果预览 总结 前言 做wpf界面的时候 ...

  5. C++ 实现自定义撤销重做功能

    文章目录 前言 一.完整代码 二.使用示例 1.基本用法 2.gdi画线撤销 总结 前言 在使用c++做界面开发的时候,需要涉及到到撤销重做操作,尤其是实现白板功能时需要自己实现一套撤销重做功能,如果 ...

  6. 基于Web的Markdown编辑器HedgeDoc

    什么是 HedgeDoc ? HedgeDoc 是一个开源的.基于 web 的.自托管的.协作的markdown编辑器.您可以使用它轻松地在笔记.图形甚至演示文稿上进行实时协作.用户需要做的就是将你的 ...

  7. Mac OS开发—Xcode给Mac应用添加编辑快捷键(剪切 复制 粘贴 全选 删除 撤销 重做)功能

    前言 笔者做Windows开发的,无奈公司有Mac OS的项目需要维护,之前没有编辑菜单的功能,需要笔者加上这个功能,笔者只能硬着头皮上.还好会c++,Mac应用里面一般都会用object-c 也可以 ...

  8. 实现编辑功能有哪几个action_Web 应用的撤销重做实现

    背景 前不久,我参与开发了团队中的一个 web 应用,其中的一个页面操作如下图所示: GIF 这个制作间页面有着类似 PPT 的交互:从左侧的工具栏中选择元素放入中间的画布.在画布中可以删除.操作(拖 ...

  9. JSP+SQL基于WEB的开放性实验管理系统设计与实现(源代码+论文+开题报告+中英文献+答辩PPT)

    毕 业 设 计(论文)任 务 书 毕 业 设 计(论文)任 务 书 毕业设计(论文)题目 基于WEB的开放性实验管理系统 毕业设计(论文)时间 毕业设计(论文)进行地点 毕业设计(论文)内容及要求:高 ...

  10. 基于Web的在线考试系统

    基于Web的在线考试系统     二十一世纪的今天,随着社会的迅猛发展,科技也越来越发达,互联网已经遍布于家家户户,深入到人们的日常生活中.网络技术给我们当今的教育方式的带来了一次历史性的变革,在线考 ...

最新文章

  1. java rest httpclient_java http请求建议使用webClient,少用RestTemplate,不用HttpClient
  2. 文巾解题 7. 整数反转
  3. pd 生成mysql 脚本_PowerDesigner 如何生成数据库更新脚本
  4. java商城_商城系统常见开发语言及特点分享
  5. SQL Server 系统存储过程
  6. 传说中的window8 的种子
  7. Eclipse/Myeclipse生成serialVersionUID方法
  8. 工作130:computed
  9. php性能优化 --- laravel 性能优化
  10. ReactNative-地图导航-iOS
  11. Java开发笔记(六十九)泛型类的定义及其运用
  12. 安装Cocoapods, 更新gem出现的问题
  13. springboot源码解析autoconfigure之AopAutoConfiguration
  14. 高校科研管理系统源代码_教育领域各大高校如何建设智慧校园?你的学校够数字化吗?...
  15. 在ubuntu20.04下解决:E: 无法定位软件包 mingw32 的问题
  16. 自动化缺陷检测系统---项目总览
  17. 避免移动终端设备数据丢失的十个方法
  18. 提示此windows副本不是正版的win7系统7601解决方法
  19. monkey测试中防止关闭WIFI的方法
  20. 网站反爬指南:政府网站篇

热门文章

  1. 统计学基础知识之统计思维
  2. SQL Server 2012 下载和安装详细教程
  3. iOS蓝牙开发(三)实现外设功能
  4. edius隐藏快捷键_EDIUS素材隐藏快捷键是什么啊?
  5. vim命令失效了怎么办
  6. 国家漏洞库CNNVD:关于Dnsmasq多个缓冲区错误漏洞的通报
  7. (简单有效)vivo手机怎么不root激活Xposed框架
  8. Qt5.10编写FTP客户端
  9. 微信⼩程序——wxParse使⽤⽅法
  10. 160个crackme 008 Andrénalin.1