SVGEdit:老牌開源 SVG 編輯器是如何架構(gòu)的?
大家好,我是前端西瓜哥。這次簡單看看 SVGEdit 的架構(gòu)。
SVGEdit 的版本為 7.2.0。
SVGEdit 一款非常老牌的 SVG 圖形編輯器,用于編輯處理 SVG,star 數(shù)目前是 5.8k。
它的優(yōu)點(diǎn)在于經(jīng)過多年的開發(fā),完成度高,較為成熟,功能相當(dāng)豐富。
- 有豐富的工具:選擇工具、鉛筆工具、鋼筆工具(三階貝塞爾)、直線、各種圖形、圖片、文字等;
- 畫布縮放、圖形縮放旋轉(zhuǎn)、編組、復(fù)制粘貼、層級(jí)排布修改、對(duì)齊;
- 網(wǎng)格線、標(biāo)尺、圖層管理、導(dǎo)入導(dǎo)出 SVG;
- 歷史記錄,支持撤銷重做(編輯器的基本功能)等等。
缺點(diǎn)是幾乎不維護(hù)了,提交很少,大概一個(gè)月一提交,最新版 7.2.0 也是 22 年 8 月的時(shí)候了。
UI 比較簡陋,很簡單就能看到一些 UI bug。
如果要找一個(gè) UI 好看的,可以看看開源項(xiàng)目 :Method-Draw,這個(gè) UI 好看很多。它 fork 了 SVG-Edit 并做了一些改造。
Method-Draw 標(biāo)榜簡單易用,所以有意去掉了 SVGEdit 的一些高級(jí)功能,比如圖層。
技術(shù)棧
Web Component + SVG + Rollup + i18next
UI 使用了 Web Component,瀏覽器原生支持的組件方案。
作為一款 SVG 編輯器,選擇 SVG 沒有毛病,這樣渲染效果就完全交給瀏覽器,不需要根據(jù)標(biāo)準(zhǔn)去實(shí)現(xiàn)渲染效果,自己專心寫編輯器的業(yè)務(wù)邏輯即可。
沒有用 TypeScript,因?yàn)槭呛芾系捻?xiàng)目,當(dāng)時(shí) TypeScript 尚未大行其道。如果要做新項(xiàng)目,建議還是上 TypeScript,大型復(fù)雜軟件還是很需要類型系統(tǒng)的。
打包用了 Rollup。國際化用了 i18next。
模塊設(shè)計(jì)
UI 層對(duì)應(yīng) Editor 類,該類繼承了 EditorStartup 類,后者做一些初始化操作。
編輯器內(nèi)核對(duì)應(yīng) SvgCanvas 類。
SvgCanvas 感覺太多讀寫屬性的方法(getXx 和 setXx)了,學(xué) Java 學(xué)的??梢猿閹讉€(gè)小類把耦合性強(qiáng)的方法封裝起來。
有插件機(jī)制,插件對(duì)象通過 addExtension 方法注入到 SvgCanvas.extensions 對(duì)象。
可以看到注冊(cè)了 grid(網(wǎng)格)、markers(標(biāo)尺)之類的插件。
關(guān)于 UI 層和內(nèi)核層的通信,UI 層改數(shù)據(jù),會(huì)直接改內(nèi)核層,然后再改 UI 層。
這里的 zoom 有兩個(gè)數(shù)據(jù)源,可能會(huì)出現(xiàn)改了一個(gè)忘記改另一個(gè)的情況。建議只使用一個(gè)內(nèi)核層數(shù)據(jù)源,改這個(gè)數(shù)據(jù)源后通過事件通知 UI 層或其他層做數(shù)據(jù)同步。多數(shù)據(jù)源是壞文明。
渲染方案
渲染方案是 SVG。
SVG 編輯器用 SVG,相當(dāng)合理。
對(duì)于圖形樹的實(shí)現(xiàn)、圖形拾?。c(diǎn)選)、圖形渲染,SVGEdit 都交給瀏覽器都去實(shí)現(xiàn)。
SVG 對(duì)一般前端開發(fā)是非常好上手的,不需要太多圖形知識(shí),本質(zhì)就是一個(gè)有層級(jí)的 DOM 元素樹,前端同學(xué)再熟悉不過了,只是元素專門用來描述圖形。
但因?yàn)檫h(yuǎn)離底層,不方便做一些渲染優(yōu)化和緩存,圖形多的時(shí)候很卡,不適合做高性能圖形編輯器。
靈活性也較差,比如一個(gè) 0.5 線寬的直線還把畫布縮小了,根本就點(diǎn)不中好不好,希望點(diǎn)擊區(qū)域可以外擴(kuò)一些,或想點(diǎn)中一個(gè)設(shè)置為不可見的圖形點(diǎn)擊區(qū)域。這個(gè)做不了。
當(dāng)然一個(gè)可以考慮的方案是 SVG 只是單純做渲染,圖形拾取自己實(shí)現(xiàn)。
但 SVG 有一個(gè)強(qiáng)大的優(yōu)點(diǎn):方便做功能擴(kuò)展,進(jìn)行二次開發(fā)。
比如你要在圖形編輯器里加一個(gè)新的模塊,比如倒計(jì)時(shí)、一個(gè)表單組件,網(wǎng)上找到輪子集成進(jìn)去會(huì)很方便。因?yàn)?SVG 里面可以嵌入 DOM 元素,DOM 元素里也可以嵌入 SVG。
UI 層
UI 層原本是基于 jQuery UI 的,但后面丟棄 jQuery 改用 Web Component 進(jìn)行了重構(gòu)。
順帶一提,有個(gè)叫做 jPicker 的基于 jQuery 的拾色器插件,也做了魔改,去掉對(duì) jQuery 的依賴。
它沒有用 React、Vue 這些 UI 框架,而是選擇 Web Component,我認(rèn)為這是一個(gè)糟糕的決策。
Web Component 雖然被 瀏覽器原生支持,但并不是主流,生態(tài)一般,輪子不如 React 和 Vue 豐富。
我們繼續(xù)看代碼。
以左側(cè)欄 LeftPanel 為例,HTML 為:
這里的 se-button 就是一個(gè) Web Component 組件,里面有局部樣式和交互邏輯,類似 React 和 Vue。
全局樣式文件是 svgedit.css。
LeftPanel 類初始化后會(huì)調(diào)用 init 方法。
該方法會(huì):
- 讀取前面的 HTML 創(chuàng)建一個(gè) template 元素,然后添加 DOM 樹中。
- 給一些 DOM 元素綁定了事件響應(yīng)函數(shù)。
$id 這些是工具類方法。
下面代碼的作用是,給選擇工具按鈕綁定方法,該方法更改編輯器的模式為選擇模式。
LeftPanel 的 init 方法是在 EditorStartUp 類(這個(gè)是 Editor 的父類)的 init 方法中被調(diào)用的。
圖形拾取
點(diǎn)選 圖形的圖形拾取是交給瀏覽器,監(jiān)聽鼠標(biāo)按下事件的方式,讀取 mouseEvent.target。
因?yàn)榭赡苡薪M的存在,所以會(huì)不斷地讀 parentNode 找最頂層的 group 元素,直到當(dāng)前 group 結(jié)束。
框選 會(huì)在點(diǎn)中空白區(qū)域出現(xiàn),并將當(dāng)前模式(currentMode)臨時(shí)切換為 multiselect。
期間產(chǎn)生的選區(qū)矩形元素保存在 svgCanvas.rubberBox 屬性中。
拖拽修改選區(qū)矩形寬高時(shí),會(huì)遞歸 SVG 樹,計(jì)算它們的 bbox,判斷是否和選區(qū)矩形相交。將相交的圖形放到 selectedElements 屬性中。
工具管理
切換工具使用 SvgCanvas.setMode('line') 的方式。
不同工具都有各自實(shí)現(xiàn)的事件響應(yīng)函數(shù),當(dāng)用戶進(jìn)行鼠標(biāo)操作時(shí),會(huì)執(zhí)行 mouseDownEvent、mouseMoveEvent、mouseUpEvent,會(huì)根據(jù) mode 執(zhí)行不同的工具的方法。
鋼筆工具對(duì)應(yīng) svgCanvas.pathActions 屬性,對(duì)應(yīng) pathActionsMethod 方法,沒有用類,而是用了閉包的方式進(jìn)行邏輯封裝。
圖形工具的邏輯倒沒有抽一個(gè)對(duì)象出來,直接寫在 ouseDownEvent、mouseMoveEvent、mouseUpEvent 里。
操作的歷史記錄
兩個(gè)棧等價(jià)于一個(gè)數(shù)組或雙向鏈表中,加上一個(gè)指針,該指針指向多個(gè)命令中的當(dāng)前命令。
撤銷就是把指向往左移動(dòng),重做往右移,新操作則把指針后面的命令丟掉,然后把這個(gè)新的操作加到數(shù)組中,并將指針后移。
SVGEdit 的歷史命令都保存在 UndoManager 類的 undoStack 數(shù)組中,并用 undoStackPointer 指針指向最新命令的位置。
SVGEdit 使用了 patch(打補(bǔ)?。┑姆绞接涗洑v史操作,沒有使用圖形樹快照的方式。
下面是移動(dòng)一個(gè)矩形產(chǎn)生的操作命令,它記錄了修改圖形屬性的命令,該命令保存了一個(gè)元素修改前后的屬性。
這里有個(gè)特殊的 BatchCommand 批量命令對(duì)象,它的 stack 數(shù)組記錄了一次操作要執(zhí)行的多個(gè)子命令。
其實(shí)就是 宏命令。宏命令的作用是將多個(gè)命令組合在一起批量執(zhí)行。
各種命令類保存在 svgCanvas.history 中。
簡評(píng)
UI 框架應(yīng)該選擇 React 或 Vue。
這樣項(xiàng)目才會(huì)有更多人使用,作為一款比較古早的編輯器才可能煥發(fā)第二春。
最好是 Vue3,國內(nèi)很多中小廠在用,那里的程序員不喜歡造輪子,這樣他們就會(huì)用你這個(gè)編輯器,然后社區(qū)繁榮,贏。當(dāng)然最好 React 和 Vue 都做。
SVGEdit 丟掉 jQuery 用 Web Component,我不是很理解,外國比較流行這個(gè)?這樣就不好集成進(jìn)公司的項(xiàng)目中,不利于項(xiàng)目的持續(xù)發(fā)展。
不要使用單例。
我看不少地方用了單例,單例模式有個(gè)問題,如果頁面需要同時(shí)有多個(gè)編輯器實(shí)例,比如做兩張圖紙對(duì)比功能。
那它們就會(huì)因?yàn)閱卫膶?duì)象共享導(dǎo)致沖突,比如改了編輯器 A 的設(shè)置屬性會(huì)同時(shí)改了編輯器 B 的,這不是我們想要的。
類的面向?qū)ο箫L(fēng)格是比較好的解法,每個(gè)對(duì)象都要?jiǎng)?chuàng)建一個(gè)新的實(shí)例,就不會(huì)沖突了。
較多的 UI bug。
選中一個(gè)元素就能復(fù)現(xiàn),此外 UI 也不好看。
監(jiān)聽鼠標(biāo)事件應(yīng)該放到 document 下。
放到 SVG 的容器或 SVG 上其實(shí)并不是很好的做法,當(dāng)光標(biāo)移到這些元素外時(shí),監(jiān)聽就消失了,綁定到 doucment 下即使光標(biāo)移動(dòng)到瀏覽器外都能監(jiān)聽。
結(jié)尾
SVGEdit 支持的功能很多,但 UI 比較復(fù)古,小 bug 有點(diǎn)多的樣子。
但如果你要做 SVG 編輯器,與其從零開始,不如基于 SVGEdit 做去二次開發(fā)。