国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

基于Web的svg編輯器(1)——撤銷重做功能

Jaden / 2227人閱讀

摘要:撤銷重做是一款編輯器的基礎功能,它讓用戶在進行錯誤操作后,可以讓編輯器回滾到錯誤操作前的狀態。選擇實現方案基于對象序列化的實現功能,其中一個方法是基于對象序列化的。示例編輯器的撤銷重做功能使用了這種模式。

最近在做一個網頁版的 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 都遍歷組合操作下的子操作。

這種模式因為記錄的只是 正向操作 和 逆向操作,自然占用的內存和對象的多少無關。但因為需要推導出每個操作的逆向操作,代碼實現比前一種模式復雜,且不能復用。

示例編輯器的撤銷重做功能使用了這種模式。

實現

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

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

代碼部分參考了 svg-edit (一款開源基于web的,Javascript驅動的 svg 繪制編輯器) 的實現。

準備工作

首先我們創建一個 index.html 文件,里面用一個 div#drawing 元素來放 我們的 svg 元素。

為了讓代碼可讀性更好,我使用了 ES6 的模塊化,寫好后用 babel 編譯下就好。

如果要開發比較復雜的編輯器,模塊化還是必要的,模塊化可以降低代碼的耦合度,也更方便進行單元測試。此外還可以考慮引入 typescript 來提供靜態類型化,因為開發一個編輯器,無疑要使用到非常多的方法,傳入的參數如果不能保證類型的正確,可能會導致意想不到的錯誤。

下面正式開始編寫代碼。

首先我們引入 svg.js 庫,接著引入我們的入口文件 index.js,并給這個 script 的 type 設置為 module,以獲得原生的 ES6 模塊化支持。所以你要保證運行下面 html 的瀏覽器可以支持 ES6 模塊化。


    

然后我們開始編寫 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();                  // 執行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);
        },
    }
})();

每當我們創建一個 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
](https://developer.mozilla.org... 獲得了靈感,在全局添加了一個 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 設置為全局方法
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和坐標的方式來移動元素。

參考文獻

三種undo/Redo的實現

從Undo,Redo談命令模式

無操作次數限制的 Undo/Redo 實現方案

SVG 與 HTML5 的 canvas 各有什么優點,哪個更有前途?

Use execCommands to edit HTML content in your browser

《JavaScript設計模式與開發實踐》命令模式、組合模式

https://blog.csdn.net/lhrhi/a...

https://www.haorooms.com/post...

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103740.html

相關文章

  • 開發富文本輯器一些經驗教訓

    摘要:當然,這只是結合自己項目的工程結構和特點設置的一套使用方式,僅供參考開發富文本編輯器的教訓由于項目的時間較緊張,我在頁面上應用了框架的背景下,想當然的想要把也應用于富文本編輯器的開發,事實證明這是不太可行的。 此文已由作者劉詩川授權網易云社區發布。 歡迎訪問網易云社區,了解更多網易技術產品運營經驗。 最近我們的產品有一個需求是要在PC端做一個面向用戶的書評編輯器,讓用戶和編輯在蝸牛讀書...

    mtunique 評論0 收藏0
  • SpreadJS 純前端表格控件V11.1震撼來襲

    摘要:如下是具體代碼示例新增單元格范圍對角線,使您的表格數據更加醒目新增對單元格或范圍設置對角線邊框樣式的功能,并支持保存到文件或打印輸出。 純前端表格控件SpreadJS 正式發布2018 V11.1 版本,新版本提供撤銷/重做功能,并增強了UI和數據篩選,極大的擴展了產品的實用功能,可更加方便優雅的嵌入您的應用系統。 Spread 是一系列功能和Excel類似的表格工具,支持桌面、Web...

    Hydrogen 評論0 收藏0
  • SpreadJS 純前端表格控件V11.1震撼來襲

    摘要:如下是具體代碼示例新增單元格范圍對角線,使您的表格數據更加醒目新增對單元格或范圍設置對角線邊框樣式的功能,并支持保存到文件或打印輸出。 純前端表格控件SpreadJS 正式發布2018 V11.1 版本,新版本提供撤銷/重做功能,并增強了UI和數據篩選,極大的擴展了產品的實用功能,可更加方便優雅的嵌入您的應用系統。 Spread 是一系列功能和Excel類似的表格工具,支持桌面、Web...

    developerworks 評論0 收藏0
  • PHP后臺核心框架、自定義擴展功 直接上手做功開發即可

    摘要:萬能后臺自定義擴展功能基于在此感謝要封裝常用功能源代碼如初始化空對象判斷重定向全局的請求中加載文件數據驗證重寫了彈出層增加彈出確認提示框。源代碼另一個獨特地方再次簡化了讓表格顯示數據提交變得更加簡潔組件化去功能開發。 喜歡就Star,不要Fork; 想要分享的動機才是驅動力,而技術僅僅是一種方法。 ====================== 萬能后臺——自定義擴展功能 基于TP5...

    yexiaobai 評論0 收藏0

發表評論

0條評論

Jaden

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<