基于react-beautiful-dnd 封装拖拽库

1,296 阅读4分钟

背景

业务驱使,需要封装一款通用的拖拽组件。通过对比常见的拖拽库,react-dnd、react-smooth-dnd 、react-beautiful-dnd等,鉴于react-beautiful-dnd支持touch 拖拽、使用流畅等特性,选择基于react-beautiful-dnd库封装拖拽组件。

react-beautiful-dnd 结构

react-beautiful-dnd 主要包含三个组件:

DragDropContext

用于包装拖拽根组件,Draggable和Droppable都需要包裹在DragDropContext内。

Draggable

用于包装你需要拖动的组件,使组件能够被拖拽(make it draggable)

Droppable

用于包装接收拖拽元素的组件,使拖拽组件能够放置(dropped on it)

三者的关系如下图所示:

image.png

组件封装

拖拽组件

所有内容都需要由 包裹,子元素为,将react-beautiful-dnd支持的回调函数暴露出去。

onBeforeCapture?(before: BeforeCapture): void; // 开始捕获前-鼠标点下
onBeforeDragStart?(initial: DragStart): void; // 开始拖动前
onDragStart?(initial: DragStart, provided: ResponderProvided): void; // 开始拖拽
onDragUpdate?(initial: DragUpdate, provided: ResponderProvided): void; // 拖拽中
onDragEnd(result: DropResult, provided: ResponderProvided): void; //拖拽结束

一个拖拽项目中,可以存在多个可以放置拖拽元素的模块,即在 可以存在多个,因此组件props 需要传入一个以 dropId(string 类型) 为 key 的的对象。可以渲染多个。

<DragDropContext
  onDragEnd={_onDragEnd}
  onBeforeDragStart={onBeforeDragStart}
  onDragUpdate={_onDragUpdate}
>
  {Object.keys(draggableItems)?.map((item) => {
    return (
      <DropContainer
        key={item}
        id={item}
      >
        {draggableItems[item]?.map((dragItem, index) => dragItem.content(index))}
      </DropContainer>
    );
  })}
</DragDropContext>

DropContainer 与 DragContainer

为了支持JSX写法,我们需要将 Droppable 与 Draggable 暴露出去。Droppable 与 Draggable 均会暴露出 (provided, snapshot),包含当前拖拽状态等。具体含义可以参见官方文档。

<Droppable droppableId={props.id} type="TASK">
  {(provided, snapshot) => {
    return (
      <div
        ref={provided.innerRef}
        {...provided.droppableProps}
      >
        {props.children}
        {provided.placeholder}
      </div>
    );
  }}
</Droppable>
<Draggable draggableId={props.id} index={props.index} isDragDisabled={props.isDragDisabled}>
  {(provided, snapshot) => {
    return (
      <div
          {...provided.draggableProps}
          ref={provided.innerRef}
        >
          {children(provided, snapshot)}
    </div>
    );
  }}
</Draggable>

这样就将拖拽的三个主要部分导出了。可以适用于简单的拖拽场景,支持拖拽内容可配置。

踩坑记录

拖拽映射位置错位

在拖拽过程中,会出现拖拽中的元素与鼠标有错位的情况:

原因分析: react-beautiful-dnd 拖拽使用的是 position:fixed 实现的,在拖拽中使用了 transform,以及top: **,因此拖拽中映射会出现错位问题。 解决方案:

1、改变定位 (采用)

在样式中,覆盖top、left 设置值,同时在拖拽中修改对应item 定位方式。

2、使用React Portal

将组件渲染完后挂载到对应dom 节点上。

github.com/atlassian/r…

但是这个方法并不能解决我的问题,因为还有自定义placeholder的需求。在拖拽时还需要计算placeholder的left的距离,也就需要获取当前拖拽元素的parentNode下的子元素,使用createPortal则获取不到拖拽元素的原parentNode,因此放弃createPortal的方案。

拖拽排序问题

react-beautiful-dnd 的排序是基于的index 来排序的,因此在拖拽结束后,需要更新draggableItem 的index,否则会出现意想不到的找不到原因的bug。

拖拽中倾斜样式

因为在组件库中是使用`transform`来实现定位的,因此在实现倾斜时,不能直接使用`transform:rorate`,需要先获取到当前的定位数据,后拼接到设置中即可。

placeholder 占位问题

效果图如下图所示:

image.png

实现思路:在onDragUpdate时,获取到移动中的元素的宽、高以及定位元素,将其定位信息进行存储。同时在DropContainer 中添加一个定位为 absolute 的div,其定位信息为刚刚存储的定位信息。 同时要注意的是,在获取对应的dom 元素时,要获取 data-rbd-draggable-id为key的,而不是data-rbd-drag-handle-draggable-id,原因是在Draggable包裹的元素中,不是所有的元素均可拖动,data-rbd-drag-handle-draggable-id包裹的是可触发拖动部分元素, data-rbd-draggable-id是 包裹的元素,这样才能拿到对应拖动item parentNode,在定位时才不会出现偏差。

高度塌陷问题

问题描述

在实际拖拽场景中,拖拽前的item有可能是非常长的列表,在拖拽中为了展示全所有的拖拽项目,要收起长列表,只展示item 的缩略图,即拖拽中和非拖拽中的item高度是不一致的,在使用该组件库时,虽然item高度改变,但是处于拖拽item占位还是之前的高度。

调查及改进

在反复拖拽中,首先要改变拖拽元素高度,由固定值 -> auto。同时发现,在拖拽过程中,处于拖拽中元素下方的元素会被设置 transform:translate(*px,*px),因此在渲染 时,需要手动修改处于拖拽中元素下方的 transform属性,保证下方元素距离上方改变元素位置保持正常距离。同时,需要在onBeforeDragStart时,设置拖拽组件隐藏不需要展示的内容。

动画问题

image.png

在拖动结束,会给对应元素加一个transform 以及 transition。

结语

在后续业务使用中会有各种新增交互,踩坑会继续补充。