低代碼海報平臺的編輯器難點剖析
通過上一篇文章,我們對喬巴樂高海報平臺的整體架構有了初步的了解。今天我們深入到編輯器部分,對其中的難點和實現細節進行分析。
這是目前生產的編輯器頁面:
對應的原型圖:
不難看出和市面上大部分低代碼平臺一樣,由三部分組成:左側組件列表、中間畫布區域、右側屬性區域。
大致操作流程就是拖動左側的組件到中間的畫布,選中組件,右側屬性面板就會展示與該組件關聯的屬性。編輯右側屬性,畫布中對應的組件樣式就會同步更新。頁面拼接完成。
從中看出組件串聯其中,在前面一篇文章中,我們大致分析了整體頁面和組件的數據結構,但沒有細化。抽取一下文字、圖片、素材組件的通用特性:
尺寸屬性(Size)
- 寬度(width)
- 高度(height)
填充屬性(Padding)
- 上填充(padding-top)
- 右填充(padding-right)
- 下填充(padding-bottom)
- 左填充(padding-left)
視覺格式屬性
- 指定如何定位元素(position)
- 指定所定位元素的上邊緣的位置(top)
- 指定所定位元素的右邊緣的位置(right)
- 指定所定位元素底邊的位置(bottom)
- 指定定位元素左邊緣的位置(left)
- 將一個或多個陰影應用于元素的框(box-shadow)
顏色屬性(Color)
- 透明度(opacity)
邊框屬性(Border)
- 設置元素所有四個側面的邊框顏色(border-color)
- 設置元素所有四個側面的邊框寬度(border-width)
- 在元素的所有四個面上設置邊框的樣式(border-style)
- 定義元素邊界角的形狀(border-radius)
除此之外,文字組件還具有以下屬性:
字體屬性(Fonts)
- 定義元素的字體列表(font-family)
- 定義文本的字體大小(font-size)
- 定義文本的字體樣式(font-style)
- 指定文本的字體粗細(font-weight)
文字屬性(Text)
- 設置內聯內容的水平對齊方式(text-align)
- 指定添加到文本的裝飾(text-decoration)
- 設置文本行之間的高度(line-height)
圖片組件還具有:
圖片屬性(Image)
- 圖片鏈接(src)
素材組件還具有:
- 背景屬性(Background)
定義元素的背景色(background-color)
我們將上面的操作流程拆解為三步:
1?? 拖動左側的組件到中間的畫布
2?? 選中組件,右側屬性面板就會展示與該組件關聯的屬性
3?? 編輯右側屬性,畫布中對應的組件樣式就會同步更新
1.添加組件到畫布
通過上一篇文章,我們知道編輯器整體的數據結構是這么設計的:
那么從左側組件列表添加組件到畫布的操作其實就是向componentData中push一條組件數據。
這里主要是關注下組件列表要怎么設計:為了便于用戶快速創建活動,組件列表最好是預設一些模板,其實就是針對文字、圖片和素材分別提供一些已有的元素。這樣當對應組件點擊添加到畫布時,對應就會commit一個mutation來修改store中的componentData。
這里組件列表底層渲染也是用的組件庫,只是不同模板的props不同。
2.選中組件展示其關聯屬性
當在畫布中選中具體組件時,我們需要知道此刻是哪個組件被選中了,意味著需要一個變量來存儲當前高亮的組件。那么在store中添加setActive和getCurrentElement:
當在畫布中選中組件時,就會觸發setActive,更新currentElement。(通過getCurrentElement可以獲取到當前正在被操作的組件)。
這個時候,怎么在右側屬性區域動態展示不同組件的不同屬性呢?
對于單獨的組件來說,屬性面板應該是語義化的,無論是開發還是非開發同學,通過屬性面板的操作區,就可以直觀的知道一個組件的屬性是什么,應該如何使用和編輯。
那么屬性面板應該包含哪些內容呢?
1、label:屬性名稱。這個可以顯式的告訴具體的屬性的作用,比如元素的寬高、邊框、背景顏色等。
2、description:屬性的描述信息。對于一些特殊屬性,可能第一下通過label并不能直觀的識別屬性的含義,添加描述信息可以進行詳細的闡述。
3、content:屬性渲染器。用戶可以基于此實現對屬性的修改。最常見的有 textarea、input、select 等。
4、error:屬性校驗信息。當用戶輸入了不合法的或者類型不匹配時,可給予適當的錯誤提示信息。
通過以上描述,我們會發現,這其實就是我們常用的表單。
對應上面組件的props信息,我們可以對這些屬性做一些歸類,那歸類的標準又是什么呢?我認為應該把屬性與js中的數據類型做一下映射,然后在具體的分類下選用合適的渲染器。
我們知道在JavaScript中,一共有七種數據類型,字符串(String)、數字(Number)、布爾(Boolean)、空(Null)、未定義(Undefined)、Symbol和對象(Object)。其中對象類型包括:數組(Array)、函數(Function)、還有兩個特殊的對象:正則(RegExp)和日期(Date)。
這里面的空(Null)、未定義(Undefined)、Symbol和正則(RegExp)在渲染器中基本用不到。
我們先來看一下字符串(String)、數字(Number)、布爾(Boolean)和日期(Date)可能渲染的方式:
字符串(String)
渲染器類型 | 組件 |
input | |
textarea |
數字(Number)
渲染器類型 | 組件 |
input-number | |
slider |
布爾(Boolean)
渲染器類型 | 組件 |
switch |
日期(Date)
渲染器類型 | 組件 |
date |
除了這幾種,還有對象(Object)、數組(Array)、函數(Function)。
對象和數組屬于較復雜的類型,不過我們可以把它抽象為多層級(可以理解為嵌套)的基礎數據類型:
渲染器類型 | 組件 |
array |
像數組一般是用下拉框的形式來展現。
至于函數(Function),可以采用預定義的形式:
渲染器類型 | 組件 |
function |
到這里,不難想到,我們要維護一個屬性和表單組件的對應關系。屬性對應上面的key,像borderColor、text、width、fontFamily這些,那組件呢?組件其實就是對屬性的具體呈現,像width可以用數字輸入框、text可以用普通輸入框,但是對于一些比較復雜的特性,我們自己去實現這些組件,就顯得捉襟見肘了,這個時候我們就可以考慮和現有的組件庫做一下結合了(這里我采用的是Ant Design Vue)。
那么這樣,屬性prop和component基礎的對應關系就有了:
但這只是滿足了常規的基礎組件設計,像一些獨有的屬性或者基礎組件不能滿足的情況,我們需要對其做一定擴展:
渲染器類型 | 組件 |
upload | |
color-picker |
上面提到的上傳組件和顏色選擇組件是需要我們單獨去實現的。
3.編輯屬性,畫布同步更新
上面只是初步建立了屬性和組件的對應關系,組件初始值的展示、復雜組件的展示以及表單值更新后,畫布如何同步更新,這些問題我們還都沒有解決。
其實把問題簡化,這就是表單的回顯和更新問題。
以我以往的經驗來看:表單組件在設計時,有兩點是必須的:
- 表單初始值(默認value),供初始展示使用
- 表單屬性更改的事件(默認為change)
對于不同的表單,初始值和屬性更改后,參數的處理是不一樣的:
- 像高度、寬度這種數字類型的,傳入表單時應保證是number(24)類型,屬性更改后,事件參數應該是string(24px)類型的
- 字體加粗與否、傾斜與否、加下劃線與否,傳入表單時應保證是boolean(true/false)類型,屬性更改后,事件參數應該是string(bold/normal)類型的
所以給每一個屬性在傳入表單和事件更改后都要加一個額外的轉化函數去處理值:
- initialValueConvert
- eventChangeValueConvert
還有對屬性進行賦值時,不是所有的表單控件接收的都是value,像checkbox就是checked,這種單獨抽一個屬性valueProp去控制即可。
其次,像上面提到的復雜組件(我們這里是指父子層級)的渲染,除了component還要多加一個subComponent。
完善后,屬性prop和component的對應關系為:
我們的數據始終保持自上而下的順序,也就是說表單更新最終要反射回到總體的 store 當中去。這個時候我們在對應的組件當中發射出一個事件(change),當 change 發生的時候,我們能夠知道是哪個元素的哪個屬性,以及新的值是什么,我們就用這些信息更新這個值,這樣 store完成更新,元素的 props 發生更新,那么整個數據流動就完成了。
4.畫布區域交互設計實現
上面說了這么多,基本都是圍繞左側組件區域、中間畫布區域、右側屬性區域相互之間的數據流動來講的。最后來說一下畫布區域本身一些比較復雜的交互實現。
我大概整理了這幾種:
- 拖拽(組件在畫布中移動)
- 組件圖層
- 放大/縮小
- 撤銷/重做
拖拽(組件在畫布中移動)
這個相對比較簡單,就是mousedown、mousemove和mouseup事件的結合使用:在組件上按下鼠標后,記錄組件當前位置,也就是 x、y 坐標(對應的是 css 中的 left 和 top);每次鼠標移動時用當前最新的 xy 坐標減去最開始的 xy 坐標,計算出移動的距離,然后更新組件位置;鼠標抬起時結束移動。
組件圖層
圖層面板主要是控制組件的顯示/隱藏、不同組件的層級關系以及點擊選中。
這里主要說一下層級關系吧,正常情況下,我們會選擇使用z-index來控制層級。但是這里我沒有使用z-index,而是利用了層疊領域黃金準則的第二條。
層疊領域黃金準則:1、誰大誰上: 當具有明顯的層疊水平標示的時候,如識別的 z-indx 值,在同一個層疊上下文領域,層疊水平值大的那一個覆蓋小的那一個。2、后來居上: 當元素的層疊水平一致、層疊順序相同的時候,在 DOM 流中處于后面的元素會覆蓋前面的元素。
為什么選擇第二個而沒有選擇最常見的第一條呢?首先,我們需要一個圖層列表可以對每個組件對應的圖層進行排序,其實就是對store中的components進行排序,也就是數組排序了,那么在圖層列表中,如果你想增加某一圖層的層級,把它放置到后面就可以了(這樣渲染時,數組后面的元素就會處在 DOM 流的后面了。對應的層疊順序也就居上了),這樣不僅操作方便,也不用增加額外冗余代碼,可謂一舉兩得。
放大/縮小
核心實現:在畫布組件的四個角(↖?、↗?、↙?、↘?)分別加一個小圓點:
左上:組件 left、top 均減小;width、height 均變大
右上:組件 left 不變、top 減小;width、height 均變大
左下:組件 left 減小、top 不變;width、height 均變大
右下:組件 left、top 均不變;width、height 均變大
撤銷/重做
撤銷、重做其實是我們平時一直在用的操作。對應快捷鍵一般就是? Z / Ctrl+Z、?? Z / Ctrl+Shift+Z。這個功能是很常見的,他可以極大的提升用戶體驗,提高編輯效率,但是用代碼應該如何實現呢?