乐趣区

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

本文是可视化拖拽系列的第四篇,比起之前的三篇文章,这篇性能点要略微少一点,总共有五点:

  1. SVG 组件
  2. 动静属性面板
  3. 数据起源(接口申请)
  4. 组件联动
  5. 组件按需加载

如果你对我之前的系列文章不是很理解,倡议先把这三篇文章看一遍,再来浏览本文(否则没有上下文,不太好了解):

  • 可视化拖拽组件库一些技术要点原理剖析
  • 可视化拖拽组件库一些技术要点原理剖析(二)
  • 可视化拖拽组件库一些技术要点原理剖析(三)

另附上我的项目、在线 DEMO 地址:

  • 一个低代码(可视化拖拽)教学我的项目
  • 在线 DEMO

SVG 组件

目前我的项目里提供的自定义组件都是反对自在放大放大的,不过他们有一个共同点——都是规定形态。也就是说对它们放大放大,间接扭转宽高就能够实现了,无需做其余解决。然而不规则形态就不一样了,譬如一个五角星,你得思考放大放大时,如何成比例的扭转尺寸。最终,我采纳了 svg 的计划来实现(还思考过用 iconfont 来实现,不过有缺点,放弃了),上面让咱们来看看具体的实现细节。

用 SVG 画一个五角星

假如咱们须要画一个 100 * 100 的五角星,它的代码是这样的:

<svg 
    version="1.1" 
    baseProfile="full" 
    xmlns="http://www.w3.org/2000/svg"
>
    <polygon 
        points="50 0,62.5 37.5,100 37.5,75 62.5,87.5 100,50 75,12.5 100,25 62.5,0 37.5,37.5 37.5" 
        stroke="#000" 
        fill="rgba(255, 255, 255, 1)" 
        stroke-width="1"
    ></polygon>
</svg>

svg 上的版本、命名空间之类的属性不是很重要,能够先疏忽。重点是 polygon 这个元素,它在 svg 中定义了一个由 一组首尾相连的直线线段形成的闭合多边形形态 ,最初一点连贯到第一点。也就是说这个多边形由一系列坐标点组成,相连的点之间会主动连上。polygon 的 points 属性用来示意多边形的一系列坐标点,每个坐标点由 x y 坐标组成,每个坐标点之间用 , 逗号分隔。

上图就是一个用 svg 画的五角星,它由十个坐标点组成 50 0,62.5 37.5,100 37.5,75 62.5,87.5 100,50 75,12.5 100,25 62.5,0 37.5,37.5 37.5。因为这是一个 100*100 的五角星,所以咱们可能很容易的依据每个坐标点的数值算出它们在五角星(坐标系)中所占的比例。譬如第一个点是 p1(50,0),那么它的 x y 坐标比例是 50%, 0;第二个点 p2(62.5,37.5),对应的比例是 62.5%, 37.5%

// 五角星十个坐标点的比例汇合
const points = [[0.5, 0],
    [0.625, 0.375],
    [1, 0.375],
    [0.75, 0.625],
    [0.875, 1],
    [0.5, 0.75],
    [0.125, 1],
    [0.25, 0.625],
    [0, 0.375],
    [0.375, 0.375],
]

既然晓得了五角星的比例,那么要画出其余尺寸的五角星也就大海捞针了。咱们只须要在每次对五角星进行放大放大,扭转它的尺寸时,等比例的给出每个坐标点的具体数值即要。

<div class="svg-star-container">
    <svg
        version="1.1"
        baseProfile="full"
        xmlns="http://www.w3.org/2000/svg"
    >
        <polygon
            ref="star"
            :points="points"
            :stroke="element.style.borderColor"
            :fill="element.style.backgroundColor"
            stroke-width="1"
        />
    </svg>
    <v-text :prop-value="element.propValue" :element="element" />
</div>

<script>
function drawPolygon(width, height) {
    // 五角星十个坐标点的比例汇合
    const points = [[0.5, 0],
        [0.625, 0.375],
        [1, 0.375],
        [0.75, 0.625],
        [0.875, 1],
        [0.5, 0.75],
        [0.125, 1],
        [0.25, 0.625],
        [0, 0.375],
        [0.375, 0.375],
    ]

    const coordinatePoints = points.map(point => width * point[0] + ' ' + height * point[1])
    this.points = coordinatePoints.toString() // 得出五角星的 points 属性数据}
</script>

其余 SVG 组件

同理,要画其余类型的 svg 组件,咱们只有晓得它们坐标点所占的比例就能够了。如果你不晓得一个 svg 怎么画,能够网上搜一下,先找一个能用的 svg 代码(这个五角星的 svg 代码,就是在网上找的)。而后再计算它们每个坐标点所占的比例,转成小数点的模式,最初把这些数据代入下面提供的 drawPolygon() 函数即可。譬如画一个三角形的代码是这样的:

function drawTriangle(width, height) {
    const points = [[0.5, 0.05],
        [1, 0.95],
        [0, 0.95],
    ]

    const coordinatePoints = points.map(point => width * point[0] + ' ' + height * point[1])
    this.points = coordinatePoints.toString() // 得出三角形的 points 属性数据}

动静属性面板

目前所有自定义组件的属性面板都共用同一个 AttrList 组件。因而弊病很显著,须要在这里写很多 if 语句,因为不同的组件有不同的属性。例如矩形组件有 content 属性,然而图片没有,一个不同的属性就得写一个 if 语句。

<el-form-item v-if="name ==='rectShape'"label=" 内容 ">
   <el-input />
</el-form-item>
<!-- 其余属性... -->

幸好,这个问题的解决方案也不难。在本系列的第一篇文章中,有解说过如何动静渲染自定义组件:

<component :is="item.component"></component> <!-- 动静渲染组件 -->

在每个自定义组件的数据结构中都有一个 component 属性,这是该组件在 Vue 中注册的名称。因而,每个自定义组件的属性面板能够和组件自身一样(利用 component 属性),做成动静的:

<!-- 右侧属性列表 -->
<section class="right">
    <el-tabs v-if="curComponent" v-model="activeName">
        <el-tab-pane label="属性" name="attr">
            <component :is="curComponent.component +'Attr'" /> <!-- 动静渲染属性面板 -->
        </el-tab-pane>
        <el-tab-pane label="动画" name="animation" style="padding-top: 20px;">
            <AnimationList />
        </el-tab-pane>
        <el-tab-pane label="事件" name="events" style="padding-top: 20px;">
            <EventList />
        </el-tab-pane>
    </el-tabs>
    <CanvasAttr v-else></CanvasAttr>
</section>

同时,自定义组件的目录构造也须要做下调整,原来的目录构造为:

- VText.vue
- Picture.vue
...

调整后变为:

- VText
    - Attr.vue <!-- 组件的属性面板 -->
    - Component.vue <!-- 组件自身 -->
- Picture
    - Attr.vue
    - Component.vue

当初每一个组件都蕴含了组件自身和它的属性面板。通过革新后,图片属性面板代码也更加精简了:

<template>
    <div class="attr-list">
        <CommonAttr></CommonAttr> <!-- 通用属性 -->
        <el-form>
            <el-form-item label="镜像翻转">
                <div style="clear: both;">
                    <el-checkbox v-model="curComponent.propValue.flip.horizontal" label="horizontal"> 程度翻转 </el-checkbox>
                    <el-checkbox v-model="curComponent.propValue.flip.vertical" label="vertical"> 垂直翻转 </el-checkbox>
                </div>
            </el-form-item>
        </el-form>
    </div>
</template>

这样一来,组件和对应的属性面板都变成动静的了。当前须要独自给某个自定义组件增加属性就十分不便了。

数据起源(接口申请)

有些组件会有动静加载数据的需要,所以顺便加了一个 Request 公共属性组件,用于申请数据。当一个自定义组件领有 request 属性时,就会在属性面板上渲染接口申请的相干内容。至此,属性面板的公共组件曾经有两个了:

-common
    - Request.vue <!-- 接口申请 -->
    - CommonAttr.vue <!-- 通用款式 -->
// VText 自定义组件的数据结构
{
    component: 'VText',
    label: '文字',
    propValue: '双击编辑文字',
    icon: 'wenben',
    request: { // 接口申请
        method: 'GET',
        data: [],
        url: '',
        series: false, // 是否定时发送申请
        time: 1000, // 定时更新工夫
        paramType: '', // string object array
        requestCount: 0, // 申请次数限度,0 为有限
    },
    style: { // 通用款式
        width: 200,
        height: 28,
        fontSize: '',
        fontWeight: 400,
        lineHeight: '',
        letterSpacing: 0,
        textAlign: '',
        color: '',
    },
}

从下面的动图能够看出,api 申请的办法参数等都是能够手动批改的。然而怎么管制返回来的数据赋值给组件的某个属性呢?这能够在发出请求的时候把组件的整个数据对象 obj 以及要批改属性的 key 当成参数一起传进去,当数据返回来时,就能够间接应用 obj[key] = data 来批改数据了。

// 第二个参数是要批改数据的父对象,第三个参数是批改数据的 key,第四个数据批改数据的类型
this.cancelRequest = request(this.request, this.element, 'propValue', 'string')

组件联动

组件联动:当一个组件触发事件时,另一个组件会收到告诉,并且做出相应的操作。

下面这个动图的矩形,它别离监听了上面两个按钮的悬浮事件,第一个按钮触发悬浮并播送事件,矩形执行回调向右旋转挪动;第二个按钮则相同,向左旋转挪动。

要实现这个性能,首先要给自定义组件加一个新属性 linkage,用来记录所有要联动的组件:

{
    // 组件的其余属性...
    linkage: {
         duration: 0, // 过渡持续时间
         data: [ // 组件联动
             {
                 id: '', // 联动的组件 id
                 label: '', // 联动的组件名称
                 event: '', // 监听事件
                 style: [{key: '', value:''}], // 监听的事件触发时,须要扭转的属性
             },
         ],
     }
}

对应的属性面板为:

组件联动实质上就是订阅 / 公布模式的使用,每个组件在渲染时都会遍历它监听的所有组件。

事件监听

<script>
import eventBus from '@/utils/eventBus'

export default {
    props: {
        linkage: {
            type: Object,
            default: () => {},
        },
        element: {
            type: Object,
            default: () => {},
        },
    },
    created() {if (this.linkage?.data?.length) {eventBus.$on('v-click', this.onClick)
            eventBus.$on('v-hover', this.onHover)
        }
    },
    mounted() {const { data, duration} = this.linkage || {}
        if (data?.length) {this.$el.style.transition = `all ${duration}s`
        }
    },
    beforeDestroy() {if (this.linkage?.data?.length) {eventBus.$off('v-click', this.onClick)
            eventBus.$off('v-hover', this.onHover)
        }
    },
    methods: {changeStyle(data = []) {
            data.forEach(item => {
                item.style.forEach(e => {if (e.key) {this.element.style[e.key] = e.value
                    }
                })
            })
        },

        onClick(componentId) {const data = this.linkage.data.filter(item => item.id === componentId && item.event === 'v-click')
            this.changeStyle(data)
        },

        onHover(componentId) {const data = this.linkage.data.filter(item => item.id === componentId && item.event === 'v-hover')
            this.changeStyle(data)
        },
    },
}
</script>

从上述代码能够看出:

  1. 每一个自定义组件初始化时,都会监听 v-click v-hover 两个事件(目前只有点击、悬浮两个事件)
  2. 事件回调函数触发时会收到一个参数——收回事件的组件 id(譬如多个组件都触发了点击事件,须要依据 id 来判断是否是本人监听的组件)
  3. 最初再批改对应的属性

事件触发

<template>
    <div @click="onClick" @mouseenter="onMouseEnter">
        <component
            :is="config.component"
            ref="component"
            class="component"
            :style="getStyle(config.style)"
            :prop-value="config.propValue"
            :element="config"
            :request="config.request"
            :linkage="config.linkage"
        />
    </div>
</template>

<script>
import eventBus from '@/utils/eventBus'

export default {
    methods: {onClick() {
            const events = this.config.events
            Object.keys(events).forEach(event => {this[event](events[event])
            })

            eventBus.$emit('v-click', this.config.id)
        },

        onMouseEnter() {eventBus.$emit('v-hover', this.config.id)
        },
    },
}
</script>

从上述代码能够看出,在渲染组件时,每一个组件的最外层都监听了 click mouseenter 事件,当这些事件触发时,eventBus 就会触发对应的事件(v-click 或 v-hover),并且把以后的组件 id 作为参数传过来。

最初再捊一遍整体逻辑:

  1. a 组件监听原生事件 click mouseenter
  2. 用户点击或挪动鼠标到组件上触发原生事件 click 或 mouseenter
  3. 事件回调函数再用 eventBus 触发 v-click 或 v-hover 事件
  4. 监听了这两个事件的 b 组件收到告诉后再批改 b 组件的相干属性(例如下面矩形的 x 坐标和旋转角度)

组件按需加载

目前这个我的项目自身是没有做按需加载的,然而我把实现计划用文字的模式写进去其实也差不多。

第一步,抽离

第一步须要把所有的自定义组件出离进去,独自寄存。倡议应用 monorepo 的形式来寄存,所有的组件放在一个仓库里。每一个 package 就是一个组件,能够独自打包。

- node_modules
- packages
    - v-text # 一个组件就是一个包 
    - v-button
    - v-table
- package.json
- lerna.json

第二步,打包

倡议每个组件都打包成一个 js 文件,例如叫 bundle.js。打包好间接调用上传接口放到服务器存起来(公布到 npm 也能够),每个组件都有一个惟一 id。前端每次渲染组件的时,通过这个组件 id 向服务器申请组件资源的 URL。

第三步,动静加载组件

动静加载组件有两种形式:

  1. import()
  2. <script> 标签

第一种形式实现起来比拟不便:

const name = 'v-text' // 组件名称
const component = await import('https://xxx.xxx/bundile.js')
Vue.component(name, component)

然而兼容性上有点小问题,如果要反对一些旧的浏览器(例如 IE),能够应用 <script> 标签的模式来加载:

function loadjs(url) {return new Promise((resolve, reject) => {const script = document.createElement('script')
        script.src = url
        script.onload = resolve
        script.onerror = reject
    })
}

const name = 'v-text' // 组件名称
await loadjs('https://xxx.xxx/bundile.js')
// 这种形式加载组件,会间接将组件挂载在全局变量 window 下,所以 window[name] 取值后就是组件
Vue.component(name, window[name])

为了同时反对这两种加载形式,在加载组件时须要判断一下浏览器是否反对 ES6。如果反对就用第一种形式,如果不反对就用第二种形式:

function isSupportES6() {
    try {new Function('const fn = () => {};')
    } catch (error) {return false}

    return true
}

最初一点,打包也要同时兼容这两种加载形式:

import VText from './VText.vue'

if (typeof window !== 'undefined') {window['VText'] = VText
}

export default VText

同时导出组件和把组件挂在 window 下。

其余小优化

图片镜像翻转

图片镜像翻转须要应用 canvas 来实现,次要应用的是 canvas 的 translate() scale() 两个办法。假如咱们要对一个 100*100 的图片进行程度镜像翻转,它的代码是这样的:

<canvas width="100" height="100"></canvas>

<script>
    const canvas = document.querySelector('canvas')
    const ctx = canvas.getContext('2d')
    const img = document.createElement('img')
    const width = 100
    const height = 100
    img.src = 'https://avatars.githubusercontent.com/u/22117876?v=4'
    img.onload = () => ctx.drawImage(img, 0, 0, width, height)

    // 程度翻转
    setTimeout(() => {
        // 革除图片
        ctx.clearRect(0, 0, width, height)
        // 平移图片
        ctx.translate(width, 0)
        // 对称镜像
        ctx.scale(-1, 1)
        ctx.drawImage(img, 0, 0, width, height)
        // 还原坐标点
        ctx.setTransform(1, 0, 0, 1, 0, 0)
    }, 2000)
</script>

ctx.translate(width, 0) 这行代码的意思是把图片的 x 坐标往前挪动 width 个像素,所以平移后,图片就刚好在画布里面。而后这时应用 ctx.scale(-1, 1) 对图片进行程度翻转,就能失去一个程度翻转后的图片了。

垂直翻转也是一样的原理,只不过参数不一样:

// 原来程度翻转是 ctx.translate(width, 0)
ctx.translate(0, height) 
// 原来程度翻转是 ctx.scale(-1, 1)
ctx.scale(1, -1)

实时组件列表

画布中的每一个组件都是有层级的,然而每个组件的具体层级并不会实时显现出来。因而,就有了这个实时组件列表的性能。

这个性能实现起来并不难,它的原理和画布渲染组件是一样的,只不过这个列表只须要渲染图标和名称。

<div class="real-time-component-list">
    <div
        v-for="(item, index) in componentData"
        :key="index"
        class="list"
        :class="{actived: index === curComponentIndex}"
        @click="onClick(index)"
    >
        <span class="iconfont" :class="'icon-' + getComponent(index).icon"></span>
        <span>{{getComponent(index).label }}</span>
    </div>
</div>

然而有一点要留神,在组件数据的数组里,越靠后的组件层级越高。所以不对数组的数据索引做解决的话,用户看到的场景是这样的(假如增加组件的程序为文本、按钮、图片):

从用户的角度来看,层级最高的图片,在实时列表里排在最初。这跟咱们平时的认知不太一样。所以,咱们须要对组件数据做个 reverse() 翻转一下。譬如文字组件的索引为 0,层级最低,它应该显示在底部。那么每次在实时列表展现时,咱们能够通过上面的代码转换一下,失去翻转后的索引,而后再渲染,这样的排序看起来就比拟难受了。

<div class="real-time-component-list">
    <div
        v-for="(item, index) in componentData"
        :key="index"
        class="list"
        :class="{actived: transformIndex(index) === curComponentIndex }"
        @click="onClick(transformIndex(index))"
    >
        <span class="iconfont" :class="'icon-' + getComponent(index).icon"></span>
        <span>{{getComponent(index).label }}</span>
    </div>
</div>

<script>
function getComponent(index) {return componentData[componentData.length - 1 - index]
}

function transformIndex(index) {return componentData.length - 1 - index}
</script>

通过转换后,层级最高的图片在实时列表里排在最下面,完满!

总结

至此,可视化拖拽系列的第四篇文章曾经完结了,间隔上一篇系列文章的公布工夫(2021 年 02 月 15 日)曾经有一年多了。没想到这个我的项目这么受欢迎,在短短一年的工夫里取得了很多网友的认可。所以心愿本系列的第四篇文章还是能像之前一样,对大家有帮忙,再次感激!

最初,自告奋勇一下本人,自己五年 + 前端,有基础架构和带团队的教训。有没有大佬有北京、天津的前端岗位举荐。如果有,请在评论区留言,或者私信帮忙内推一下,感激!

自己的社交主页:

  • Github
  • 知乎
  • 掘金
退出移动版