美文网首页编程NT-TECHReact Native实践
React Editor 应用编辑器(2) - 编辑区基本设计

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

作者: 黄子毅 | 来源:发表于2016-10-18 16:19 被阅读1629次

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

    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

    相关文章

      网友评论

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

      本文链接:https://www.haomeiwen.com/subject/umrtuttx.html