React Editor 应用编辑器(2) - 编辑区基本设计

上一篇说了如何实现灵活的拖拽,那么加上编辑功能,拖拽编辑器的两大核心功能就集齐了,剩下就是组件树、版本管理、模板、预览、快捷键、事件、动画、在线编辑代码以及部署方式这些边角功能,当然这些边角功能都不影响大局,这次我们来谈谈如何设计编辑区,类似下图的结构:

editor.gif
  1. 从图中可以看出,编辑区涉及很多数据同步操作,我们使用了 mobx 很好的解决了这个问题,本篇文章因为重点描述编辑器设计,因此数据设计部分不会过多涉及。
  2. 除了基本属性设置,还应该有脚本设置、事件设置、动画设置,这些后续文章再讨论。

通用属性编辑

我们发现,样式才是最通用的属性,无论何种组件都逃离不了样式的设置,除此以外的属性都是自定义的,我们无法抽象出共性加以定制,但是样式是固定的,所以编辑区先要支持通用样式的编辑。

通用样式:背景 边框 字体 边距 布局 溢出处理 宽高 透明度

我们提供了对应的 13 余中定制编辑类型,比如像上图的边距调节器,专门针对边距进行修改,只要将编辑类型设置为 marginPadding ,编辑框中就会出现非常方便的边距调节器。

还有一种通用属性处理,比如有一个图标组件,实现以下效果:

icon.gif

如果单独为图标类型设置一种编辑状态很不划算,这种分类可以划为 实例类型每一个图标其实是这个组件接收了某种参数后的状态,我们预先提供这些状态,编辑器将这些状态的组件分别实例化显示出来,每当鼠标点击时,就将当前状态覆盖到页面中。编辑配置入下:

const instances = [{
    name: 'icnMineSettingB'
}, {
    name: 'iconFindSearch'
}, {
    name: 'minus'
}]

const editOption = {
    field: null as string,
    label: '',
    editor: 'instance',
    editable: true,
    instance: instances
}

每一种图标样式其实就是 name 属性的不同,将这些 name 分别填充给实例化出来的组件,就能看到上图的效果,每次点击都会将 instances 中当前项作为 props 覆盖到页面组件中,便实现了预期效果,并且类似需求都具有很强的通用性。

通用属性如何设置在组件上

每个组件都是一个 React Class ,其 defaultProps 属性只要包含了 gaeaName gaeaIcon gaeaUniqueKeygaeaEdit 属性,就拥有编辑功能。

gaeaNamegaeaIcon 分别是显示在编辑器上的组件名和图标。

gaeaUniqueKey 是给每个组件起的唯一 key,所有类的寻找都以此为依据。

gaeaEdit 是数组,存放了编辑类型。

一个基本的 gaeaEdit 对象如下:

gaeaEdit = [{
    field: 'name',
    label: '名称',
    editor: 'text',
    editable: true
}]

editor 表示了当前属性用什么类型编辑器编辑,通用编辑类型有文本框,选择框,开关等等,除此之外还有定制编辑类型,比如 background

field 表示了编辑后对应改变哪个字段的值。

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 数组中:

gaeaEdit = [
    opacityEditor
]

其中 utils 表示数字类型框可选的单位,inputRange outputRange 如上设置,那么编辑器中输入框填入80,实际会转换成 0.8 赋值到 opacity 属性上。

因为通用属性是固定的,所以我们提供了 gaeaHelper ,提供许多常用编辑类型:

import gaeaHelper from 'gaea-helper'

export class PropsGaea {
    gaeaName = '图标'
    gaeaIcon = 'square-o'
    gaeaUniqueKey = 'wefan-icon'
    gaeaEdit = [
        '图标',
        {
            field: null as string,
            label: '',
            editor: 'instance',
            editable: true,
            instance: instances
        },
        '布局',
        gaeaHelper.marginPaddingEditor,
        gaeaHelper.widthHeightEditor,
        '特效',
        gaeaHelper.opacityEditor
    ]
}

最后我们写自定义的 props 类集成描述编辑状态的 PropsGaea

export class Props extends PropsGaea {
    name = '名称'
}

将其实例化后赋值在 defaultProps 即可:

static defaultProps = new Props()

自定义属性编辑

值得寻味的是,通用属性看起来其实更像定制属性,而自定义属性其实更需要通用设计。

许多时候编辑器需要修改的属性都是某些字段,而这些字段都其对应的类型和通用编辑规则,所以我们提供了基础的 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 字段描述其详细设置。比如最大最小值、单位、输出转换、按钮调解速度、步长、是否拥有 Slider 做滑动调节。

自定义与通用属性混合编辑

编辑器混合了通用属性与自定义属性,完全通过 gaeaEditor 这个字段来描述:

gaeaEdit = [
    '图标',
    {
        field: null as string,
        label: '',
        editor: 'instance',
        editable: true,
        instance: instances
    },
    '布局',
    gaeaHelper.marginPaddingEditor,
    gaeaHelper.widthHeightEditor,
    '特效',
    gaeaHelper.opacityEditor
]

只要将两者混合写入数组即可,同时如果传入的是字符串,会作为标题分割,方便区分功能区域。

记录编辑历史

本来支持 undo redo 快捷键是个边角功能,但是由于需要编辑区的支持,所以也放在这一节说。

Undo Redo

就像编辑 word 一样,我们需要记录每一次用户操作,以便回退或者重做,记录历史有以下三种方案:

每次操作记录全量编辑 json,撤销的时候刷新整体视图区域

这种方式太原始了,虽然操作方便不容易出错,但弊端也非常明显,就是占用内存过大,每次记录了全量数据肯定不是一件好事。

每次操作记录增量编辑 json, 撤销的时候根据每一步骤做 merge ,再刷新整体视图区域

这种方式改进了一下内存占用,但缺点是刷新整体视图区域的操作太笨重,如果视图区域有 1000 个组件实例,全量刷新就是一件很痛苦的事,我们操作时明明是局部刷新,为什么回退历史要全量呢?

记录每一步的操作类型、操作数据,回退时根据操作类型模拟人工操作

一个好的系统架构,是会将 action store 分离出来的,我们手动拖拽、编辑组件的时候,都会触发对应 action,进而修改 store,自动触发视图区域刷新(利用了mobx),在回退历史记录的时候,我们只需要逆向调用对应的 action 就能够模拟出高性能人工操作,付出的代价是需要记录不同操作类型,并记录不同的数据格式。

分类记录操作历史

值得记录的操作种类有 添加 移动 删除 排序 更新组件属性 粘贴 等,我们的 editor 还有 属性重置 新增模板 这两种操作属性,下面是对这几种操作类型的描述:

export interface Diff {
    // 操作类型
    type: 'add' | 'move' | 'remove' | 'exchange' | 'update' | 'paste' | 'reset' | 'addCombo' | 'addSource'
    // 操作组件的 mapUniqueKey
    mapUniqueKey: string
    // 新增操作
    add?: {
        // 新增组件的唯一标识 id
        uniqueId: string
        // 父级 mapKey
        parentMapUniqueKey: string
        // 插入的位置
        index: number
    }
    // 移动到另一个父元素
    move?: {
        // 移动到的父级 mapKey
        targetParentMapUniqueKey: string
        // 移动前父级 mapKey
        sourceParentMapUniqueKey: string
        // 插入的位置
        targetIndex: number
        // 移除的位置
        sourceIndex: number
    }
    // 删除组件
    remove?: DiffRemove
    // 内部交换顺序
    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 的, 是真正对应的 mapUniqueKey)
        componentInfo: ViewportComponentFullInfo
    }
    // 新增模板
    addSource?: {
        // 父级 mapKey
        parentMapUniqueKey: string
        // 父级的 index
        index: number
        // 组合的完整信息(不是 copy 的, 是真正对应的 mapUniqueKey)
        componentInfo: ViewportComponentFullInfo
    }
}

在 undo,redo时,根据不同编辑类型还原操作,就可以高效模拟操作了。

https://github.com/ascoders/gaea-editor/blob/master/gaea-editor/store/viewport.tsx#L768

上述仓库地址中可以看到每一步历史只存了还原它需要的最小字段,因此大大降低了内存占用。顺带一提,因为使用了 Mobx 打平 map 存储视图中的所有组件,因此每个组件都会保存对应 mapUniqueKey 来找到对应实例。

undo redo 操作效果如图所示:

undo-redo.gif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,548评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,497评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,990评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,618评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,618评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,246评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,819评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,725评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,268评论 1 320
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,356评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,488评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,181评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,862评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,331评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,445评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,897评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,500评论 2 359

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,696评论 18 139
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 11,004评论 6 13
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,662评论 18 399
  • 今天和老妹一起出去逛街,她说大洋百货做活动充1000有2300,特别划算。 于是我们背上大包,抱上一一,逛街去了!...
    叶听雨阅读 2,019评论 0 1
  • 文/老葫芦 (2017年3月25日) 何时对你如此依赖 记不清那个流年 丝丝一吸的醉 放飞在每日的夜晚 风吹得不是...
    老葫芦阅读 536评论 0 1