关于拖拽的组内分享
背景
因为这一段时间做的工作都是与拖拽相关,所以老大让我做一期以拖拽为主题的分享。(因团队的技术栈是react,所以下文提到的库和代码均是与react相关)
项目选型
在做拖拽之前,首先比较一下当下比较流行的拖拽库,从中挑选一个合适的进行开发。
库名 | star数 | 支持的拖拽方向 | 最近一次更新 | issues 数量 | 向后兼容 |
---|---|---|---|---|---|
react-dnd | 18.8k | all | 2023-01-20 | 364 | 是 |
react-beautiful-dnd | 29.1k | 只支持横向或纵向 | 2022-11-15 | 495 | 是 |
react-sortable-hoc | 10.3k | all | 2022-05-26 | 211 | react18支持上欠缺 |
根据以上的调查结果显示,react-dnd 更符合现有的项目需求
api概览
DndProvider 与 HTML5Backend
DndProvider
是包裹组件最外层的context,用于数据共享。它还有一个backend
的参数,用于对拖拽行为所在的操作端进行适配。
因为本次需求在pc端,所以主要用到的是react-dnd-html5-backend
。
其他还有控制移动端的react-dnd-touch-backend
,当然,如果上述两个插件不满足需求的话,那么你也可以自定义适合项目的backend并传入DndProvider
即可。
代码示例如下:
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
...
<DndProvider backend={HTML5Backend}>
...your code
</DndProvider>
useDrag
实现组件可以被拖拽的hook 以下会列出一些项目中常用的参数及返回的数据,更多详情见 react-dnd官方文档
传入的参数
type
(必填)定义组件的类型,在后续可以让useDrop
区分出哪些类型可以被放置item
(必填)定义一个描述该组件的对象,如果是一个函数,则组件开始被拖拽时调用并定义一个描述该组件的对象collect
组件的监视器,收集组件当前的状态,并返回到当前组件canDrag
控制组件是否可以被拖拽end
组件被放置时被调用
返回的数据(一个包含三个数据的数组)
- 从 collect 函数收集的属性的对象
- 拖动源的连接器,需要包裹被拖动的组件
- 用于拖动预览的连接器,可以修改组件被拖拽时的预览
useDrop
实现组件可以被放置的hook
传入的参数
accept
(必填)此组件只会对type
相同的拖动源产生反应drop
当拖拽组件放到目标上时调用。如果你有嵌套放置目标,您可以通过检查monitor.didDrop()和来测试嵌套目标是否已经处理monitor.getDropResult()。这在复杂需求下很有用。hover
当拖拽组件在目标上时调用。你可以检查monitor.isOver({ shallow: true })以测试悬停是只发生在当前目标上,还是发生在嵌套目标上。drop()也会调用此方法。canDrop
用它来指定放置目标是否能够接受该项目。collect
组件的监视器,收集组件当前的状态,并返回到当前组件
返回的数据(一个包含两个数据的数组)
- 从 collect 函数收集的属性的对象
- 放置目标的连接器功能,需要包裹放置组件
以上属于前置知识,接下来就是实例演示环节
Monitor 状态存储器
保存被拖拽组件以及接收放置组件的状态并提供给外界查询,方便用户可以实时的查看组件的状态。 常用的被查询的状态如下:
monitor.isDragging()
查询组件是否被拖拽monitor.isOver()
查询组件是否被遮挡monitor.didDrop()
查询组件是否被放置monitor.getClientOffset()
拖动操作正在进行时,返回指针的最后记录的{x,y}client偏移量,常用于拖拽组件体积较大的情况,如超过遮挡盒子宽度的一半,放置行为才生效
demo演示
单向拖拽
只有一个方向的拖拽,以下示例为纵向拖拽 效果图
- 纵向拖拽
- 横向拖拽
代码如下
...
import { useDrag, useDrop } from 'react-dnd';
...
const Card: FC<CardProps> = ({ id, text, index, moveCard, lastHoverCard }) => {
const ref = useRef<HTMLDivElement>(null);
const [ , drop ] = useDrop<
DragItem,
void
>({
accept: ItemTypes.CARD,
hover(item: DragItem, monitor) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
if (dragIndex === hoverIndex) {
return;
}
// 确定盒子的位置
const hoverBoundingRect = ref.current?.getBoundingClientRect();
// 纵向的一半
const hoverMiddleY = hoverBoundingRect.height / 2;
// 鼠标位置
const clientOffset = monitor.getClientOffset();
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
// 向下拖拽 但是鼠标的位置低于下面盒子高度的一半
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return;
// 向上拖拽 但是鼠标的位置低于下面盒子高度的一半
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return;
moveCard(dragIndex, hoverIndex);
item.index = hoverIndex;
},
});
const [ { isDragging }, drag, dragPreview ] = useDrag({
type: ItemTypes.CARD,
item: () => {
console.log('开始拖拽', index);
return { id, index };
},
collect: (monitor: any) => ({
isDragging: monitor.isDragging(),
}),
end: () => {
console.log('结束拖拽');
},
});
return (
<div ref={ref} className="flex mr-2">
...your code
</div>
);
};
多向拖拽
多个方向上结合的拖拽
效果图
代码如下:
多方向的拖拽,为了体验更好,一般要在外面再嵌套一层
注意点
- 因为是多层,可以利用
monitor.didDrop()
来判断被拖拽的组件在内层是否被处理,如果没有被处理,说明该组件没有在内层的任何一个节点上,此时可以根据实际需求,如果要求不高,可以直接放在最后,如果要求较高,可以通过查询鼠标位置来判断被拖拽的位置
外层的Container
...
import { useDrop } from 'react-dnd';
import Card from './Card';
...
const Container: FC = () => {
const [ cards, setCards ] = useState(Array(50)
.fill('')
.map((_item, index) => ({ id: `${index}`, text: `card ${index}` })));
const moveCard = useCallback((dragIndex: number, hoverIndex: number) => {
setCards((prevCards: Item[]) => update(prevCards, {
$splice: [
[ dragIndex, 1 ],
[ hoverIndex, 0, prevCards[dragIndex] as Item ],
],
}));
}, []);
const [ , drop ] = useDrop({
accept: ItemTypes.CARD,
drop(item, monitor) {
const didDrop = monitor.didDrop();
const dropItem = item as {index: number};
if (!didDrop) {
/** 如果在内部拖拽时,没有放到任何一个盒子上面,则将其放到最后 */
moveCard(dropItem.index, cards.length);
}
},
});
const lastHoverCard = useRef<{
dragIndex: number;
hoverIndex: number;
}>();
return (
<div className="flex flex-wrap" ref={drop}>
{cards.map((card, index) => {
return (
<Card {...cardProps} />
);
})}
</div>
);
};
内层的card 注意点
- 其中的
lastHoverCard
的主要作用是保存拖拽的组件信息,方便在end
也就是拖拽结束时再进行渲染,即使数据量较大的场景下,也能保证其性能
...
import { useDrag, useDrop } from 'react-dnd';
...
const Card: FC<CardProps> = ({ id, text, index, moveCard, lastHoverCard }) => {
const ref = useRef<HTMLDivElement>(null);
const [ { isOver }, drop ] = useDrop<DragItem, void, { isOver: boolean }>({
accept: ItemTypes.CARD,
collect(monitor) {
return { isOver: monitor.isOver() };
},
drop(item: DragItem) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
if (dragIndex === hoverIndex) {
return;
}
item.index = hoverIndex;
const toback = dragIndex < hoverIndex;
lastHoverCard.current = {
dragIndex,
hoverIndex: toback ? hoverIndex - 1 : hoverIndex,
};
},
});
const [ { isDragging }, drag, dragPreview ] = useDrag({
type: ItemTypes.CARD,
item: () => {
return { id, index };
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
end: (item) => {
if (lastHoverCard.current) {
const { dragIndex, hoverIndex } = lastHoverCard.current;
moveCard(dragIndex, hoverIndex);
lastHoverCard.current = undefined;
}
},
});
drag(drop(ref));
return (
<div ref={ref} className="flex mr-2">
...your code
</div>
);
};
参考链接
React DnD React拖拽排序组件库对比研究 react-dnd 用法详解
上述代码链接
转载自:https://juejin.cn/post/7205908717887750205