We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
上一篇说了如何实现灵活的拖拽,那么加上编辑功能,拖拽编辑器的两大核心功能就集齐了,剩下就是组件树、版本管理、模板、预览、快捷键、事件、动画、在线编辑代码以及部署方式这些边角功能,当然这些边角功能都不影响大局,这次我们来谈谈如何设计编辑区,类似下图的结构:
mobx
我们发现,样式才是最通用的属性,无论何种组件都逃离不了样式的设置,除此以外的属性都是自定义的,我们无法抽象出共性加以定制,但是样式是固定的,所以编辑区先要支持通用样式的编辑。
通用样式:背景 边框 字体 边距 布局 溢出处理 宽高 透明度
背景
边框
字体
边距
布局
溢出处理
宽高
透明度
我们提供了对应的 13 余中定制编辑类型,比如像上图的边距调节器,专门针对边距进行修改,只要将编辑类型设置为 marginPadding ,编辑框中就会出现非常方便的边距调节器。
marginPadding
还有一种通用属性处理,比如有一个图标组件,实现以下效果:
如果单独为图标类型设置一种编辑状态很不划算,这种分类可以划为 实例类型,每一个图标其实是这个组件接收了某种参数后的状态,我们预先提供这些状态,编辑器将这些状态的组件分别实例化显示出来,每当鼠标点击时,就将当前状态覆盖到页面中。编辑配置入下:
实例类型
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 覆盖到页面组件中,便实现了预期效果,并且类似需求都具有很强的通用性。
name
instances
props
每个组件都是一个 React Class ,其 defaultProps 属性只要包含了 gaeaName gaeaIcon gaeaUniqueKey 和 gaeaEdit 属性,就拥有编辑功能。
React Class
defaultProps
gaeaName
gaeaIcon
gaeaUniqueKey
gaeaEdit
gaeaName 和 gaeaIcon 分别是显示在编辑器上的组件名和图标。
gaeaUniqueKey 是给每个组件起的唯一 key,所有类的寻找都以此为依据。
key
gaeaEdit 是数组,存放了编辑类型。
一个基本的 gaeaEdit 对象如下:
gaeaEdit = [{ field: 'name', label: '名称', editor: 'text', editable: true }]
editor 表示了当前属性用什么类型编辑器编辑,通用编辑类型有文本框,选择框,开关等等,除此之外还有定制编辑类型,比如 background。
editor
background
field 表示了编辑后对应改变哪个字段的值。
field
label 表示在编辑器上显示的提示文案。
label
editor 还有许多类型,比如 editor: number 类型的配置如下(透明度就是封装了 number 的编辑类型):
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 属性上。
utils
inputRange
outputRange
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:
PropsGaea
export class Props extends PropsGaea { name = '名称' }
将其实例化后赋值在 defaultProps 即可:
static defaultProps = new Props()
值得寻味的是,通用属性看起来其实更像定制属性,而自定义属性其实更需要通用设计。
许多时候编辑器需要修改的属性都是某些字段,而这些字段都其对应的类型和通用编辑规则,所以我们提供了基础的 text number selector switch array object 等通用编辑类型,并且通过额外配置来适配简单需求。
text
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 做滑动调节。
.
Slider
编辑器混合了通用属性与自定义属性,完全通过 gaeaEditor 这个字段来描述:
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
这种方式太原始了,虽然操作方便不容易出错,但弊端也非常明显,就是占用内存过大,每次记录了全量数据肯定不是一件好事。
每次操作记录增量编辑 json, 撤销的时候根据每一步骤做 merge ,再刷新整体视图区域
merge
这种方式改进了一下内存占用,但缺点是刷新整体视图区域的操作太笨重,如果视图区域有 1000 个组件实例,全量刷新就是一件很痛苦的事,我们操作时明明是局部刷新,为什么回退历史要全量呢?
记录每一步的操作类型、操作数据,回退时根据操作类型模拟人工操作
一个好的系统架构,是会将 action store 分离出来的,我们手动拖拽、编辑组件的时候,都会触发对应 action,进而修改 store,自动触发视图区域刷新(利用了mobx),在回退历史记录的时候,我们只需要逆向调用对应的 action 就能够模拟出高性能人工操作,付出的代价是需要记录不同操作类型,并记录不同的数据格式。
action
store
值得记录的操作种类有 添加 移动 删除 排序 更新组件属性 粘贴 等,我们的 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 来找到对应实例。
Mobx
map
mapUniqueKey
undo redo 操作效果如图所示:
The text was updated successfully, but these errors were encountered:
可视化编辑器设计
Sorry, something went wrong.
No branches or pull requests
React Editor 应用编辑器(2) - 编辑区基本设计
上一篇说了如何实现灵活的拖拽,那么加上编辑功能,拖拽编辑器的两大核心功能就集齐了,剩下就是组件树、版本管理、模板、预览、快捷键、事件、动画、在线编辑代码以及部署方式这些边角功能,当然这些边角功能都不影响大局,这次我们来谈谈如何设计编辑区,类似下图的结构:
mobx
很好的解决了这个问题,本篇文章因为重点描述编辑器设计,因此数据设计部分不会过多涉及。通用属性编辑
我们发现,样式才是最通用的属性,无论何种组件都逃离不了样式的设置,除此以外的属性都是自定义的,我们无法抽象出共性加以定制,但是样式是固定的,所以编辑区先要支持通用样式的编辑。
通用样式:
背景
边框
字体
边距
布局
溢出处理
宽高
透明度
我们提供了对应的 13 余中定制编辑类型,比如像上图的边距调节器,专门针对边距进行修改,只要将编辑类型设置为
marginPadding
,编辑框中就会出现非常方便的边距调节器。还有一种通用属性处理,比如有一个图标组件,实现以下效果:
如果单独为图标类型设置一种编辑状态很不划算,这种分类可以划为
实例类型
,每一个图标其实是这个组件接收了某种参数后的状态,我们预先提供这些状态,编辑器将这些状态的组件分别实例化显示出来,每当鼠标点击时,就将当前状态覆盖到页面中。编辑配置入下:每一种图标样式其实就是
name
属性的不同,将这些name
分别填充给实例化出来的组件,就能看到上图的效果,每次点击都会将instances
中当前项作为props
覆盖到页面组件中,便实现了预期效果,并且类似需求都具有很强的通用性。通用属性如何设置在组件上
每个组件都是一个
React Class
,其defaultProps
属性只要包含了gaeaName
gaeaIcon
gaeaUniqueKey
和gaeaEdit
属性,就拥有编辑功能。gaeaName
和gaeaIcon
分别是显示在编辑器上的组件名和图标。gaeaUniqueKey
是给每个组件起的唯一key
,所有类的寻找都以此为依据。gaeaEdit
是数组,存放了编辑类型。一个基本的
gaeaEdit
对象如下:editor
表示了当前属性用什么类型编辑器编辑,通用编辑类型有文本框,选择框,开关等等,除此之外还有定制编辑类型,比如background
。field
表示了编辑后对应改变哪个字段的值。label
表示在编辑器上显示的提示文案。editor
还有许多类型,比如editor: number
类型的配置如下(透明度就是封装了number
的编辑类型):使用时我们直接放入
gaeaEdit
数组中:其中
utils
表示数字类型框可选的单位,inputRange
outputRange
如上设置,那么编辑器中输入框填入80,实际会转换成 0.8 赋值到opacity
属性上。因为通用属性是固定的,所以我们提供了 gaeaHelper ,提供许多常用编辑类型:
最后我们写自定义的 props 类集成描述编辑状态的
PropsGaea
:将其实例化后赋值在
defaultProps
即可:自定义属性编辑
值得寻味的是,通用属性看起来其实更像定制属性,而自定义属性其实更需要通用设计。
许多时候编辑器需要修改的属性都是某些字段,而这些字段都其对应的类型和通用编辑规则,所以我们提供了基础的
text
number
selector
switch
array
object
等通用编辑类型,并且通过额外配置来适配简单需求。比如
number
类型的编辑配置:field
属性支持.
的方式访问深层对象,比如 style 属性的 opacity 字段就是这次要修改的字段。number
类型的编辑类型,通过number
字段描述其详细设置。比如最大最小值、单位、输出转换、按钮调解速度、步长、是否拥有Slider
做滑动调节。自定义与通用属性混合编辑
编辑器混合了通用属性与自定义属性,完全通过
gaeaEditor
这个字段来描述:只要将两者混合写入数组即可,同时如果传入的是字符串,会作为标题分割,方便区分功能区域。
记录编辑历史
本来支持
undo
redo
快捷键是个边角功能,但是由于需要编辑区的支持,所以也放在这一节说。Undo Redo
就像编辑 word 一样,我们需要记录每一次用户操作,以便回退或者重做,记录历史有以下三种方案:
这种方式太原始了,虽然操作方便不容易出错,但弊端也非常明显,就是占用内存过大,每次记录了全量数据肯定不是一件好事。
这种方式改进了一下内存占用,但缺点是刷新整体视图区域的操作太笨重,如果视图区域有 1000 个组件实例,全量刷新就是一件很痛苦的事,我们操作时明明是局部刷新,为什么回退历史要全量呢?
一个好的系统架构,是会将
action
store
分离出来的,我们手动拖拽、编辑组件的时候,都会触发对应action
,进而修改store
,自动触发视图区域刷新(利用了mobx),在回退历史记录的时候,我们只需要逆向调用对应的action
就能够模拟出高性能人工操作,付出的代价是需要记录不同操作类型,并记录不同的数据格式。分类记录操作历史
值得记录的操作种类有 添加 移动 删除 排序 更新组件属性 粘贴 等,我们的 editor 还有 属性重置 新增模板 这两种操作属性,下面是对这几种操作类型的描述:
在 undo,redo时,根据不同编辑类型还原操作,就可以高效模拟操作了。
https://github.com/ascoders/gaea-editor/blob/master/gaea-editor/store/viewport.tsx#L768
上述仓库地址中可以看到每一步历史只存了还原它需要的最小字段,因此大大降低了内存占用。顺带一提,因为使用了
Mobx
打平map
存储视图中的所有组件,因此每个组件都会保存对应mapUniqueKey
来找到对应实例。undo
redo
操作效果如图所示:The text was updated successfully, but these errors were encountered: