可视化拖拽组件库一些技术要点原理分析(四)(下)

简介: 可视化拖拽组件库一些技术要点原理分析(四)(下)

数据来源(接口请求)

有些组件会有动态加载数据的需求,所以特地加了一个 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-clickv-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>

从上述代码可以看出,在渲染组件时,每一个组件的最外层都监听了 clickmouseenter 事件,当这些事件触发时,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. </code> 标签</li></ol><div>第一种方式实现起来比较方便:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22const%20name%20%3D%20'v-text'%20%2F%2F%20%E7%BB%84%E4%BB%B6%E5%90%8D%E7%A7%B0%5Cnconst%20component%20%3D%20await%20import('https%3A%2F%2Fxxx.xxx%2Fbundile.js')%5CnVue.component(name%2C%20component)%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22Ju4rV%22%7D"></div><div>但是兼容性上有点小问题,如果要支持一些旧的浏览器(例如 IE),可以使用 <code><script></code> 标签的形式来加载:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22function%20loadjs(url)%20%7B%5Cn%20%20%20%20return%20new%20Promise((resolve%2C%20reject)%20%3D%3E%20%7B%5Cn%20%20%20%20%20%20%20%20const%20script%20%3D%20document.createElement('script')%5Cn%20%20%20%20%20%20%20%20script.src%20%3D%20url%5Cn%20%20%20%20%20%20%20%20script.onload%20%3D%20resolve%5Cn%20%20%20%20%20%20%20%20script.onerror%20%3D%20reject%5Cn%20%20%20%20%7D)%5Cn%7D%5Cnconst%20name%20%3D%20'v-text'%20%2F%2F%20%E7%BB%84%E4%BB%B6%E5%90%8D%E7%A7%B0%5Cnawait%20loadjs('https%3A%2F%2Fxxx.xxx%2Fbundile.js')%5Cn%2F%2F%20%E8%BF%99%E7%A7%8D%E6%96%B9%E5%BC%8F%E5%8A%A0%E8%BD%BD%E7%BB%84%E4%BB%B6%EF%BC%8C%E4%BC%9A%E7%9B%B4%E6%8E%A5%E5%B0%86%E7%BB%84%E4%BB%B6%E6%8C%82%E8%BD%BD%E5%9C%A8%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F%20window%20%E4%B8%8B%EF%BC%8C%E6%89%80%E4%BB%A5%20window%5Bname%5D%20%E5%8F%96%E5%80%BC%E5%90%8E%E5%B0%B1%E6%98%AF%E7%BB%84%E4%BB%B6%5CnVue.component(name%2C%20window%5Bname%5D)%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22F3XJ1%22%7D"></div><div>为了同时支持这两种加载方式,在加载组件时需要判断一下浏览器是否支持 ES6。如果支持就用第一种方式,如果不支持就用第二种方式:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22function%20isSupportES6()%20%7B%5Cn%20%20%20%20try%20%7B%5Cn%20%20%20%20%20%20%20%20new%20Function('const%20fn%20%3D%20()%20%3D%3E%20%7B%7D%3B')%5Cn%20%20%20%20%7D%20catch%20(error)%20%7B%5Cn%20%20%20%20%20%20%20%20return%20false%5Cn%20%20%20%20%7D%5Cn%20%20%20%20return%20true%5Cn%7D%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22oHMHg%22%7D"></div><div>最后一点,打包也要同时兼容这两种加载方式:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22import%20VText%20from%20'.%2FVText.vue'%5Cnif%20(typeof%20window%20!%3D%3D%20'undefined')%20%7B%5Cn%20%20%20%20window%5B'VText'%5D%20%3D%20VText%5Cn%7D%5Cnexport%20default%20VText%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22ZgCSv%22%7D"></div><div>同时导出组件和把组件挂在 window 下。</div><h2 id="item-6">其他小优化</h2><h3 id="item-6-5">图片镜像翻转</h3><div><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fdxrdxmxsuvtcc_31b53d71565c4a109b607bc958085c14.gif%22%2C%22originWidth%22%3A577%2C%22originHeight%22%3A330%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A577%2C%22height%22%3A330%7D"></span></div><div>图片镜像翻转需要使用 canvas 来实现,主要使用的是 canvas 的 <code>translate()</code> <code>scale()</code> 两个方法。假设我们要对一个 100*100 的图片进行水平镜像翻转,它的代码是这样的:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Ccanvas%20width%3D%5C%22100%5C%22%20height%3D%5C%22100%5C%22%3E%3C%2Fcanvas%3E%5Cn%3Cscript%3E%5Cn%20%20%20%20const%20canvas%20%3D%20document.querySelector('canvas')%5Cn%20%20%20%20const%20ctx%20%3D%20canvas.getContext('2d')%5Cn%20%20%20%20const%20img%20%3D%20document.createElement('img')%5Cn%20%20%20%20const%20width%20%3D%20100%5Cn%20%20%20%20const%20height%20%3D%20100%5Cn%20%20%20%20img.src%20%3D%20'https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F22117876%3Fv%3D4'%5Cn%20%20%20%20img.onload%20%3D%20()%20%3D%3E%20ctx.drawImage(img%2C%200%2C%200%2C%20width%2C%20height)%5Cn%20%20%20%20%2F%2F%20%E6%B0%B4%E5%B9%B3%E7%BF%BB%E8%BD%AC%5Cn%20%20%20%20setTimeout(()%20%3D%3E%20%7B%5Cn%20%20%20%20%20%20%20%20%2F%2F%20%E6%B8%85%E9%99%A4%E5%9B%BE%E7%89%87%5Cn%20%20%20%20%20%20%20%20ctx.clearRect(0%2C%200%2C%20width%2C%20height)%5Cn%20%20%20%20%20%20%20%20%2F%2F%20%E5%B9%B3%E7%A7%BB%E5%9B%BE%E7%89%87%5Cn%20%20%20%20%20%20%20%20ctx.translate(width%2C%200)%5Cn%20%20%20%20%20%20%20%20%2F%2F%20%E5%AF%B9%E7%A7%B0%E9%95%9C%E5%83%8F%5Cn%20%20%20%20%20%20%20%20ctx.scale(-1%2C%201)%5Cn%20%20%20%20%20%20%20%20ctx.drawImage(img%2C%200%2C%200%2C%20width%2C%20height)%5Cn%20%20%20%20%20%20%20%20%2F%2F%20%E8%BF%98%E5%8E%9F%E5%9D%90%E6%A0%87%E7%82%B9%5Cn%20%20%20%20%20%20%20%20ctx.setTransform(1%2C%200%2C%200%2C%201%2C%200%2C%200)%5Cn%20%20%20%20%7D%2C%202000)%5Cn%3C%2Fscript%3E%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22HPfeZ%22%7D"></div><div><code>ctx.translate(width, 0)</code> 这行代码的意思是把图片的 x 坐标往前移动 width 个像素,所以平移后,图片就刚好在画布外面。然后这时使用 <code>ctx.scale(-1, 1)</code> 对图片进行水平翻转,就能得到一个水平翻转后的图片了。</div><div><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fdxrdxmxsuvtcc_4cc8fef9e6a143579568cf1ae5534c7a.gif%22%2C%22originWidth%22%3A164%2C%22originHeight%22%3A108%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A164%2C%22height%22%3A108%7D"></span></div><div>垂直翻转也是一样的原理,只不过参数不一样:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%2F%2F%20%E5%8E%9F%E6%9D%A5%E6%B0%B4%E5%B9%B3%E7%BF%BB%E8%BD%AC%E6%98%AF%20ctx.translate(width%2C%200)%5Cnctx.translate(0%2C%20height)%20%5Cn%2F%2F%20%E5%8E%9F%E6%9D%A5%E6%B0%B4%E5%B9%B3%E7%BF%BB%E8%BD%AC%E6%98%AF%20ctx.scale(-1%2C%201)%5Cnctx.scale(1%2C%20-1)%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22WV1xZ%22%7D"></div><h3 id="item-6-6">实时组件列表</h3><div>画布中的每一个组件都是有层级的,但是每个组件的具体层级并不会实时显现出来。因此,就有了这个实时组件列表的功能。</div><div>这个功能实现起来并不难,它的原理和画布渲染组件是一样的,只不过这个列表只需要渲染图标和名称。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Cdiv%20class%3D%5C%22real-time-component-list%5C%22%3E%5Cn%20%20%20%20%3Cdiv%5Cn%20%20%20%20%20%20%20%20v-for%3D%5C%22(item%2C%20index)%20in%20componentData%5C%22%5Cn%20%20%20%20%20%20%20%20%3Akey%3D%5C%22index%5C%22%5Cn%20%20%20%20%20%20%20%20class%3D%5C%22list%5C%22%5Cn%20%20%20%20%20%20%20%20%3Aclass%3D%5C%22%7B%20actived%3A%20index%20%3D%3D%3D%20curComponentIndex%20%7D%5C%22%5Cn%20%20%20%20%20%20%20%20%40click%3D%5C%22onClick(index)%5C%22%5Cn%20%20%20%20%3E%5Cn%20%20%20%20%20%20%20%20%3Cspan%20class%3D%5C%22iconfont%5C%22%20%3Aclass%3D%5C%22'icon-'%20%2B%20getComponent(index).icon%5C%22%3E%3C%2Fspan%3E%5Cn%20%20%20%20%20%20%20%20%3Cspan%3E%7B%7B%20getComponent(index).label%20%7D%7D%3C%2Fspan%3E%5Cn%20%20%20%20%3C%2Fdiv%3E%5Cn%3C%2Fdiv%3E%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22y1xI8%22%7D"></div><div>但是有一点要注意,在组件数据的数组里,越靠后的组件层级越高。所以不对数组的数据索引做处理的话,用户看到的场景是这样的(<strong>假设添加组件的顺序为文本、按钮、图片</strong>):</div><div><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fdxrdxmxsuvtcc_467efda9ab09447c9b7f150f7ea09766.png%22%2C%22originWidth%22%3A583%2C%22originHeight%22%3A674%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A583%2C%22height%22%3A674%7D"></span></div><div>从用户的角度来看,层级最高的图片,在实时列表里排在最后。这跟我们平时的认知不太一样。所以,我们需要对组件数据做个 <code>reverse()</code> 翻转一下。譬如文字组件的索引为 0,层级最低,它应该显示在底部。那么每次在实时列表展示时,我们可以通过下面的代码转换一下,得到翻转后的索引,然后再渲染,这样的排序看起来就比较舒服了。</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Cdiv%20class%3D%5C%22real-time-component-list%5C%22%3E%5Cn%20%20%20%20%3Cdiv%5Cn%20%20%20%20%20%20%20%20v-for%3D%5C%22(item%2C%20index)%20in%20componentData%5C%22%5Cn%20%20%20%20%20%20%20%20%3Akey%3D%5C%22index%5C%22%5Cn%20%20%20%20%20%20%20%20class%3D%5C%22list%5C%22%5Cn%20%20%20%20%20%20%20%20%3Aclass%3D%5C%22%7B%20actived%3A%20transformIndex(index)%20%3D%3D%3D%20curComponentIndex%20%7D%5C%22%5Cn%20%20%20%20%20%20%20%20%40click%3D%5C%22onClick(transformIndex(index))%5C%22%5Cn%20%20%20%20%3E%5Cn%20%20%20%20%20%20%20%20%3Cspan%20class%3D%5C%22iconfont%5C%22%20%3Aclass%3D%5C%22'icon-'%20%2B%20getComponent(index).icon%5C%22%3E%3C%2Fspan%3E%5Cn%20%20%20%20%20%20%20%20%3Cspan%3E%7B%7B%20getComponent(index).label%20%7D%7D%3C%2Fspan%3E%5Cn%20%20%20%20%3C%2Fdiv%3E%5Cn%3C%2Fdiv%3E%5Cn%3Cscript%3E%5Cnfunction%20getComponent(index)%20%7B%5Cn%20%20%20%20return%20componentData%5BcomponentData.length%20-%201%20-%20index%5D%5Cn%7D%5Cnfunction%20transformIndex(index)%20%7B%5Cn%20%20%20%20return%20componentData.length%20-%201%20-%20index%5Cn%7D%5Cn%3C%2Fscript%3E%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22AeIrI%22%7D"></div><div><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fdxrdxmxsuvtcc_8b7b88613cd046c98539e4e3f04211b1.png%22%2C%22originWidth%22%3A606%2C%22originHeight%22%3A664%2C%22size%22%3A0%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Afalse%2C%22bottom%22%3Afalse%7D%2C%22width%22%3A606%2C%22height%22%3A664%7D"></span></div><div>经过转换后,层级最高的图片在实时列表里排在最上面,完美!</div><h2 id="item-7">总结</h2><div>至此,可视化拖拽系列的第四篇文章已经结束了,距离上一篇系列文章的发布时间(2021年02月15日)已经有一年多了。没想到这个项目这么受欢迎,在短短一年的时间里获得了很多网友的认可。所以希望本系列的第四篇文章还是能像之前一样,对大家有帮助,再次感谢!</div><div><strong>最后</strong>,毛遂自荐一下自己,本人五年+前端,有基础架构和带团队的经验。有没有大佬有北京、天津的前端岗位推荐。如果有,请在评论区留言,或者私信帮忙内推一下,感谢!</div>
目录
相关文章
|
3月前
|
JavaScript
从零开始写一套广告组件【一】搭建基础框架并配置UI组件库
其实这个从零有点歧义,因为本质上是要基于`tdesign-vue-next`来进行二次封装为一套广告UI组件库,现在让我们在一起快乐的搭建自己的广告UI库之前,先对以下内容做出共识:
97 0
从零开始写一套广告组件【一】搭建基础框架并配置UI组件库
|
4月前
|
C# 开发者 数据处理
WPF开发者必备秘籍:深度解析数据网格最佳实践,轻松玩转数据展示与编辑大揭秘!
【8月更文挑战第31天】数据网格控件是WPF应用程序中展示和编辑数据的关键组件,提供排序、筛选等功能,显著提升用户体验。本文探讨WPF中数据网格的最佳实践,通过DevExpress DataGrid示例介绍其集成方法,包括添加引用、定义数据模型及XAML配置。通过遵循数据绑定、性能优化、自定义列等最佳实践,可大幅提升数据处理效率和用户体验。
69 0
|
5月前
|
小程序 前端开发
【微信小程序-原生开发】实用教程22 - 绘制图表(引入 echarts,含图表的懒加载-获取到数据后再渲染图表,多图表加载等技巧)
【微信小程序-原生开发】实用教程22 - 绘制图表(引入 echarts,含图表的懒加载-获取到数据后再渲染图表,多图表加载等技巧)
287 0
|
数据可视化 JavaScript
可视化拖拽组件库一些技术要点原理分析(二)(上)
可视化拖拽组件库一些技术要点原理分析(二)
110 1
|
数据可视化 API
可视化拖拽组件库一些技术要点原理分析(三)(二)
可视化拖拽组件库一些技术要点原理分析(三)(二)
108 0
|
数据可视化 索引
可视化拖拽组件库一些技术要点原理分析(二)
可视化拖拽组件库一些技术要点原理分析(二)
155 0
|
数据可视化 JavaScript
可视化拖拽组件库一些技术要点原理分析(四)(上)
可视化拖拽组件库一些技术要点原理分析(四)
97 0
可视化拖拽组件库一些技术要点原理分析(四)(上)
|
JSON 数据可视化 前端开发
可视化拖拽组件库一些技术要点原理分析(二)(下)
可视化拖拽组件库一些技术要点原理分析(二)(下)
112 0
|
数据可视化 API 索引
可视化拖拽组件库一些技术要点原理分析(三)(三)
可视化拖拽组件库一些技术要点原理分析(三)(三)
143 0
|
数据可视化 API
可视化拖拽组件库一些技术要点原理分析(三)(一)
可视化拖拽组件库一些技术要点原理分析(三)
108 0