关于拖拽的组内分享

906 阅读5分钟

背景

因为这一段时间做的工作都是与拖拽相关,所以老大让我做一期以拖拽为主题的分享。(因团队的技术栈是react,所以下文提到的库和代码均是与react相关)

项目选型

在做拖拽之前,首先比较一下当下比较流行的拖拽库,从中挑选一个合适的进行开发。

库名star数支持的拖拽方向最近一次更新issues 数量向后兼容
react-dnd18.8kall2023-01-20364
react-beautiful-dnd29.1k只支持横向或纵向2022-11-15495
react-sortable-hoc10.3kall2022-05-26211react18支持上欠缺

根据以上的调查结果显示,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 组件被放置时被调用
返回的数据(一个包含三个数据的数组)
  1. 从 collect 函数收集的属性的对象
  2. 拖动源的连接器,需要包裹被拖动的组件
  3. 用于拖动预览的连接器,可以修改组件被拖拽时的预览

useDrop

实现组件可以被放置的hook

传入的参数
  • accept (必填)此组件只会对type相同的拖动源产生反应
  • drop 当拖拽组件放到目标上时调用。如果你有嵌套放置目标,您可以通过检查monitor.didDrop()和来测试嵌套目标是否已经处理monitor.getDropResult()。这在复杂需求下很有用。
  • hover 当拖拽组件在目标上时调用。你可以检查monitor.isOver({ shallow: true })以测试悬停是只发生在当前目标上,还是发生在嵌套目标上。drop()也会调用此方法。
  • canDrop 用它来指定放置目标是否能够接受该项目。
  • collect 组件的监视器,收集组件当前的状态,并返回到当前组件
返回的数据(一个包含两个数据的数组)
  1. 从 collect 函数收集的属性的对象
  2. 放置目标的连接器功能,需要包裹放置组件

以上属于前置知识,接下来就是实例演示环节

Monitor 状态存储器

保存被拖拽组件以及接收放置组件的状态并提供给外界查询,方便用户可以实时的查看组件的状态。
常用的被查询的状态如下:

  • monitor.isDragging() 查询组件是否被拖拽
  • monitor.isOver() 查询组件是否被遮挡
  • monitor.didDrop() 查询组件是否被放置
  • monitor.getClientOffset() 拖动操作正在进行时,返回指针的最后记录的{x,y}client偏移量,常用于拖拽组件体积较大的情况,如超过遮挡盒子宽度的一半,放置行为才生效

demo演示

单向拖拽

只有一个方向的拖拽,以下示例为纵向拖拽
效果图

  • 纵向拖拽 vertical.gif
  • 横向拖拽 horizontal.gif

代码如下

...
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>
  );
};

多向拖拽

多个方向上结合的拖拽
效果图 mix.gif 代码如下:
多方向的拖拽,为了体验更好,一般要在外面再嵌套一层
注意点

  • 因为是多层,可以利用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 用法详解

上述代码链接

drag demo