摘要:撤銷重做是一款編輯器的基礎(chǔ)功能,它讓用戶在進(jìn)行錯(cuò)誤操作后,可以讓編輯器回滾到錯(cuò)誤操作前的狀態(tài)。選擇實(shí)現(xiàn)方案基于對(duì)象序列化的實(shí)現(xiàn)功能,其中一個(gè)方法是基于對(duì)象序列化的。示例編輯器的撤銷重做功能使用了這種模式。
最近在做一個(gè)網(wǎng)頁版的 svg 編輯器,為此學(xué)習(xí)了編輯器相關(guān)方面的知識(shí)。本文是我的一些粗淺學(xué)習(xí)總結(jié),希望可以給初學(xué)者一些思路。前面的話
隨著近幾年前端技術(shù)的快速發(fā)展,人們更傾向于將應(yīng)用開發(fā)放到網(wǎng)頁瀏覽器上,即 B/S 架構(gòu) 。相比與傳統(tǒng)的 C/S 模式,它的兼容性更好,開發(fā)成本更低,且不需要安裝,只要打開瀏覽器的一個(gè)頁面即可。
Web 的圖形編輯器主要使用到了 HTML5 的 Canvas 技術(shù)和 SVG 技術(shù)。Canvas 是使用 JavaScript 程序繪圖,SVG是使用XML文檔描述來繪圖。SVG 是基于矢量的,放大縮小不失真。而 Canvas 是基于位圖的,適合做像素處理,也很適合做 HTML5 小游戲。它們各有優(yōu)劣,開發(fā)時(shí)具體使用哪種方案,需要根據(jù)自己的需求進(jìn)行選擇。
而我要做的是一個(gè) SVG 編輯器,所以毫無疑問選擇了 SVG 技術(shù)方案。此外,為更方便的操作 SVG,且使代碼有更好的的可讀性,而使用了 svg.js 庫。svg.js 提供了可讀性很好的鏈?zhǔn)綄懛?,另外這個(gè)對(duì)學(xué)習(xí) svg 也有很大幫助(通過簡(jiǎn)單的代碼就可以生成一個(gè)svg )。我會(huì)在代碼中和 svg.js 相關(guān)的代碼旁邊寫上注釋,所以你不會(huì) svg.js 也能看懂我的代碼。
功能描述撤銷(undo):返回到最后一個(gè)操作前的狀態(tài)。
重做(redo):如果撤銷過程中,發(fā)現(xiàn)過度撤銷,可以通過 “重做”,進(jìn)入某一個(gè)操作后的狀態(tài)。
一般來說,稍微復(fù)雜點(diǎn)的編輯器都是有 撤銷/重做 功能的。撤銷重做 是一款編輯器的基礎(chǔ)功能,它讓用戶在進(jìn)行錯(cuò)誤操作后,可以讓編輯器回滾到錯(cuò)誤操作前的狀態(tài)。
選擇實(shí)現(xiàn)方案 基于對(duì)象序列化的Undo/Redo實(shí)現(xiàn)undo/redo 功能,其中一個(gè)方法是 基于 對(duì)象序列化 的Undo/Redo 。
每進(jìn)行一個(gè)操作,就 將之前的所有對(duì)象序列化(即存儲(chǔ)當(dāng)前視圖狀態(tài)到一個(gè)變量中) ,將其推入到名為 undoStack 的棧中。當(dāng)需要撤銷時(shí),undoStack 出棧,將出棧的數(shù)據(jù)進(jìn)行解析,還原到 UI 層,此時(shí)還要將出棧的序列化數(shù)據(jù)推入到 redoStack 棧內(nèi)。
這種模式,優(yōu)點(diǎn)是代碼容易實(shí)現(xiàn),復(fù)雜度較低,缺點(diǎn)是當(dāng)對(duì)象數(shù)量越多,每次保存狀態(tài)都要使用的內(nèi)存也就越大,所以并不是編輯器的首選解決方案。
基于命令模式的 Undo/Redo命令模式則是 給每一個(gè)操作創(chuàng)建一個(gè) command 對(duì)象,該對(duì)象記錄了具體的執(zhí)行方法(execute)和一個(gè)逆執(zhí)行方法(undo) 。編輯器每進(jìn)行一次操作,對(duì)應(yīng)的 command 對(duì)象會(huì)被創(chuàng)建,并執(zhí)行該命令對(duì)象的 execute 方法,然后將這個(gè)對(duì)象 推入到 undo 棧中。
當(dāng)用戶撤銷(undo)時(shí),如果 undo 棧中不為空,彈出 undo 棧頂?shù)?command 對(duì)象,執(zhí)行它的 execute 方法,然后將這個(gè)對(duì)象推入到 redo 棧中。
重做(redo)的操作和上面類似。如果 redo 棧不為空,彈出棧頂對(duì)象,執(zhí)行 execute 方法,并把這個(gè)對(duì)象推入到 undo 棧中。
每次進(jìn)行一個(gè)操作時(shí),而創(chuàng)建一個(gè)新的 command 時(shí),如果 redo 棧 不為空,將其清空。
有些操作可能是多個(gè)操作的組合,這時(shí)候需要用到設(shè)計(jì)模式中的 “組合模式”,將多個(gè)操作包裝成一個(gè)組合操作。每次 execute 和 redo 都遍歷組合操作下的子操作。
這種模式因?yàn)橛涗浀闹皇?正向操作 和 逆向操作,自然占用的內(nèi)存和對(duì)象的多少無關(guān)。但因?yàn)樾枰茖?dǎo)出每個(gè)操作的逆向操作,代碼實(shí)現(xiàn)比前一種模式復(fù)雜,且不能復(fù)用。
示例編輯器的撤銷重做功能使用了這種模式。
實(shí)現(xiàn)教程示例源代碼地址:https://github.com/F-star/web...
演示地址:https://f-star.github.io/web-...
代碼部分參考了 svg-edit (一款開源基于web的,Javascript驅(qū)動(dòng)的 svg 繪制編輯器) 的實(shí)現(xiàn)。
準(zhǔn)備工作首先我們創(chuàng)建一個(gè) index.html 文件,里面用一個(gè) div#drawing 元素來放 我們的 svg 元素。
為了讓代碼可讀性更好,我使用了 ES6 的模塊化,寫好后用 babel 編譯下就好。
如果要開發(fā)比較復(fù)雜的編輯器,模塊化還是必要的,模塊化可以降低代碼的耦合度,也更方便進(jìn)行單元測(cè)試。此外還可以考慮引入 typescript 來提供靜態(tài)類型化,因?yàn)殚_發(fā)一個(gè)編輯器,無疑要使用到非常多的方法,傳入的參數(shù)如果不能保證類型的正確,可能會(huì)導(dǎo)致意想不到的錯(cuò)誤。
下面正式開始編寫代碼。
首先我們引入 svg.js 庫,接著引入我們的入口文件 index.js,并給這個(gè) script 的 type 設(shè)置為 module,以獲得原生的 ES6 模塊化支持。所以你要保證運(yùn)行下面 html 的瀏覽器可以支持 ES6 模塊化。
然后我們開始編寫 history.js 文件的相關(guān)代碼。這里我使用了 ES6 的 class 語法,因?yàn)檫@種寫法相比 “原型繼承” 的寫法,明顯可讀性更好。當(dāng)然你也可以用 “原型繼承” 的寫法,class 只是它的語法糖。
命令類首先我們創(chuàng)建一個(gè)命令基類。
// history.js // 命令基類 class Command { constructor() {} execute() { throw new Error("未重寫execute方法!"); // 繼承時(shí)如果沒有覆蓋此方法,會(huì)報(bào)錯(cuò)。通過這種方式,保證繼承的子命令類重寫此方法。 } undo() { console.error("未重寫undo方法!"); // 同上 } }
然后我們就可以根據(jù)業(yè)務(wù)邏輯,包裝成一個(gè)個(gè)子命令類,在需要的時(shí)候?qū)嵗?。下面?InsertElementCommand 類的作用是創(chuàng)建新元素。
// history.js // 創(chuàng)建不同元素的方法集合 const InsertElement = { // 在 svg 元素下,創(chuàng)建了一個(gè)寬高為 size,位于 [x, y],內(nèi)容為 content 的 text 元素, // 并返回了這個(gè)節(jié)點(diǎn)對(duì)象的引用(svgjs包裝后的對(duì)象)。 text(x, y, size, content="") { return draw.text(content).move(x, y).size(size); } // 這里還可以寫 rect, circle 等方法。 } // 插入元素命令類 export class InsertElementCommand extends Command { // 指定 元素類型 和 需要保存的狀態(tài)。 constructor(type, ...args) { super(); this.el = null; this.type = type; this.args = args; } execute() { // 這里寫創(chuàng)建的方法 console.log("exec") this.el = InsertElement[this.type](...this.args); } undo() { console.log("undo") // 移除元素 this.el.remove(); } }
這里為了更好的通用性,我們創(chuàng)建了一個(gè) InsertElement 對(duì)象,里面保存了創(chuàng)建不同類型的各種方法。這個(gè)對(duì)象其實(shí)就是設(shè)計(jì)模式中 “策略模式” 中 的策略對(duì)象。這里,我們對(duì) text 類型的創(chuàng)建代碼寫在了 InsertElement 對(duì)象的 text 方法中了。
CommandManager 對(duì)象這樣,我們就寫好一個(gè)具體的命令類了。接下來,我們需要寫一個(gè)命令管理對(duì)象(CommandManager)來管理我們的創(chuàng)建的所有命令。
// history.js // 命令管理對(duì)象 export const cmdManager = (() => { let redoStack = []; // 重做棧 let undoStack = []; // 撤銷棧 return { execute(cmd) { cmd.execute(); // 執(zhí)行execute undoStack.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); }, } })();
每當(dāng)我們創(chuàng)建一個(gè) Command 對(duì)象后,就要調(diào)用 cmdManager.execute(cmd) 方法后,它會(huì)執(zhí)行 Command 對(duì)象的 execute 方法,并將這個(gè) Command 對(duì)象推入 undoStack 中。
redo/undo 棧的實(shí)現(xiàn)方式有很多種,這里為了讓代碼更直觀簡(jiǎn)單,直接用兩個(gè)數(shù)組來保存兩個(gè)棧。
而在 svg-edit 中,則使用了雙向鏈表的方式:使用了一個(gè)數(shù)組,并給了一個(gè)指針,指向一個(gè) Command 對(duì)象。指針左邊是 undoStack,右邊為 redoStack。這樣每次撤銷重做時(shí),只要修改指針位置,而不需要修改對(duì)數(shù)組進(jìn)行操作,時(shí)間復(fù)雜度更低。
進(jìn)一步包裝通過下面這樣的代碼,我們就可以執(zhí)行并保存每一步操作了。
let cmd = new InsertElementCommand("text", x, y, 20, "好"); cmdManager.execute(cmd);
但如果每個(gè)操作都要寫下面這樣的代碼,無疑有些累贅。于是我從 js 原生的方法 [document.execCommand
](https://developer.mozilla.org... 獲得了靈感,在全局添加了一個(gè) executeCommand 方法。
// commondAction.js import { 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 設(shè)置為全局方法 window.executeCommond = (cmdName, ...args) => { commondAction[cmdName](...args); }
然后我們通過下面這種方式,就能在任何位置創(chuàng)建 command 對(duì)象,并執(zhí)行它的 execute 命令。
executeCommond("drawText", x, y, 20, "好"); executeCommond("undo"); executeCommond("redo");
隨著命令的擴(kuò)展,我們可以在對(duì)第一參數(shù) cmdName 進(jìn)行解析,判斷是創(chuàng)建一個(gè)元素,還是修改一個(gè)元素的一些參數(shù)等(如"create rect", "update text"),然后調(diào)用對(duì)應(yīng)的各種方法。
最后我們?cè)谌肟?index.js 文件內(nèi),將這些命令綁定到事件響應(yīng)事件上就完事了。
課后練習(xí)你可以下載我在 github 上提供的源碼,試著添加 “創(chuàng)建 rect 的功能。
如果你想挑戰(zhàn)一下的話,還可以寫一個(gè)移動(dòng)元素的功能。如果還要考慮交互的話,會(huì)涉及到 mousedown, mousemove, mouseup 三個(gè)事件,會(huì)有點(diǎn)復(fù)雜,可以先不考慮考慮交互,通過傳入元素id和坐標(biāo)的方式來移動(dòng)元素。
參考文獻(xiàn)三種undo/Redo的實(shí)現(xiàn)
從Undo,Redo談命令模式
無操作次數(shù)限制的 Undo/Redo 實(shí)現(xiàn)方案
SVG 與 HTML5 的 canvas 各有什么優(yōu)點(diǎn),哪個(gè)更有前途?
Use execCommands to edit HTML content in your browser
《JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐》命令模式、組合模式
https://blog.csdn.net/lhrhi/a...
https://www.haorooms.com/post...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/103740.html
摘要:當(dāng)然,這只是結(jié)合自己項(xiàng)目的工程結(jié)構(gòu)和特點(diǎn)設(shè)置的一套使用方式,僅供參考開發(fā)富文本編輯器的教訓(xùn)由于項(xiàng)目的時(shí)間較緊張,我在頁面上應(yīng)用了框架的背景下,想當(dāng)然的想要把也應(yīng)用于富文本編輯器的開發(fā),事實(shí)證明這是不太可行的。 此文已由作者劉詩川授權(quán)網(wǎng)易云社區(qū)發(fā)布。 歡迎訪問網(wǎng)易云社區(qū),了解更多網(wǎng)易技術(shù)產(chǎn)品運(yùn)營(yíng)經(jīng)驗(yàn)。 最近我們的產(chǎn)品有一個(gè)需求是要在PC端做一個(gè)面向用戶的書評(píng)編輯器,讓用戶和編輯在蝸牛讀書...
摘要:如下是具體代碼示例新增單元格范圍對(duì)角線,使您的表格數(shù)據(jù)更加醒目新增對(duì)單元格或范圍設(shè)置對(duì)角線邊框樣式的功能,并支持保存到文件或打印輸出。 純前端表格控件SpreadJS 正式發(fā)布2018 V11.1 版本,新版本提供撤銷/重做功能,并增強(qiáng)了UI和數(shù)據(jù)篩選,極大的擴(kuò)展了產(chǎn)品的實(shí)用功能,可更加方便優(yōu)雅的嵌入您的應(yīng)用系統(tǒng)。 Spread 是一系列功能和Excel類似的表格工具,支持桌面、Web...
摘要:如下是具體代碼示例新增單元格范圍對(duì)角線,使您的表格數(shù)據(jù)更加醒目新增對(duì)單元格或范圍設(shè)置對(duì)角線邊框樣式的功能,并支持保存到文件或打印輸出。 純前端表格控件SpreadJS 正式發(fā)布2018 V11.1 版本,新版本提供撤銷/重做功能,并增強(qiáng)了UI和數(shù)據(jù)篩選,極大的擴(kuò)展了產(chǎn)品的實(shí)用功能,可更加方便優(yōu)雅的嵌入您的應(yīng)用系統(tǒng)。 Spread 是一系列功能和Excel類似的表格工具,支持桌面、Web...
摘要:萬能后臺(tái)自定義擴(kuò)展功能基于在此感謝要封裝常用功能源代碼如初始化空對(duì)象判斷重定向全局的請(qǐng)求中加載文件數(shù)據(jù)驗(yàn)證重寫了彈出層增加彈出確認(rèn)提示框。源代碼另一個(gè)獨(dú)特地方再次簡(jiǎn)化了讓表格顯示數(shù)據(jù)提交變得更加簡(jiǎn)潔組件化去功能開發(fā)。 喜歡就Star,不要Fork; 想要分享的動(dòng)機(jī)才是驅(qū)動(dòng)力,而技術(shù)僅僅是一種方法。 ====================== 萬能后臺(tái)——自定義擴(kuò)展功能 基于TP5...
閱讀 1204·2023-04-26 00:12
閱讀 3428·2021-11-17 09:33
閱讀 1126·2021-09-04 16:45
閱讀 1268·2021-09-02 15:40
閱讀 2355·2019-08-30 15:56
閱讀 3076·2019-08-30 15:53
閱讀 3613·2019-08-30 11:23
閱讀 2006·2019-08-29 13:54