上一篇說了如何實(shí)現(xiàn)靈活的拖拽勇哗,那么加上編輯功能昼扛,拖拽編輯器的兩大核心功能就集齊了,剩下就是組件樹欲诺、版本管理、模板渺鹦、預(yù)覽扰法、快捷鍵、事件毅厚、動畫塞颁、在線編輯代碼以及部署方式這些邊角功能,當(dāng)然這些邊角功能都不影響大局吸耿,這次我們來談?wù)勅绾卧O(shè)計編輯區(qū)祠锣,類似下圖的結(jié)構(gòu):
- 從圖中可以看出,編輯區(qū)涉及很多數(shù)據(jù)同步操作咽安,我們使用了
mobx
很好的解決了這個問題伴网,本篇文章因?yàn)橹攸c(diǎn)描述編輯器設(shè)計,因此數(shù)據(jù)設(shè)計部分不會過多涉及妆棒。 - 除了基本屬性設(shè)置澡腾,還應(yīng)該有腳本設(shè)置沸伏、事件設(shè)置、動畫設(shè)置动分,這些后續(xù)文章再討論毅糟。
通用屬性編輯
我們發(fā)現(xiàn),樣式才是最通用的屬性澜公,無論何種組件都逃離不了樣式的設(shè)置姆另,除此以外的屬性都是自定義的,我們無法抽象出共性加以定制坟乾,但是樣式是固定的蜕青,所以編輯區(qū)先要支持通用樣式的編輯。
通用樣式:背景
邊框
字體
邊距
布局
溢出處理
寬高
透明度
我們提供了對應(yīng)的 13 余中定制編輯類型糊渊,比如像上圖的邊距調(diào)節(jié)器右核,專門針對邊距進(jìn)行修改,只要將編輯類型設(shè)置為 marginPadding
渺绒,編輯框中就會出現(xiàn)非常方便的邊距調(diào)節(jié)器贺喝。
還有一種通用屬性處理,比如有一個圖標(biāo)組件宗兼,實(shí)現(xiàn)以下效果:
如果單獨(dú)為圖標(biāo)類型設(shè)置一種編輯狀態(tài)很不劃算躏鱼,這種分類可以劃為 實(shí)例類型
,每一個圖標(biāo)其實(shí)是這個組件接收了某種參數(shù)后的狀態(tài)殷绍,我們預(yù)先提供這些狀態(tài)染苛,編輯器將這些狀態(tài)的組件分別實(shí)例化顯示出來,每當(dāng)鼠標(biāo)點(diǎn)擊時主到,就將當(dāng)前狀態(tài)覆蓋到頁面中茶行。編輯配置入下:
const instances = [{
name: 'icnMineSettingB'
}, {
name: 'iconFindSearch'
}, {
name: 'minus'
}]
const editOption = {
field: null as string,
label: '',
editor: 'instance',
editable: true,
instance: instances
}
每一種圖標(biāo)樣式其實(shí)就是 name
屬性的不同,將這些 name
分別填充給實(shí)例化出來的組件登钥,就能看到上圖的效果畔师,每次點(diǎn)擊都會將 instances
中當(dāng)前項(xiàng)作為 props
覆蓋到頁面組件中,便實(shí)現(xiàn)了預(yù)期效果牧牢,并且類似需求都具有很強(qiáng)的通用性看锉。
通用屬性如何設(shè)置在組件上
每個組件都是一個 React Class
,其 defaultProps
屬性只要包含了 gaeaName
gaeaIcon
gaeaUniqueKey
和 gaeaEdit
屬性塔鳍,就擁有編輯功能伯铣。
gaeaName
和 gaeaIcon
分別是顯示在編輯器上的組件名和圖標(biāo)。
gaeaUniqueKey
是給每個組件起的唯一 key
轮纫,所有類的尋找都以此為依據(jù)腔寡。
gaeaEdit
是數(shù)組,存放了編輯類型蜡感。
一個基本的 gaeaEdit
對象如下:
gaeaEdit = [{
field: 'name',
label: '名稱',
editor: 'text',
editable: true
}]
editor
表示了當(dāng)前屬性用什么類型編輯器編輯蹬蚁,通用編輯類型有文本框恃泪,選擇框,開關(guān)等等犀斋,除此之外還有定制編輯類型贝乎,比如 background
。
field
表示了編輯后對應(yīng)改變哪個字段的值叽粹。
label
表示在編輯器上顯示的提示文案览效。
editor
還有許多類型,比如 editor: number
類型的配置如下(透明度就是封裝了 number
的編輯類型):
export const opacityEditor = {
field: 'style.opacity',
label: '透明度',
editor: 'number',
number: {
units: [{
key: '',
value: '%'
}],
currentUnit: '',
max: 100,
min: 0,
step: 1,
inputRange: [0, 100],
outputRange: [0, 1],
slider: true
},
editable: true
}
使用時我們直接放入 gaeaEdit
數(shù)組中:
gaeaEdit = [
opacityEditor
]
其中 utils
表示數(shù)字類型框可選的單位虫几,inputRange
outputRange
如上設(shè)置锤灿,那么編輯器中輸入框填入80,實(shí)際會轉(zhuǎn)換成 0.8 賦值到 opacity
屬性上辆脸。
因?yàn)橥ㄓ脤傩允枪潭ǖ牡#晕覀兲峁┝?gaeaHelper ,提供許多常用編輯類型:
import gaeaHelper from 'gaea-helper'
export class PropsGaea {
gaeaName = '圖標(biāo)'
gaeaIcon = 'square-o'
gaeaUniqueKey = 'wefan-icon'
gaeaEdit = [
'圖標(biāo)',
{
field: null as string,
label: '',
editor: 'instance',
editable: true,
instance: instances
},
'布局',
gaeaHelper.marginPaddingEditor,
gaeaHelper.widthHeightEditor,
'特效',
gaeaHelper.opacityEditor
]
}
最后我們寫自定義的 props 類集成描述編輯狀態(tài)的 PropsGaea
:
export class Props extends PropsGaea {
name = '名稱'
}
將其實(shí)例化后賦值在 defaultProps
即可:
static defaultProps = new Props()
自定義屬性編輯
值得尋味的是啡氢,通用屬性看起來其實(shí)更像定制屬性状囱,而自定義屬性其實(shí)更需要通用設(shè)計。
許多時候編輯器需要修改的屬性都是某些字段倘是,而這些字段都其對應(yīng)的類型和通用編輯規(guī)則亭枷,所以我們提供了基礎(chǔ)的 text
number
selector
switch
array
object
等通用編輯類型,并且通過額外配置來適配簡單需求搀崭。
比如 number
類型的編輯配置:
{
field: 'style.opacity',
label: '透明度',
editor: 'number',
number: {
units: [{
key: '',
value: '%'
}],
currentUnit: '',
max: 100,
min: 0,
step: 1,
inputRange: [0, 100],
outputRange: [0, 1],
slider: true
}
}
field
屬性支持 .
的方式訪問深層對象叨粘,比如 style 屬性的 opacity 字段就是這次要修改的字段。number
類型的編輯類型瘤睹,通過 number
字段描述其詳細(xì)設(shè)置升敲。比如最大最小值、單位默蚌、輸出轉(zhuǎn)換冻晤、按鈕調(diào)解速度、步長绸吸、是否擁有 Slider
做滑動調(diào)節(jié)。
自定義與通用屬性混合編輯
編輯器混合了通用屬性與自定義屬性设江,完全通過 gaeaEditor
這個字段來描述:
gaeaEdit = [
'圖標(biāo)',
{
field: null as string,
label: '',
editor: 'instance',
editable: true,
instance: instances
},
'布局',
gaeaHelper.marginPaddingEditor,
gaeaHelper.widthHeightEditor,
'特效',
gaeaHelper.opacityEditor
]
只要將兩者混合寫入數(shù)組即可锦茁,同時如果傳入的是字符串,會作為標(biāo)題分割叉存,方便區(qū)分功能區(qū)域码俩。
記錄編輯歷史
本來支持 undo
redo
快捷鍵是個邊角功能,但是由于需要編輯區(qū)的支持歼捏,所以也放在這一節(jié)說稿存。
Undo Redo
就像編輯 word 一樣笨篷,我們需要記錄每一次用戶操作,以便回退或者重做瓣履,記錄歷史有以下三種方案:
每次操作記錄全量編輯
json
率翅,撤銷的時候刷新整體視圖區(qū)域
這種方式太原始了,雖然操作方便不容易出錯袖迎,但弊端也非常明顯冕臭,就是占用內(nèi)存過大,每次記錄了全量數(shù)據(jù)肯定不是一件好事燕锥。
每次操作記錄增量編輯
json
, 撤銷的時候根據(jù)每一步驟做merge
辜贵,再刷新整體視圖區(qū)域
這種方式改進(jìn)了一下內(nèi)存占用,但缺點(diǎn)是刷新整體視圖區(qū)域的操作太笨重归形,如果視圖區(qū)域有 1000 個組件實(shí)例托慨,全量刷新就是一件很痛苦的事,我們操作時明明是局部刷新暇榴,為什么回退歷史要全量呢厚棵?
記錄每一步的操作類型、操作數(shù)據(jù)跺撼,回退時根據(jù)操作類型模擬人工操作
一個好的系統(tǒng)架構(gòu)窟感,是會將 action
store
分離出來的,我們手動拖拽歉井、編輯組件的時候柿祈,都會觸發(fā)對應(yīng) action
,進(jìn)而修改 store
哩至,自動觸發(fā)視圖區(qū)域刷新(利用了mobx)躏嚎,在回退歷史記錄的時候,我們只需要逆向調(diào)用對應(yīng)的 action
就能夠模擬出高性能人工操作菩貌,付出的代價是需要記錄不同操作類型卢佣,并記錄不同的數(shù)據(jù)格式。
分類記錄操作歷史
值得記錄的操作種類有 添加 移動 刪除 排序 更新組件屬性 粘貼 等箭阶,我們的 editor 還有 屬性重置 新增模板 這兩種操作屬性虚茶,下面是對這幾種操作類型的描述:
export interface Diff {
// 操作類型
type: 'add' | 'move' | 'remove' | 'exchange' | 'update' | 'paste' | 'reset' | 'addCombo' | 'addSource'
// 操作組件的 mapUniqueKey
mapUniqueKey: string
// 新增操作
add?: {
// 新增組件的唯一標(biāo)識 id
uniqueId: string
// 父級 mapKey
parentMapUniqueKey: string
// 插入的位置
index: number
}
// 移動到另一個父元素
move?: {
// 移動到的父級 mapKey
targetParentMapUniqueKey: string
// 移動前父級 mapKey
sourceParentMapUniqueKey: string
// 插入的位置
targetIndex: number
// 移除的位置
sourceIndex: number
}
// 刪除組件
remove?: DiffRemove
// 內(nèi)部交換順序
exchange?: {
oldIndex: number
newIndex: number
}
// 更新操作
update?: {
oldValue: ComponentProps
newValue: ComponentProps
}
// 粘貼操作
paste?: DiffRemove
// 重置組件
reset?: {
// 重置前的信息
beforeProps: ComponentProps
beforeName: string
}
// 新增組合
addCombo?: {
// 父級 mapKey
parentMapUniqueKey: string
// 父級的 index
index: number
// 組合的完整信息(不是 copy 的, 是真正對應(yīng)的 mapUniqueKey)
componentInfo: ViewportComponentFullInfo
}
// 新增模板
addSource?: {
// 父級 mapKey
parentMapUniqueKey: string
// 父級的 index
index: number
// 組合的完整信息(不是 copy 的, 是真正對應(yīng)的 mapUniqueKey)
componentInfo: ViewportComponentFullInfo
}
}
在 undo,redo時仇参,根據(jù)不同編輯類型還原操作嘹叫,就可以高效模擬操作了。
https://github.com/ascoders/gaea-editor/blob/master/gaea-editor/store/viewport.tsx#L768
上述倉庫地址中可以看到每一步歷史只存了還原它需要的最小字段诈乒,因此大大降低了內(nèi)存占用罩扇。順帶一提,因?yàn)槭褂昧?Mobx
打平 map
存儲視圖中的所有組件怕磨,因此每個組件都會保存對應(yīng) mapUniqueKey
來找到對應(yīng)實(shí)例喂饥。
undo
redo
操作效果如圖所示: