通过上一篇文章,我们对乔巴乐高海报平台的整体架构有了初步的了解。今天我们深入编辑器部分,分析难点和实现细节。这是目前制作出来的编辑器页面:对应原型图:不难看出,和市面上大部分低代码平台一样,它由三部分组成:左边的组件列表,中间的canvas区域,和右边的物业区。大致的操作流程是将左侧的组件拖到中间的画布上,选中组件,右侧的属性面板会显示该组件关联的属性。编辑右侧的属性,画布中对应的组件样式会同步更新。页面拼接完成。可以看出,组件是串联的。在上一篇文章中,我们粗略地分析了整体页面和组件的数据结构,并没有细化。提取文本、图片、素材组件的通用特征:尺寸属性(Size)、宽度??(width)、高度(height)、填充属性(Padding)、顶部填充(padding-top)、右侧填充(padding-right),和底边距(padding-bottom))左边距(padding-left)视觉格式属性指定如何定位元素(position)指定被定位元素的上边缘位置(top)指定右边的位置定位元素的边缘(右)指定定位元素底部边缘的位置(底部)指定放置元素左边缘的位置(左)将一个或多个阴影应用于元素的框(框阴影)颜色property(Color)Transparency(opacity)Borderproperty(Border)设置元素的所有四个边border-color设置元素所有四个边的边框宽度(border-width)设置所有四个边的边框样式元素的两侧(边框样式)定义了s元素的边框角的形状(border-radius)除此之外,文本组件还有以下属性:字体属性(Fonts)定义元素的字体列表(font-family)定义文本的字体大小(font-size)定义文本的字体样式(font-style)指定文本的字体Font-weight文本属性(Text)设置行内内容的水平对齐方式(text-align)指定添加到文本的装饰(text-decoration)设置文本行间距(line-height)图片组件还有:图片属性(Image)图片链接(src)材质组件还有:背景属性(Background)定义元素的背景颜色(background-color)我们把上面的操作流程拆解成三步:1??把左边的Components拖到中间的画布上2??选中组件,右边的属性面板会显示组件关联的属性3??编辑属性上里ght,canvas中对应的组件样式会同步更新1.向canvas中添加组件通过上一篇文章,我们知道编辑器的整体数据结构是这样设计的:state:{//添加的所有组件数据到画布componentData:[],}reducers:{//将组件添加到componentDataaddComponentData(){},//编辑组件,更新componentData和curComponenteditComponentData(){},//删除组件delComponentData(){}}那么从左边的组件列表中添加一个组件到画布的操作,其实就是把一个组件数据push到componentData中。这里主要关注如何设计组件列表:为了方便用户快速创建活动,最好为组件列表预置一些模板。其实就是为文字、图片、素材提供一些已有的元素。这样当对应的组件被点击添加到画布时,就会提交一个mutation来修改store中的componentData。这里的组件列表底层渲染也是使用组件库,只是不同模板的props不一样。2.所选组件显示其关联属性。当在画布中选择一个特定的组件时,我们需要知道当前选中的是哪个组件,也就是说需要一个变量来存储当前高亮的组件。然后在store中添加setActive和getCurrentElement:consteditorModule={state:{componentData:[],currentElement:"",},mutations:{addComponentData(state,component){},setActive(state,id){state.currentElement=编号;},},getters:{getCurrentElement:(state)=>{returnstate.componentData.find((component)=>component.id===state.currentElement);},}}当在画布中被选中的组件被激活时,会触发setActive更新currentElement。(当前正在操作的组件可以通过getCurrentElement获取)。这时候,如何在右侧的属性区动态显示不同组件的不同属性呢?对于单个组件,属性面板应该是语义化的。无论您是开发人员还是非开发人员,都可以通过属性面板的操作区直观地了解一个组件的属性有哪些以及如何使用和编辑。那么属性面板应该包含什么?1.标签:属性名称。这样可以明确的告诉具体属性的作用,比如元素的宽高,边框,背景色等。2.description:属性的描述信息。对于一些特殊的属性,您可能无法在第一时间通过标签直观地识别属性的含义,可以添加描述信息进行阐述。3.内容:属性渲染器。用户可以在此基础上修改属性。最常见的有textarea、input、select等。4.error:属性校验信息。当用户输入非法或类型不匹配时,可以给出适当的错误信息。通过上面的描述,我们会发现其实这也是我们常用的形式。对应以上组件的props信息,我们可以对这些属性进行分类,那么分类标准是什么呢?我觉得应该把属性和js中的数据类型进行映射,然后在具体的类别下选择合适的渲染器。我们知道,在JavaScript中,一共有七种数据类型,字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、符号(Symbol)和对象(Object)。对象类型包括:数组(Array)、函数(Function),以及两种特殊对象:正则(RegExp)和日期(Date)。空(Null)、未定义(Undefined)、符号和正则(RegExp)在渲染器中基本不用。我们来看看String(字符串)、数字(Number)、布尔(Boolean)和日期(Date)可能的渲染方式:Boolean)renderertypecomponentswitchDate(Date)renderertypecomponentdate除了这些类型之外,还有对象(Object)、数组(Array)、函数(Function)。对象和数组是比较复杂的类型,但是我们可以将它们抽象成多级(可以理解为嵌套)的基本数据类型:渲染器类型组件数组一般像数组一样以下拉框的形式显示。至于函数(Function),可以是预定义的形式:renderer类型的组件函数就在这里,不难想象我们需要维护一个属性和一个表单组件的对应关系。属性对应上面的key,比如borderColor、text、width、fontFamily等,那components呢?组件其实就是属性的具体表现。比如width可以使用数字输入框,text可以使用普通输入框。然而,对于一些更复杂的特性,我们自己实现这些组件似乎有些吃力。这时候我们可以考虑将其与已有的组件库结合起来(这里我使用的是AntDesignVue)。那么这样一来,属性prop与组件base的对应关系就是:constmapPropsToComponents={text:{component:"a-input",},width:{component:"a-input-number",},borderWidth:{component:"a-slider",},//...}但这只是满足了一般的基础组件设计,像一些独特的属性或者基础组件无法满足的情况,我们需要对其进行扩展:rendering上面提到的上传组件和选色组件需要单独实现。3.编辑属性,同步更新画布。以上只是初步建立了属性与组件的对应关系。我们还没有解决组件初始值显示、复杂组件显示、表单值更新后如何同步更新画布的问题。其实为了简化问题,这就是表单的回显和更新问题。根据我以往的经验:在设计表单组件时,有两件事是必须的:表单的初始值(默认值),用于初始显示使用更改表单属性的事件(默认为change)对于不同的表单,初始值参数的处理与属性改变后的不同:对于高度、宽度等数值类型,在表单中传入时应为number(24)类型,在属性后改变,事件参数类型为string(24px)字体是否为粗体、斜体、下划线,传入表单时为boolean(true/false)类型,属性改变后,事件参数应该是string(bold/normal)类型的,所以给每个属性传入表单,事件发生变化后,必须额外增加一个转换函数来处理值:initialValueConverteventChangeValueConvert并且在给属性赋值的时候,不是all表单控件接收值,比如checkbox被选中,这种单独抽象一个属性valueProp可以用来控制它。其次,对于上面提到的复杂组件的渲染(这里我们指的是父子层级),除了组件之外,还额外增加了一个subComponent。完善后,属性prop和component的对应关系为:constmapPropsToComponents={text:{component:"textarea-fix",eventName:"change",valueProp:"value",initialValueConvert:(v:any)=>v,eventChangeValueConvert:(e:any)=>e.target.value,text:"文本",},width:{component:"a-input-number",eventName:"change",valueProp:"value",initialValueConvert:(v:string)=>(v?parseInt(v):""),eventChangeValueConvert:(e:number)=>(e?`${e}px`:""),text:"广度",},textDecoration:{component:“icon-switch”,eventName:“change”,initialValueConvert:(v:string)=>v===“underline”,eventChangeValueConvert:(e:boolean)=>(e?“underline”:"none"),valueProp:"checked",},backgroundSize:{组件:"a-select",eventName:"change",initialValueConvert:(v:any)=>v,eventChangValueConvert:(e:any)=>e,subComponent:"a-select-option",text:"backgroundsize",options:[{value:"contain",text:"autoscale"},{value:"封面",text:"autofill"},{value:"",text:"default"},],},//...}我们的数据总是自上而下的顺序,也就是说表单最终会更新反映到整个商店。这时,我们在相应的组件中发出一个事件(变化)。当变化发生时,我们可以知道它是哪个元素的哪个属性,以及新的值是什么。我们利用这些信息来更新这个值,这样store就更新了,元素的props就更新了,整个数据流就完成了。4.Canvas区域交互设计实现上面说了这么多,基本上就是讲左边的组件区域,中间的canvas区域,右边的属性区域之间的数据流转。最后,让我们谈谈画布区域本身的一些更复杂的交互实现。我大致梳理了这几种:dragging(组件在canvas中移动)componentlayerzoomin/outundo/redodragging(组件在canvas中移动)这个比较简单,就是mousedown、mousemove和mouseup事件的组合:鼠标在组件上按下,记录组件当前位置,即x和y坐标(对应css中的left和top);每次鼠标移动时,将当前最新的xy坐标减去初始的xy坐标,计算移动的距离,然后更新组件位置;抬起鼠标时结束移动。组件图层图层面板主要是控制组件的显示/隐藏,不同组件的层级关系,点击选择。这里主要讲层次关系。一般情况下,我们会选择使用z-index来控制层级。但是这里我没有使用z-index,而是利用了级联领域的第二条黄金法则。级联字段中的黄金法则:1、谁大谁胜:当在同一个级联上下文字段中有明显的级联级别标记,比如识别出的z-index值时,级联级别值大的将覆盖较小的那个。2、从后面来:当元素的堆叠层次和堆叠顺序相同时,DOM流中靠后的元素会覆盖前面的元素。为什么选择第二种而不是最常见的第一种?首先,我们需要一个层列表来对每个组件对应的层进行排序。其实就是对store中的组件进行排序,也就是对数组进行排序。然后在图层列表中,如果要添加某个图片图层的层级可以放在后面(这样渲染的时候,数组后面的元素就会在DOM流的后面。对应的层叠顺序也会是ontop),这样不仅操作方便,而且不需要增加额外的冗余代码,可以说是一箭双雕。放大/缩小核心实现:在canvas组件的四个角(↖?、↗?、↙?、↘?)各加一个小点:左上:左上组件都缩小了;width和height都增加了Upperright:componentleft没有变化,top减少了;width,heightbothincreaseBottomleft:componentleft减少,top不变;width,height都增加Bottomright:componentleft,top保持不变;width,height都增加Undo/RedoUndo和redo实际上是我们一直在使用的操作。对应的快捷键一般是?Z/Ctrl+Z,??Z/Ctrl+Shift+Z。这个功能很常见。可以大大改善用户体验,提高编辑效率,但是如何用代码实现呢?
