背景
业务驱使,需要封装一款通用的拖拽组件。通过对比常见的拖拽库,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)
三者的关系如下图所示:
组件封装
拖拽组件
所有内容都需要由 包裹,子元素为,将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 节点上。
但是这个方法并不能解决我的问题,因为还有自定义placeholder的需求。在拖拽时还需要计算placeholder的left的距离,也就需要获取当前拖拽元素的parentNode下的子元素,使用createPortal则获取不到拖拽元素的原parentNode,因此放弃createPortal的方案。
拖拽排序问题
react-beautiful-dnd 的排序是基于的index 来排序的,因此在拖拽结束后,需要更新draggableItem 的index,否则会出现意想不到的找不到原因的bug。
拖拽中倾斜样式
因为在组件库中是使用`transform`来实现定位的,因此在实现倾斜时,不能直接使用`transform:rorate`,需要先获取到当前的定位数据,后拼接到设置中即可。
placeholder 占位问题
效果图如下图所示:
实现思路:在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时,设置拖拽组件隐藏不需要展示的内容。
动画问题
在拖动结束,会给对应元素加一个transform 以及 transition。
结语
在后续业务使用中会有各种新增交互,踩坑会继续补充。