京东低代码平台:水滴物料拖拽至画布方案实践与解析

4,963 阅读4分钟

前言\color{#003a8c}{前言}

拖拽能力作为低代码平台核心能力之一,对于搭建效率的提升有至关重要的作用。水滴低代码平台的画布区域是基于react-grid-layout实现,原生具备了画布内的拖拽能力,但对于侧边栏物料等画布以外的元素如何拖拽至画布指定位置则需要自行实现。本文是京东水滴低代码平台关于物料可视化拖拽的一些思路及总结,希望可以对有相关需求的同学提供一些帮助和借鉴。

实现思路\color{#003a8c}{实现思路}

把一个大象装进冰箱需要3步,那么如果我们想把如下图所示的物料拖到画布指定位置需要几步呢?

第一步:物料可以被拖动

拖放(Drag 和 Drop)是很常见的特性。它指的是您抓取某物并拖入不同的位置。拖放是 HTML5 标准的组成部分:任何元素都是可拖放的。

怎么才可以让页面物料可以拖动呢?很幸运,HTML5已经为我们提供了拖放能力,而且使用很方便,只需要在希望被拖动的元素上设置draggable="true"即可。这样目标元素就可以进行拖拽了。

第二步:确定当前拖动物料

物料是可以拖拽了,但是还需要知道当前拖拽的是什么物料,才能正确添加到画布。拖放能力也提供了一些在拖放的过程中会触发的事件:

  • 在拖动目标上触发事件(源元素):

    • ondragstart - 用户开始拖动元素时触发
    • ondrag - 元素正在拖动时触发
    • ondragend - 用户完成元素拖动后触发
  • 释放目标时触发的事件:

    • ondragenter - 当被鼠标拖动的对象进入其容器范围内时触发此事件
    • ondragover - 当某被拖动的对象在另一对象容器范围内拖动时触发此事件
    • ondragleave - 当被鼠标拖动的对象离开其容器范围内时触发此事件
    • ondrop - 在一个拖动过程中,释放鼠标键时触发此事件

用户开始拖动元素时触发ondragstart事件,这个事件很符合要求,当该事件触发,这时就可以通过dispatch将拖拽的物料数据保存到store中。很好,数据保存了,物料也可以拖动,离目标更近了一步。

第三步:确定物料放入画布区域以及在画布中的位置信息

HTML5的拖放能力提供的事件中有一个当释放目标时触发的事件ondrop,但是前面在谈及现状时,其实我们已经提到,目前画布区域是基于react-grid-layout插件实现的,所以不能直接这样使用,这就需要查看react-grid-layout插件的文档是否支持该事件。在文档上面,找到了需要用到的三个字段

// 该字段是判断是否要触发onDrop以及是否允许在画布内拖动
isDroppable: ?boolean = false// 拖拽元素在画布中的占位元素信息,用来计算最终的位置信息
// i -- id
// w -- width
// h -- height
droppingItem?: { i: string, w: number, h: number }
​
// 当释放拖拽目标到画布区域时触发的事件,这里插件帮助我们计算了拖拽元素在画布中最后落下的位置信息
// Calls when an element has been dropped into the grid from outside.
onDrop: (layout: Layout, item: ?LayoutItem, e: Event) => void

通过onDrop字段可以确定拖拽目标落入画布,同时还提供了目标元素在画布中的位置信息,到这里需要的数据和条件都满足了,现在就可以尝试把我们的方案去实现了。

代码实现\color{#003a8c}{代码实现}

基本思路如下图:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b5023897f62c4210b8246d467a978b1c~tplv-k3u1fbpfcp-zoom-1.image

第一步:支持拖拽设置

<div
   className={`droppable-element ${styles.materialGroupItem}`}
    // 允许拖拽 
    draggable
    // unselectable="on"
    key={key}
    // 用户开始拖动元素时触发
    onDragStart={(e) => {
      onDragStart(type, key, e)
    }}
  <i className={icon} style={{ fontSize: 30 }} />
  <div className={styles.materialGroupItemName}>{name}</div>
</div>

第二步:保存当前拖拽物料的数据

const onDragStart = (type, key, e) => {
  ...
  setMaterial({
    type,
    key: _key,
    materialData: data,
    droppingItem: {
      i,
      w: layout?.width || 1200,
      h: layout?.height || 360
    },
    offsetX: e.nativeEvent.offsetX,
    offsetY: e.nativeEvent.offsetY
  })
}

第三步:确定物料放入画布区域并且将其添加到画布

<GridLayout
  ...
  onDrop={onDrop}
  isDroppable
  droppingItem={material.droppingItem}
</GridLayout>
  /**
   * Calls when an element has been dropped into the grid from outside
   */
  const onDrop = (layouts, item, e) => {
    // 拖拽元素在画布中位置
    const { x, y } = item
    const dropInfo = {
      ...material,
      x,
      y,
      layouts
    }
    addMaterial(dropInfo)
  }

效果展示:

遇到问题\color{#003a8c}{遇到问题}

  • 问题一
    • 问题描述

      当把物料拖入画布,布局发生变化react-layout-grid插件就会触发onchange事件;这时会在store中添加一个新的物料,但是如果虚幻一枪,只是在画布中逛了一圈,但是没有放入画布,这时react-layout-grid插件只是删除了占位元素,但是没有触发onchange,导致store中多一条不应该添加的数据。

    • 解决方案

      当画布在拖拽过程中时,不要去更新store中的物料列表,具体方案就是添加一个标志位,当触发ondragstart时设置成true;拖拽完成触发ondragend设置成false;只有物料落入画布,触发ondrop时,将更新到物料列表。

  • 问题二
    • 问题描述

      当物料在画布中拖动时,会比较卡顿,非常影响用户体验

    • 解决方案

      在使用chrome浏览器performance面板对页面进行性能分析时,发现拖拽过程中一直在高频率调用ondragover,造成了大量long task执行,导致页面掉帧严重,通过对该事件进行截流,减少调用次数,降低function call消耗时间,避免对页面渲染进行阻塞,将每秒的渲染帧数从36帧提升到53帧

      优化前:

      https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d4049e91892242eb96a4490f622ee227~tplv-k3u1fbpfcp-zoom-1.image

      优化后:

      https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ca2683d915bc4be6b9d5bc02f95bf39f~tplv-k3u1fbpfcp-zoom-1.image

总结\color{#003a8c}{总结}

通过HTML5拖拽能力的draggable属性以及ondragstart事件和ondrop事件,并且借助react-grid-layout插件提供的物料位置信息,完成了物料可视化拖拽的实现。需要注意的是对于拖拽这类高频触发的能力,应该多考虑性能问题,避免对用户造成不好的用户体验感。

如果有好的方案或者建议,欢迎提出。同时感谢大家抽出宝贵的时间阅读本篇文章。