关于前端:可视化拖拽组件库一些技术要点原理分析

7次阅读

共计 18698 个字符,预计需要花费 47 分钟才能阅读完成。

本文次要对以下技术要点进行剖析:

  1. 编辑器
  2. 自定义组件
  3. 拖拽
  4. 删除组件、调整图层层级
  5. 放大放大
  6. 吊销、重做
  7. 组件属性设置
  8. 吸附
  9. 预览、保留代码
  10. 绑定事件
  11. 绑定动画
  12. 导入 PSD
  13. 手机模式

为了让本文更加容易了解,我将以上技术要点联合在一起写了一个可视化拖拽组件库 DEMO:

  • github 我的项目地址
  • 在线预览

倡议联合源码一起浏览,成果更好(这个 DEMO 应用的是 Vue 技术栈)。

1. 编辑器

先来看一下页面的整体构造。

这一节要讲的编辑器其实就是两头的画布。它的作用是:当从右边组件列表拖拽出一个组件放到画布中时,画布要把这个组件渲染进去。

这个编辑器的实现思路是:

  1. 用一个数组 componentData 保护编辑器中的数据。
  2. 把组件拖拽到画布中时,应用 push() 办法将新的组件数据增加到 componentData
  3. 编辑器应用 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'},则 <component :is="item.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 属性。另外,在将组件列表中的组件拖拽到画布中,还有两个事件是起到关键作用的:

  1. dragstart 事件,在拖拽刚开始时触发。它次要用于将拖拽的组件信息传递给画布。
  2. 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。除了这一点外,还要通过监听三个事件来进行挪动:

  1. mousedown 事件,在组件上按下鼠标时,记录组件以后的地位,即 xy 坐标(为了不便解说,这里应用的坐标轴,实际上 xy 对应的是 css 中的 lefttop
  2. mousemove 事件,每次鼠标挪动时,都用以后最新的 xy 坐标减去最开始的 xy 坐标,从而计算出挪动间隔,再扭转组件地位。
  3. 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)
}

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)

5. 放大放大

仔细的网友可能会发现,点击画布上的组件时,组件上会呈现 8 个小圆点。这 8 个小圆点就是用来放大放大用的。实现原理如下:

1. 在每个组件外面包一层 Shape 组件,Shape 组件里蕴含 8 个小圆点和一个 <slot> 插槽,用于搁置组件。

<!-- 页面组件列表展现 -->
<Shape v-for="(item, index) in componentData"
    :defaultStyle="item.style"
    :style="getShapeStyle(item.style, index)"
    :key="item.id"
    :active="item === curComponent"
    :element="item"
    :zIndex="index"
>
    <component
        class="component"
        :is="item.component"
        :style="getComponentStyle(item.style)"
        :propValue="item.propValue"
    />
</Shape>

Shape 组件内部结构:

<template>
    <div class="shape" :class="{active: this.active}" @click="selectCurComponent" @mousedown="handleMouseDown"
    @contextmenu="handleContextMenu">
        <div
            class="shape-point"
            v-for="(item, index) in (active? pointList : [])"
            @mousedown="handleMouseDownOnPoint(item)"
            :key="index"
            :style="getPointStyle(item)">
        </div>
        <slot></slot>
    </div>
</template>

2. 点击组件时,将 8 个小圆点显示进去。

起作用的是这行代码 :active="item === curComponent"

3. 计算每个小圆点的地位。

先来看一下计算小圆点地位的代码:

const pointList = ['t', 'r', 'b', 'l', 'lt', 'rt', 'lb', 'rb']

getPointStyle(point) {const { width, height} = this.defaultStyle
    const hasT = /t/.test(point)
    const hasB = /b/.test(point)
    const hasL = /l/.test(point)
    const hasR = /r/.test(point)
    let newLeft = 0
    let newTop = 0

    // 四个角的点
    if (point.length === 2) {
        newLeft = hasL? 0 : width
        newTop = hasT? 0 : height
    } else {
        // 高低两点的点,宽度居中
        if (hasT || hasB) {
            newLeft = width / 2
            newTop = hasT? 0 : height
        }

        // 左右两边的点,高度居中
        if (hasL || hasR) {
            newLeft = hasL? 0 : width
            newTop = Math.floor(height / 2)
        }
    }

    const style = {
        marginLeft: hasR? '-4px' : '-3px',
        marginTop: '-3px',
        left: `${newLeft}px`,
        top: `${newTop}px`,
        cursor: point.split('').reverse().map(m => this.directionKey[m]).join('') + '-resize',
    }

    return style
}

计算小圆点的地位须要获取一些信息:

  • 组件的高度 height、宽度 width

留神,小圆点也是相对定位的,绝对于 Shape 组件。所以有四个小圆点的地位很好确定:

  1. 左上角的小圆点,坐标 left: 0, top: 0
  2. 右上角的小圆点,坐标 left: width, top: 0
  3. 左下角的小圆点,坐标 left: 0, top: height
  4. 右下角的小圆点,坐标 left: width, top: height

另外的四个小圆点须要通过计算间接算进去。例如右边两头的小圆点,计算公式为 left: 0, top: height / 2,其余小圆点同理。

4. 点击小圆点时,能够进行放大放大操作。

handleMouseDownOnPoint(point) {
    const downEvent = window.event
    downEvent.stopPropagation()
    downEvent.preventDefault()

    const pos = {...this.defaultStyle}
    const height = Number(pos.height)
    const width = Number(pos.width)
    const top = Number(pos.top)
    const left = Number(pos.left)
    const startX = downEvent.clientX
    const startY = downEvent.clientY

    // 是否须要保留快照
    let needSave = false
    const move = (moveEvent) => {
        needSave = true
        const currX = moveEvent.clientX
        const currY = moveEvent.clientY
        const disY = currY - startY
        const disX = currX - startX
        const hasT = /t/.test(point)
        const hasB = /b/.test(point)
        const hasL = /l/.test(point)
        const hasR = /r/.test(point)
        const newHeight = height + (hasT? -disY : hasB? disY : 0)
        const newWidth = width + (hasL? -disX : hasR? disX : 0)
        pos.height = newHeight > 0? newHeight : 0
        pos.width = newWidth > 0? newWidth : 0
        pos.left = left + (hasL? disX : 0)
        pos.top = top + (hasT? disY : 0)
        this.$store.commit('setShapeStyle', pos)
    }

    const up = () => {document.removeEventListener('mousemove', move)
        document.removeEventListener('mouseup', up)
        needSave && this.$store.commit('recordSnapshot')
    }

    document.addEventListener('mousemove', move)
    document.addEventListener('mouseup', up)
}

它的原理是这样的:

  1. 点击小圆点时,记录点击的坐标 xy。
  2. 假如咱们当初向下拖动,那么 y 坐标就会增大。
  3. 用新的 y 坐标减去原来的 y 坐标,就能够晓得在纵轴方向的挪动间隔是多少。
  4. 最初再将挪动间隔加上原来组件的高度,就能够得出新的组件高度。
  5. 如果是负数,阐明是往下拉,组件的高度在减少。如果是正数,阐明是往上拉,组件的高度在缩小。

6. 吊销、重做

撤销重做的实现原理其实挺简略的,先看一下代码:

snapshotData: [], // 编辑器快照数据
snapshotIndex: -1, // 快照索引
        
undo(state) {if (state.snapshotIndex >= 0) {
        state.snapshotIndex--
        store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
    }
},

redo(state) {if (state.snapshotIndex < state.snapshotData.length - 1) {
        state.snapshotIndex++
        store.commit('setComponentData', deepCopy(state.snapshotData[state.snapshotIndex]))
    }
},

setComponentData(state, componentData = []) {Vue.set(state, 'componentData', componentData)
},

recordSnapshot(state) {
    // 增加新的快照
    state.snapshotData[++state.snapshotIndex] = deepCopy(state.componentData)
    // 在 undo 过程中,增加新的快照时,要将它前面的快照清理掉
    if (state.snapshotIndex < state.snapshotData.length - 1) {state.snapshotData = state.snapshotData.slice(0, state.snapshotIndex + 1)
    }
},

用一个数组来保留编辑器的快照数据。保留快照就是不停地执行 push() 操作,将以后的编辑器数据推入 snapshotData 数组,并减少快照索引 snapshotIndex。目前以下几个动作会触发保留快照操作:

  • 新增组件
  • 删除组件
  • 扭转图层层级
  • 拖动组件完结时

撤销

假如当初 snapshotData 保留了 4 个快照。即 [a, b, c, d],对应的快照索引为 3。如果这时进行了撤销操作,咱们须要将快照索引减 1,而后将对应的快照数据赋值给画布。

例如以后画布数据是 d,进行撤销后,索引 -1,当初画布的数据是 c。

重做

明确了撤销,那重做就很好了解了,就是将快照索引加 1,而后将对应的快照数据赋值给画布。

不过还有一点要留神,就是在撤销操作中进行了新的操作,要怎么办呢?有两种解决方案:

  1. 新操作替换以后快照索引前面所有的数据。还是用方才的数据 [a, b, c, d] 举例,假如当初进行了两次撤销操作,快照索引变为 1,对应的快照数据为 b,如果这时进行了新的操作,对应的快照数据为 e。那 e 会把 cd 顶掉,当初的快照数据为 [a, b, e]
  2. 不顶掉数据,在原来的快照中新增一条记录。用方才的例子举例,e 不会把 cd 顶掉,而是在 cd 之前插入,即快照数据变为 [a, b, e, c, d]

我采纳的是第一种计划。

7. 吸附

什么是吸附?就是在拖拽组件时,如果它和另一个组件的间隔比拟靠近,就会主动吸附在一起。

吸附的代码大略在 300 行左右,倡议本人关上源码文件看(文件门路:src\\components\\Editor\\MarkLine.vue)。这里不贴代码了,次要说说原理是怎么实现的。

标线

在页面上创立 6 条线,别离是三横三竖。这 6 条线的作用是对齐,它们什么时候会呈现呢?

  1. 高低方向的两个组件右边、两头、左边对齐时会呈现竖线
  2. 左右方向的两个组件上边、两头、下边对齐时会呈现横线

具体的计算公式次要是依据每个组件的 xy 坐标和宽度高度进行计算的。例如要判断 ab 两个组件的右边是否对齐,则要晓得它们每个组件的 x 坐标;如果要晓得它们左边是否对齐,除了要晓得 x 坐标,还要晓得它们各自的宽度。

// 左对齐的条件
a.x == b.x

// 右对齐的条件
a.x + a.width == b.x + b.width

在对齐的时候,显示标线。

另外还要判断 ab 两个组件是否“足够”近。如果足够近,就吸附在一起。是否足够近要靠一个变量来判断:

diff: 3, // 相距 dff 像素将主动吸附 

小于等于 diff 像素则主动吸附。

吸附

吸附成果是怎么实现的呢?

假如当初有 ab 组件,a 组件坐标 xy 都是 0,宽高都是 100。当初假如 a 组件不动,咱们正在拖拽 b 组件。当把 b 组件拖到坐标为 x: 0, y: 103 时,因为 103 - 100 <= 3(diff),所以能够断定它们曾经靠近得足够近。这时须要手动将 b 组件的 y 坐标值设为 100,这样就将 ab 组件吸附在一起了。

优化

在拖拽时如果 6 条标线都显示进去会不太好看。所以咱们能够做一下优化,在纵横方向上最多只同时显示一条线。实现原理如下:

  1. a 组件在右边不动,咱们拖着 b 组件往 a 组件凑近。
  2. 这时它们最先对齐的是 a 的左边和 b 的右边,所以只须要一条线就够了。
  3. 如果 ab 组件曾经凑近,并且 b 组件持续往左边挪动,这时就要判断它们俩的两头是否对齐。
  4. b 组件持续拖动,这时须要判断 a 组件的右边和 b 组件的左边是否对齐,也是只须要一条线。

能够发现,要害的中央是咱们要晓得两个组件的方向。即 ab 两个组件凑近,咱们要晓得到底 b 是在 a 的右边还是左边。

这一点能够通过鼠标挪动事件来判断,之前在解说拖拽的时候说过,mousedown 事件触发时会记录终点坐标。所以每次触发 mousemove 事件时,用以后坐标减去原来的坐标,就能够判断组件方向。例如 x 方向上,如果 b.x - a.x 的差值为正,阐明是 b 在 a 左边,否则为右边。

// 触发元素挪动事件,用于显示标线、吸附性能
// 前面两个参数代表鼠标挪动方向
// currY - startY > 0 true 示意向下挪动 false 示意向上挪动
// currX - startX > 0 true 示意向右挪动 false 示意向左挪动
eventBus.$emit('move', this.$el, currY - startY > 0, currX - startX > 0)

8. 组件属性设置

每个组件都有一些通用属性和独有的属性,咱们须要提供一个能显示和批改属性的中央。

// 每个组件数据大略是这样
{
    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: '',
    },
}

我定义了一个 AttrList 组件,用于显示每个组件的属性。

<template>
    <div class="attr-list">
        <el-form>
            <el-form-item v-for="(key, index) in styleKeys" :key="index" :label="map[key]">
                <el-color-picker v-if="key =='borderColor'"v-model="curComponent.style[key]"></el-color-picker>
                <el-color-picker v-else-if="key =='color'"v-model="curComponent.style[key]"></el-color-picker>
                <el-color-picker v-else-if="key =='backgroundColor'"v-model="curComponent.style[key]"></el-color-picker>
                <el-select v-else-if="key =='textAlign'"v-model="curComponent.style[key]">
                    <el-option
                        v-for="item in options"
                        :key="item.value"
                        :label="item.label"
                        :value="item.value"
                    ></el-option>
                </el-select>
                <el-input type="number" v-else v-model="curComponent.style[key]" />
            </el-form-item>
            <el-form-item label="内容" v-if="curComponent && curComponent.propValue && !excludes.includes(curComponent.component)">
                <el-input type="textarea" v-model="curComponent.propValue" />
            </el-form-item>
        </el-form>
    </div>
</template>

代码逻辑很简略,就是遍历组件的 style 对象,将每一个属性遍历进去。并且须要依据具体的属性用不同的组件显示进去,例如色彩属性,须要用色彩选择器显示;数值类的属性须要用 type=number 的 input 组件显示等等。

为了不便用户批改属性值,我应用 v-model 将组件和值绑定在一起。

9. 预览、保留代码

预览和编辑的渲染原理是一样的,区别是不须要编辑性能。所以只须要将原先渲染组件的代码略微改一下就能够了。

<!-- 页面组件列表展现 -->
<Shape v-for="(item, index) in componentData"
    :defaultStyle="item.style"
    :style="getShapeStyle(item.style, index)"
    :key="item.id"
    :active="item === curComponent"
    :element="item"
    :zIndex="index"
>
    <component
        class="component"
        :is="item.component"
        :style="getComponentStyle(item.style)"
        :propValue="item.propValue"
    />
</Shape>

通过方才的介绍,咱们晓得 Shape 组件具备了拖拽、放大放大的性能。当初只须要将 Shape 组件去掉,里面改成套一个一般的 DIV 就能够了(其实不必这个 DIV 也行,但为了绑定事件这个性能,所以须要加上)。

<!-- 页面组件列表展现 -->
<div v-for="(item, index) in componentData" :key="item.id">
    <component
        class="component"
        :is="item.component"
        :style="getComponentStyle(item.style)"
        :propValue="item.propValue"
    />
</div>

保留代码的性能也特地简略,只须要保留画布上的数据 componentData 即可。保留有两种抉择:

  1. 保留到服务器
  2. 本地保留

在 DEMO 上我应用的 localStorage 保留在本地。

10. 绑定事件

每个组件有一个 events 对象,用于存储绑定的事件。目前我只定义了两个事件:

  • alert 事件
  • redirect 事件
// 编辑器自定义事件
const events = {redirect(url) {if (url) {window.location.href = url}
    },

    alert(msg) {if (msg) {alert(msg)
        }
    },
}

const mixins = {methods: events,}

const eventList = [
    {
        key: 'redirect',
        label: '跳转事件',
        event: events.redirect,
        param: '',
    },
    {
        key: 'alert',
        label: 'alert 事件',
        event: events.alert,
        param: '',
    },
]

export {
    mixins,
    events,
    eventList,
}

不过不能在编辑的时候触发,能够在预览的时候触发。

增加事件

通过 v-for 指令将事件列表渲染进去:

<el-tabs v-model="eventActiveName">
    <el-tab-pane v-for="item in eventList" :key="item.key" :label="item.label" :name="item.key" style="padding: 0 20px">
        <el-input v-if="item.key =='redirect'"v-model="item.param"type="textarea"placeholder=" 请输出残缺的 URL" />
        <el-input v-if="item.key =='alert'"v-model="item.param"type="textarea"placeholder=" 请输出要 alert 的内容 " />
        <el-button style="margin-top: 20px;" @click="addEvent(item.key, item.param)"> 确定 </el-button>
    </el-tab-pane>
</el-tabs>

选中事件时将事件增加到组件的 events 对象。

触发事件

预览或真正渲染页面时,也须要在每个组件里面套一层 DIV,这样就能够在 DIV 上绑定一个点击事件,点击时触发咱们方才增加的事件。

<template>
    <div @click="handleClick">
        <component
            class="conponent"
            :is="config.component"
            :style="getStyle(config.style)"
            :propValue="config.propValue"
        />
    </div>
</template>
handleClick() {
    const events = this.config.events
    // 循环触发绑定的事件
    Object.keys(events).forEach(event => {this[event](events[event])
    })
}

11. 绑定动画

动画和事件的原理是一样的,先将所有的动画通过 v-for 指令渲染进去,而后点击动画将对应的动画增加到组件的 animations 数组里。同事件一样,执行的时候也是遍历组件所有的动画并执行。

为了不便,咱们应用了 animate.css 动画库。

// main.js
import '@/styles/animate.css'

当初咱们提前定义好所有的动画数据:

export default [
    {
        label: '进入',
        children: [{ label: '渐显', value: 'fadeIn'},
            {label: '向右进入', value: 'fadeInLeft'},
            {label: '向左进入', value: 'fadeInRight'},
            {label: '向上进入', value: 'fadeInUp'},
            {label: '向下进入', value: 'fadeInDown'},
            {label: '向右长距进入', value: 'fadeInLeftBig'},
            {label: '向左长距进入', value: 'fadeInRightBig'},
            {label: '向上长距进入', value: 'fadeInUpBig'},
            {label: '向下长距进入', value: 'fadeInDownBig'},
            {label: '旋转进入', value: 'rotateIn'},
            {label: '左顺时针旋转', value: 'rotateInDownLeft'},
            {label: '右逆时针旋转', value: 'rotateInDownRight'},
            {label: '左逆时针旋转', value: 'rotateInUpLeft'},
            {label: '右逆时针旋转', value: 'rotateInUpRight'},
            {label: '弹入', value: 'bounceIn'},
            {label: '向右弹入', value: 'bounceInLeft'},
            {label: '向左弹入', value: 'bounceInRight'},
            {label: '向上弹入', value: 'bounceInUp'},
            {label: '向下弹入', value: 'bounceInDown'},
            {label: '光速从右进入', value: 'lightSpeedInRight'},
            {label: '光速从左进入', value: 'lightSpeedInLeft'},
            {label: '光速从右退出', value: 'lightSpeedOutRight'},
            {label: '光速从左退出', value: 'lightSpeedOutLeft'},
            {label: 'Y 轴旋转', value: 'flip'},
            {label: '核心 X 轴旋转', value: 'flipInX'},
            {label: '核心 Y 轴旋转', value: 'flipInY'},
            {label: '左长半径旋转', value: 'rollIn'},
            {label: '由小变大进入', value: 'zoomIn'},
            {label: '左变大进入', value: 'zoomInLeft'},
            {label: '右变大进入', value: 'zoomInRight'},
            {label: '向上变大进入', value: 'zoomInUp'},
            {label: '向下变大进入', value: 'zoomInDown'},
            {label: '向右滑动开展', value: 'slideInLeft'},
            {label: '向左滑动开展', value: 'slideInRight'},
            {label: '向上滑动开展', value: 'slideInUp'},
            {label: '向下滑动开展', value: 'slideInDown'},
        ],
    },
    {
        label: '强调',
        children: [{ label: '弹跳', value: 'bounce'},
            {label: '闪动', value: 'flash'},
            {label: '放大放大', value: 'pulse'},
            {label: '放大放大弹簧', value: 'rubberBand'},
            {label: '左右晃动', value: 'headShake'},
            {label: '左右扇形摇晃', value: 'swing'},
            {label: '放大晃动放大', value: 'tada'},
            {label: '扇形摇晃', value: 'wobble'},
            {label: '左右高低晃动', value: 'jello'},
            {label: 'Y 轴旋转', value: 'flip'},
        ],
    },
    {
        label: '退出',
        children: [{ label: '渐隐', value: 'fadeOut'},
            {label: '向左退出', value: 'fadeOutLeft'},
            {label: '向右退出', value: 'fadeOutRight'},
            {label: '向上退出', value: 'fadeOutUp'},
            {label: '向下退出', value: 'fadeOutDown'},
            {label: '向左长距退出', value: 'fadeOutLeftBig'},
            {label: '向右长距退出', value: 'fadeOutRightBig'},
            {label: '向上长距退出', value: 'fadeOutUpBig'},
            {label: '向下长距退出', value: 'fadeOutDownBig'},
            {label: '旋转退出', value: 'rotateOut'},
            {label: '左顺时针旋转', value: 'rotateOutDownLeft'},
            {label: '右逆时针旋转', value: 'rotateOutDownRight'},
            {label: '左逆时针旋转', value: 'rotateOutUpLeft'},
            {label: '右逆时针旋转', value: 'rotateOutUpRight'},
            {label: '弹出', value: 'bounceOut'},
            {label: '向左弹出', value: 'bounceOutLeft'},
            {label: '向右弹出', value: 'bounceOutRight'},
            {label: '向上弹出', value: 'bounceOutUp'},
            {label: '向下弹出', value: 'bounceOutDown'},
            {label: '核心 X 轴旋转', value: 'flipOutX'},
            {label: '核心 Y 轴旋转', value: 'flipOutY'},
            {label: '左长半径旋转', value: 'rollOut'},
            {label: '由小变大退出', value: 'zoomOut'},
            {label: '左变大退出', value: 'zoomOutLeft'},
            {label: '右变大退出', value: 'zoomOutRight'},
            {label: '向上变大退出', value: 'zoomOutUp'},
            {label: '向下变大退出', value: 'zoomOutDown'},
            {label: '向左滑动收起', value: 'slideOutLeft'},
            {label: '向右滑动收起', value: 'slideOutRight'},
            {label: '向上滑动收起', value: 'slideOutUp'},
            {label: '向下滑动收起', value: 'slideOutDown'},
        ],
    },
]

而后用 v-for 指令渲染进去动画列表。

增加动画

<el-tabs v-model="animationActiveName">
    <el-tab-pane v-for="item in animationClassData" :key="item.label" :label="item.label" :name="item.label">
        <el-scrollbar class="animate-container">
            <div
                class="animate"
                v-for="(animate, index) in item.children"
                :key="index"
                @mouseover="hoverPreviewAnimate = animate.value"
                @click="addAnimation(animate)"
            >
                <div :class="[hoverPreviewAnimate === animate.value && animate.value +' animated']">
                    {{animate.label}}
                </div>
            </div>
        </el-scrollbar>
    </el-tab-pane>
</el-tabs>

点击动画将调用 addAnimation(animate) 将动画增加到组件的 animations 数组。

触发动画

运行动画的代码:

export default async function runAnimation($el, animations = []) {const play = (animation) => new Promise(resolve => {$el.classList.add(animation.value, 'animated')
        const removeAnimation = () => {$el.removeEventListener('animationend', removeAnimation)
            $el.removeEventListener('animationcancel', removeAnimation)
            $el.classList.remove(animation.value, 'animated')
            resolve()}
            
        $el.addEventListener('animationend', removeAnimation)
        $el.addEventListener('animationcancel', removeAnimation)
    })

    for (let i = 0, len = animations.length; i < len; i++) {await play(animations[i])
    }
}

运行动画须要两个参数:组件对应的 DOM 元素(在组件应用 this.$el 获取)和它的动画数据 animations。并且须要监听 animationend 事件和 animationcancel 事件:一个是动画完结时触发,一个是动画意外终止时触发。

利用这一点再配合 Promise 一起应用,就能够一一运行组件的每个动画了。

12. 导入 PSD

因为工夫关系,这个性能我还没做。当初简略的形容一下怎么做这个性能。那就是应用 psd.js 库,它能够解析 PSD 文件。

应用 psd 库解析 PSD 文件得出的数据如下:

{ children: 
   [ { type: 'group',
       visible: false,
       opacity: 1,
       blendingMode: 'normal',
       name: 'Version D',
       left: 0,
       right: 900,
       top: 0,
       bottom: 600,
       height: 600,
       width: 900,
       children: 
        [ { type: 'layer',
            visible: true,
            opacity: 1,
            blendingMode: 'normal',
            name: 'Make a change and save.',
            left: 275,
            right: 636,
            top: 435,
            bottom: 466,
            height: 31,
            width: 361,
            mask: {},
            text: 
             { value: 'Make a change and save.',
               font: 
                { name: 'HelveticaNeue-Light',
                  sizes: [33],
                  colors: [[ 85, 96, 110, 255] ],
                  alignment: ['center'] },
               left: 0,
               top: 0,
               right: 0,
               bottom: 0,
               transform: {xx: 1, xy: 0, yx: 0, yy: 1, tx: 456, ty: 459} },
            image: {}} ] } ],
    document: 
       { width: 900,
         height: 600,
         resources: 
          { layerComps: 
             [{ id: 692243163, name: 'Version A', capturedInfo: 1},
               {id: 725235304, name: 'Version B', capturedInfo: 1},
               {id: 730932877, name: 'Version C', capturedInfo: 1} ],
            guides: [],
            slices: []} } }

从以上代码能够发现,这些数据和 css 十分像。依据这一点,只须要写一个转换函数,将这些数据转换成咱们组件所需的数据,就能实现 PSD 文件转成渲染组件的性能。目前 quark-h5 和 luban-h5 都是这样实现的 PSD 转换性能。

13. 手机模式

因为画布是能够调整大小的,咱们能够应用 iphone6 的分辨率来开发手机页面。

这样开发进去的页面也能够在手机下失常浏览,但可能会有款式偏差。因为我自定义的三个组件是没有做适配的,如果你须要开发手机页面,那自定义组件必须应用挪动端的 UI 组件库。或者本人开发挪动端专用的自定义组件。

总结

因为 DEMO 的代码比拟多,所以在解说每一个性能点时,我只把要害代码贴上来。所以大家会发现 DEMO 的源码和我贴上来的代码会有些区别,请不用在意。

另外,DEMO 的款式也比拟简陋,次要是最近事件比拟多,没太多工夫写难看点,请见谅。

参考资料

  • ref-line
  • quark-h5
  • luban-h5
  • 易企秀
  • drag 事件
正文完
 0