本文主要对以下技术要点进行分析:
- 编辑器
- 自定义组件
- 拖拽
- 删除组件、调整图层层级
- 放大缩小
- 撤消、重做
- 组件属性设置
- 吸附
- 预览、保存代码
- 绑定事件
- 绑定动画
- 导入 PSD
- 手机模式
为了让本文更加容易理解,我将以上技术要点结合在一起写了一个可视化拖拽组件库 DEMO:
建议结合源码一起阅读,效果更好(这个 DEMO 使用的是 Vue 技术栈)。
1. 编辑器
先来看一下页面的整体结构。
这一节要讲的编辑器其实就是中间的画布。它的作用是:当从左边组件列表拖拽出一个组件放到画布中时,画布要把这个组件渲染出来。
这个编辑器的实现思路是:
- 用一个数组
componentData
维护编辑器中的数据。 - 把组件拖拽到画布中时,使用
push()
方法将新的组件数据添加到componentData
。 - 编辑器使用
v-for
指令遍历componentData
,将每个组件逐个渲染到画布(也可以使用 JSX 语法结合render()
方法代替)。
编辑器渲染的核心代码如下所示:
<component v-for="item in componentData" :key="item.id" :is="item.component" :style="item.style" :propValue="item.propValue" />
每个组件数据大概是这样:
{ component: 'v-text', // 组件名称,需要提前注册到 Vue label: '文字', // 左侧组件列表中显示的名字 propValue: '文字', // 组件所使用的值 icon: 'el-icon-edit', // 左侧组件列表中显示的名字 animations: [], // 动画列表 events: {}, // 事件列表 style: { // 组件样式 width: 200, height: 33, fontSize: 14, fontWeight: 500, lineHeight: '', letterSpacing: 0, textAlign: '', color: '', }, }
在遍历 componentData
组件数据时,主要靠 is
属性来识别出真正要渲染的是哪个组件。
例如要渲染的组件数据是 { component: 'v-text' }
,则 会被转换为
。当然,你这个组件也要提前注册到 Vue 中。
如果你想了解更多 is
属性的资料,请查看官方文档。
2. 自定义组件
原则上使用第三方组件也是可以的,但建议你最好封装一下。不管是第三方组件还是自定义组件,每个组件所需的属性可能都不一样,所以每个组件数据可以暴露出一个属性 propValue
用于传递值。
例如 a 组件只需要一个属性,你的 propValue
可以这样写:propValue: 'aaa'
。如果需要多个属性,propValue
则可以是一个对象:
propValue: { a: 1, b: 'text' }
在这个 DEMO 组件库中我定义了三个组件。
图片组件 Picture
:
<template> <div style="overflow: hidden"> <img :src="propValue"> </div> </template> <script> export default { props: { propValue: { type: String, require: true, }, }, } </script>
按钮组件 VButton
:
<template> <button class="v-button">{{ propValue }}</button> </template> <script> export default { props: { propValue: { type: String, default: '', }, }, } </script>
文本组件 VText
:
<template> <textarea v-if="editMode == 'edit'" :value="propValue" class="text textarea" @input="handleInput" ref="v-text" ></textarea> <div v-else class="text disabled"> <div v-for="(text, index) in propValue.split('\n')" :key="index">{{ text }}</div> </div> </template> <script> import { mapState } from 'vuex' export default { props: { propValue: { type: String, }, element: { type: Object, }, }, computed: mapState([ 'editMode', ]), methods: { handleInput(e) { this.$emit('input', this.element, e.target.value) }, }, } </script>
3. 拖拽
从组件列表到画布
一个元素如果要设为可拖拽,必须给它添加一个 draggable
属性。另外,在将组件列表中的组件拖拽到画布中,还有两个事件是起到关键作用的:
dragstart
事件,在拖拽刚开始时触发。它主要用于将拖拽的组件信息传递给画布。drop
事件,在拖拽结束时触发。主要用于接收拖拽的组件信息。
先来看一下左侧组件列表的代码:
<div @dragstart="handleDragStart" class="component-list"> <div v-for="(item, index) in componentList" :key="index" class="list" draggable :data-index="index"> <i :class="item.icon"></i> <span>{{ item.label }}</span> </div> </div> handleDragStart(e) { e.dataTransfer.setData('index', e.target.dataset.index) }
可以看到给列表中的每一个组件都设置了 draggable
属性。另外,在触发 dragstart
事件时,使用 dataTransfer.setData()
传输数据。再来看一下接收数据的代码:
<div class="content" @drop="handleDrop" @dragover="handleDragOver" @click="deselectCurComponent"> <Editor /> </div> handleDrop(e) { e.preventDefault() e.stopPropagation() const component = deepCopy(componentList[e.dataTransfer.getData('index')]) this.$store.commit('addComponent', component) }
触发 drop
事件时,使用 dataTransfer.getData()
接收传输过来的索引数据,然后根据索引找到对应的组件数据,再添加到画布,从而渲染组件。
组件在画布中移动
首先需要将画布设为相对定位 position: relative
,然后将每个组件设为绝对定位 position: absolute
。除了这一点外,还要通过监听三个事件来进行移动:
mousedown
事件,在组件上按下鼠标时,记录组件当前的位置,即 xy 坐标(为了方便讲解,这里使用的坐标轴,实际上 xy 对应的是 css 中的left
和top
。mousemove
事件,每次鼠标移动时,都用当前最新的 xy 坐标减去最开始的 xy 坐标,从而计算出移动距离,再改变组件位置。mouseup
事件,鼠标抬起时结束移动。
handleMouseDown(e) { e.stopPropagation() this.$store.commit('setCurComponent', { component: this.element, zIndex: this.zIndex }) const pos = { ...this.defaultStyle } const startY = e.clientY const startX = e.clientX // 如果直接修改属性,值的类型会变为字符串,所以要转为数值型 const startTop = Number(pos.top) const startLeft = Number(pos.left) const move = (moveEvent) => { const currX = moveEvent.clientX const currY = moveEvent.clientY pos.top = currY - startY + startTop pos.left = currX - startX + startLeft // 修改当前组件样式 this.$store.commit('setShapeStyle', pos) } const up = () => { document.removeEventListener('mousemove', move) document.removeEventListener('mouseup', up) } document.addEventListener('mousemove', move) document.addEventListener('mouseup', up) }
PS: 有很多网友反馈拖拽的时候有卡顿现象,其实解决方案很简单,把浏览器的控制台关掉即可。
4. 删除组件、调整图层层级
改变图层层级
由于拖拽组件到画布中是有先后顺序的,所以可以按照数据顺序来分配图层层级。
例如画布新增了五个组件 abcde,那它们在画布数据中的顺序为 [a, b, c, d, e]
,图层层级和索引一一对应,即它们的 z-index
属性值是 01234(后来居上)。用代码表示如下:
<div v-for="(item, index) in componentData" :zIndex="index">div>
如果不了解 z-index
属性的,请看一下 MDN 文档。
理解了这一点之后,改变图层层级就很容易做到了。改变图层层级,即是改变组件数据在 componentData
数组中的顺序。例如有 [a, b, c]
三个组件,它们的图层层级从低到高顺序为 abc(索引越大,层级越高)。
如果要将 b 组件上移,只需将它和 c 调换顺序即可:
const temp = componentData[1] componentData[1] = componentData[2] componentData[2] = temp
同理,置顶置底也是一样,例如我要将 a 组件置顶,只需将 a 和最后一个组件调换顺序即可:
const temp = componentData[0] componentData[0] = componentData[componentData.lenght - 1] componentData[componentData.lenght - 1] = temp
删除组件
删除组件非常简单,一行代码搞定:componentData.splice(index, 1)
。