翼度科技»论坛 编程开发 JavaScript 查看内容

React中使用dnd-kit实现拖曳排序功能

7

主题

7

帖子

21

积分

新手上路

Rank: 1

积分
21
由于前阵子需要在开发 Picals 的时候,需要实现一些拖动排序的功能。虽然有原生的浏览器 dragger API,不过纯靠自己手写很难实现自己想要的效果,更多的是吃力不讨好。于是我四处去调研了一些 React 中比较常用的拖曳库,最终确定了
  1. dnd-kit
复制代码
作为我实现拖曳排序的工具。
当然,使用的时候肯定免不了踩坑。这篇文章的意义就是为了记录所踩的坑,希望能够为有需要的大家提供一点帮助。
在这篇文章中,我将带着大家一起实现如下的拖曳排序的例子:

那让我们开始吧。

安装

安装
  1. dnd-kit
复制代码
工具库很简单,只需要输入下面的命令进行安装即可:
  1. pnpm add @dnd-kit/core @dnd-kit/sortable @dnd-kit/modifiers @dnd-kit/utilities
复制代码
这几个包分别有什么作用呢?

    1. @dnd-kit/core
    复制代码
    :核心库,提供基本的拖拽功能。
    1. @dnd-kit/sortable
    复制代码
    :扩展库,提供排序功能和工具。
    1. @dnd-kit/modifiers
    复制代码
    :修饰库,提供拖拽行为的限制和修饰功能。
    1. @dnd-kit/utilities
    复制代码
    :工具库,提供 CSS 和实用工具函数。上述演示的平滑移动的样式就是来源于这个包。

使用方法

首先我们需要知道的是,拖曳这个行为需要涉及到两个部分:

  • 能够允许被拖曳的有限空间(父容器)
  • 用户真正进行拖曳的子元素
在使用
  1. dnd-kit
复制代码
时,需要对这两个部分分别进行定义。

父容器(DraggableList)的编写

我们首先进行拖曳父容器相关的功能配置。话不多说我们直接上代码:
  1. import { FC, useEffect, useState } from "react";
  2. import type { DragEndEvent, DragMoveEvent } from "@dnd-kit/core";
  3. import { DndContext } from "@dnd-kit/core";
  4. import {
  5.   arrayMove,
  6.   SortableContext,
  7.   rectSortingStrategy,
  8. } from "@dnd-kit/sortable";
  9. import { restrictToParentElement } from "@dnd-kit/modifiers";
  10. import "./index.scss";
  11. import DraggableItem from "../draggable-item";

  12. type ImgItem = {
  13.   id: number;
  14.   url: string;
  15. };

  16. const DraggableList: FC = () => {
  17.   const [list, setList] = useState<ImgItem[]>([]);

  18.   useEffect(() => {
  19.     setList(
  20.       Array.from({ length: 31 }, (_, index) => ({
  21.         id: index + 1,
  22.         url: String(index),
  23.       }))
  24.     );
  25.   }, []);

  26.   const getMoveIndex = (array: ImgItem[], dragItem: DragMoveEvent) => {
  27.     const { active, over } = dragItem;
  28.     const activeIndex = array.findIndex((item) => item.id === active.id);
  29.     const overIndex = array.findIndex((item) => item.id === over?.id);

  30.     // 处理未找到索引的情况
  31.     return {
  32.       activeIndex: activeIndex !== -1 ? activeIndex : 0,
  33.       overIndex: overIndex !== -1 ? overIndex : activeIndex,
  34.     };
  35.   };

  36.   const dragEndEvent = (dragItem: DragEndEvent) => {
  37.     const { active, over } = dragItem;
  38.     if (!active || !over) return; // 处理边界情况

  39.     const moveDataList = [...list];
  40.     const { activeIndex, overIndex } = getMoveIndex(moveDataList, dragItem);

  41.     if (activeIndex !== overIndex) {
  42.       const newDataList = arrayMove(moveDataList, activeIndex, overIndex);
  43.       setList(newDataList);
  44.     }
  45.   };

  46.   return (
  47.     <DndContext onDragEnd={dragEndEvent} modifiers={[restrictToParentElement]}>
  48.       <SortableContext
  49.         items={list.map((item) => item.id)}
  50.         strategy={rectSortingStrategy}
  51.       >
  52.         <div className="drag-container">
  53.           {list.map((item) => (
  54.             <DraggableItem key={item.id} item={item} />
  55.           ))}
  56.         </div>
  57.       </SortableContext>
  58.     </DndContext>
  59.   );
  60. };

  61. export default DraggableList;
复制代码
对应的
  1. index.scss
复制代码
  1. .drag-container {
  2.   position: relative;
  3.   width: 800px;
  4.   display: flex;
  5.   flex-wrap: wrap;
  6.   gap: 20px;
  7. }
复制代码
return 的 DOM 元素结构非常简单,最主要的无外乎两个上下文组件:
  1. DndContext
复制代码
  1. SortableContext
复制代码


    1. DndContext
    复制代码
    :是 dnd-kit 的核心组件,用于提供拖放的上下文。
    1. SortableContext
    复制代码
    :是一个上下文组件,用于提供排序的功能。
  1. SortableContext
复制代码
组件内部包裹的,就是我们正常的需要进行排序的列表容器了。当然,dnd-kit 也不是对任何的内容都可以进行排序的。要想实现排序功能,这个被包裹的 DOM 元素必须符合以下几个要求:

  • 必须是可排序的元素:
    1. SortableContext
    复制代码
    需要包裹的元素具有相同的父级容器,且这些元素需要具备可排序的能力。每个子元素应当是独立的可拖拽项,例如一个列表项、卡片或网格中的块。
  • 提供唯一的
    1. id
    复制代码
    :每个可排序的子元素必须具有唯一的
    1. id
    复制代码
    1. SortableContext
    复制代码
    会通过这些
    1. id
    复制代码
    来识别和管理每个拖拽项的位置。你需要确保
    1. items
    复制代码
    属性中提供的
    1. id
    复制代码
    数组与实际渲染的子元素的
    1. id
    复制代码
    一一对应。
  • 需要是同一个父容器的直接子元素:
    1. SortableContext
    复制代码
    内部的子元素必须是同一个父容器的直接子元素,不能有其他中间层级。这是因为排序和拖拽是基于元素的相对位置和布局来计算的。
  • 使用相同的布局策略:
    1. SortableContext
    复制代码
    的子元素应当使用相同的布局策略,例如使用 CSS Flexbox 或 Grid 进行布局。这样可以确保拖拽操作时,子元素之间的排列和移动逻辑一致。
  • 设置相同的样式属性:确保子元素具有相同的样式属性,例如宽度、高度、边距等。这些属性一致性有助于拖拽过程中视觉效果的一致性和准确性。
  • 添加必要的样式以支持拖拽:为了支持拖拽效果,子元素应具备必要的样式。例如,设置
    1. position
    复制代码
    1. relative
    复制代码
    以便于绝对定位的拖拽项,设置
    1. overflow
    复制代码
    以防止拖拽项溢出。
  • 确保有足够的拖拽空间:父容器应当有足够的空间来允许子元素的拖拽操作。如果空间不足,可能会导致拖拽操作不顺畅或无法完成。
  • 子元素必须具备
    1. draggable
    复制代码
    属性:每个子元素应该具备
    1. draggable
    复制代码
    属性,以表明该元素是可拖动的。这通常通过 dnd-kit 提供的组件如
    1. Draggable
    复制代码
    1. Sortable
    复制代码
    来实现。
  • 提供合适的拖拽处理程序:为子元素添加合适的拖拽处理程序,通常通过 dnd-kit 提供的钩子或组件实现。例如,使用
    1. useDraggable
    复制代码
    钩子来处理拖拽逻辑。
  • 处理子元素布局变化:确保在拖拽过程中,子元素的布局变化能够被正确处理。例如,设置适当的动画效果以平滑地更新布局。
  1. 在这里附加一个说明,可以看到我初始化的数据的列表 id 是从 1 开始的,因为 <strong>从 0 开始会导致第一个元素无法触发移动</strong> 。现阶段还不知道是什么原因,大概的猜测是在 JavaScript 和 React 中,[code]id
复制代码
  1. 0
复制代码
可能会被视为“假值”(falsy value)。许多库和框架在处理数据时,会有意无意地忽略或处理“假值”。dnd-kit 可能在某些情况下忽略了
  1. id
复制代码
  1. 0
复制代码
的元素,导致其无法正常参与拖曳操作。总之, 避免第一个拖曳元素的 id 不要为 0 或者空字符串 。[/code]对于
  1. DndContext
复制代码
,需要传入几个 props 以处理拖曳事件本身。在这里,传入了
  1. onDragEnd
复制代码
函数与
  1. modifiers
复制代码
修饰符列表。实际上,这个上下文组件能够传入很多的 props,我在这里简单截个图:

可以看到,不仅是结束回调,也接受拖曳全过程的函数回调并通过回传值进行一些数据处理。
但是,一般用于完成拖曳排序功能我们可以不管这么多,只用管鼠标松开后的回调函数,然后拿到对象进行处理就可以了。

    1. onDragEnd
    复制代码
    :顾名思义,就是用户鼠标松开后触发的拖曳事件的回调。触发时会自动传入类型为
    1. DragEndEvent
    复制代码
    的对象,我们可以从其中拿出
    1. active
    复制代码
    1. over
    复制代码
    两个参数来具体处理拖曳事件。
    active 包含 正在拖曳的元素的相关信息,over 包含最后鼠标松开时所覆盖到的元素的相关信息
    结合到我的函数:
  1. const dragEndEvent = (dragItem: DragEndEvent) => {
  2.   const { active, over } = dragItem;
  3.   if (!active || !over) return; // 处理边界情况

  4.   const moveDataList = [...list];
  5.   const { activeIndex, overIndex } = getMoveIndex(moveDataList, dragItem);

  6.   if (activeIndex !== overIndex) {
  7.     const newDataList = arrayMove(moveDataList, activeIndex, overIndex);
  8.     setList(newDataList);
  9.   }
  10. };
复制代码
首先检查
  1. active
复制代码
  1. over
复制代码
是否有效,避免边界问题,之后创建
  1. moveDataList
复制代码
的副本,调用
  1. getMoveIndex
复制代码
函数获取
  1. active
复制代码
  1. over
复制代码
项目的索引,如果两个索引不同,使用
  1. arrayMove
复制代码
移动项目,并更新
  1. list
复制代码
状态。
  1. getMoveIndex
复制代码
函数如下,用于获取拖拽项目和目标位置的索引:
  1. const getMoveIndex = (array: ImgItem[], dragItem: DragMoveEvent) => {
  2.   const { active, over } = dragItem;
  3.   const activeIndex = array.findIndex((item) => item.id === active.id);
  4.   const overIndex = array.findIndex((item) => item.id === over?.id);

  5.   // 处理未找到索引的情况
  6.   return {
  7.     activeIndex: activeIndex !== -1 ? activeIndex : 0,
  8.     overIndex: overIndex !== -1 ? overIndex : activeIndex,
  9.   };
  10. };
复制代码

  • 通过
    1. findIndex
    复制代码
    获取
    1. active
    复制代码
    1. over
    复制代码
    项目的索引,如果未找到,默认返回 0。
    1. modifiers
    复制代码
    :标识符,传入一个标识符数组以限制在父组件进行拖曳的行为。主要可选的一些标识符如下:

      1. restrictToParentElement
      复制代码
      :限制在父元素内。
      1. restrictToFirstScrollableAncestor
      复制代码
      :限制在第一个可滚动祖先元素。
      1. restrictToVerticalAxis
      复制代码
      :限制在垂直轴上。
      1. restrictToHorizontalAxis
      复制代码
      :限制在水平轴上。
      1. restrictToBoundingRect
      复制代码
      :限制在指定矩形区域内。
      1. snapCenterToCursor
      复制代码
      :使元素中心对齐到光标。
    在这里我选择了一个比较普通的限制在父元素内的标识符。可以按照具体的定制需要,配置不同的标识符组合来限制拖曳行为。

接下来是对
  1. SortableContext
复制代码
的配置解析。在这个组件中传入了
  1. items
复制代码
  1. strategy
复制代码
两个参数。同样地,它也提供了很多的 props 以供个性化配置:
  1. items
复制代码
:用于定义可排序项目的唯一标识符数组,它告诉
  1. SortableContext
复制代码
哪些项目可以被拖拽和排序。它的类型刚好和上述的 active 和 over 的 id 属性的类型相同,都是
  1. UniqueIdentifier
复制代码


这也就意味着,我们在 items 这边传入了什么数组来对排序列表进行唯一性表示,active 和 over 就按照什么来追踪元素的排序索引。
  1. UniqueIdentifier
复制代码
实际上是 string 和 number 的联合类型。


  • 因此,只要是每个 item 唯一的,无论传字符串或者数字都是可以的。
    1. strategy
    复制代码
    :策略,用于定义排序算法,它指定了拖拽项目在容器内如何排序和移动。它通过提供一个函数来控制项目在拖拽过程中的排序行为。它决定了拖拽项目的排序方式和在拖拽过程中如何移动。例如,它可以控制项目按行、按列或者自由布局进行排序,并且不同的排序策略可以提供不同的用户交互体验。例如,矩形排序、水平排序或者垂直排序等。
    常用的排序策略有如下几种:

      1. rectSortingStrategy
      复制代码

      • 适用场景:矩形网格布局,比如 flex 容器内部配置
        1. flex-wrap: wrap
        复制代码
        换行之后,可以采用这种策略。
      • 说明:项目根据矩形区域进行排序,适用于二维网格布局。

      1. horizontalListSortingStrategy
      复制代码

      • 适用场景:水平列表,只用于单行的 flex 布局。
      • 说明:项目按水平顺序排列,适用于水平滚动的列表。

      1. verticalListSortingStrategy
      复制代码

      • 适用场景:垂直列表,只用于单列的 flex 布局,配置了
        1. flex-direction: column
        复制代码
        之后使用。
      • 说明:项目按垂直顺序排列,适用于垂直滚动的列表。

    除了这几种以外,你还可以自定义一些策略,按照你自己的需求自己写。不过一般也用不到自己写 www

至此,父容器组件介绍完毕,我们来看子元素怎么写吧。

子元素(Draggable-item)的编写

上代码:
  1. import { FC } from "react";
  2. import { useSortable } from "@dnd-kit/sortable";
  3. import { CSS } from "@dnd-kit/utilities";
  4. import "./index.scss";

  5. type ImgItem = {
  6.   id: number;
  7.   url: string;
  8. };

  9. type DraggableItemProps = {
  10.   item: ImgItem;
  11. };

  12. const DraggableItem: FC<DraggableItemProps> = ({ item }) => {
  13.   const { setNodeRef, attributes, listeners, transform, transition } =
  14.     useSortable({
  15.       id: item.id,
  16.       transition: {
  17.         duration: 500,
  18.         easing: "cubic-bezier(0.25, 1, 0.5, 1)",
  19.       },
  20.     });
  21.   const styles = {
  22.     transform: CSS.Transform.toString(transform),
  23.     transition,
  24.   };

  25.   return (
  26.     <div
  27.       ref={setNodeRef}
  28.       {...attributes}
  29.       {...listeners}
  30.       style={styles}
  31.       className="draggable-item"
  32.     >
  33.       <span>{item.url}</span>
  34.     </div>
  35.   );
  36. };

  37. export default DraggableItem;
复制代码
对应的
  1. index.scss
复制代码
  1. .draggable-item {
  2.   width: 144px;
  3.   height: 144px;
  4.   background-color: #f0f0f0;
  5.   display: flex;
  6.   justify-content: center;
  7.   align-items: center;
  8.   font-size: large;
  9.   cursor: pointer;
  10.   user-select: none;
  11.   border-radius: 10px;
  12.   overflow: hidden;
  13. }
复制代码
子元素的编写相较于父容器要简单得多,需要手动配置的少,引入的包更多了。
首先是引入了
  1. useSortable
复制代码
这个 hook,主要用来启用子元素的排序功能。这个钩子返回了一组现成的属性和方法:

    1. setNodeRef
    复制代码
    :用于将 DOM 节点与拖拽行为关联。
    1. attributes
    复制代码
    :包含与可拖拽项目相关的属性,例如
    1. role
    复制代码
    1. tabIndex
    复制代码

    1. listeners
    复制代码
    :包含拖拽操作的事件监听器,例如
    1. onMouseDown
    复制代码
    1. onTouchStart
    复制代码

    1. transform
    复制代码
    :包含当前项目的转换属性,用于设置位置和旋转等。
    1. transition
    复制代码
    :定义项目的过渡效果,用于动画处理。
它接受一个配置对象,其中包含了:

    1. id
    复制代码
    :在父容器组件中提到的唯一标识符,需要和父容器中传入 items 的列表的元素的属性是一致的,一般直接通过 map 来一次性传入。
    1. transition
    复制代码
    :动画效果的配置,包含
    1. duration
    复制代码
    1. easing
    复制代码

之后我们定义了拖曳样式
  1. styles
复制代码
,使用了
  1. @dnd-kit/utilities
复制代码
提供的
  1. CSS
复制代码
工具库,用于处理 CSS 相关的样式转换,因为这里的
  1. transform
复制代码
是从 hook 拿到的,是其自定义的
  1. Transform
复制代码
类型,需要借助其转为正常的 css 样式。我们传入了从
  1. useSortable
复制代码
中拿到的
  1. transform
复制代码
  1. transition
复制代码
,用于处理拖曳 item 的样式。
之后就是直接一股脑的将配置全部传入要真正进行拖曳的 DOM 元素:
  1.   return (
  2.     <div
  3.       ref={setNodeRef}
  4.       {...attributes}
  5.       {...listeners}
  6.       style={styles}
  7.       className="draggable-item"
  8.     >
  9.       <span>{item.url}</span>
  10.     </div>
  11.   );
  12. };
复制代码

    1. ref={setNodeRef}
    复制代码
    :通过
    1. setNodeRef
    复制代码
    1. div
    复制代码
    关联到拖拽功能。
    1. {...attributes}
    复制代码
    :将所有与可拖拽项目相关的属性应用到
    1. div
    复制代码
    ,例如
    1. role="button"
    复制代码
    1. tabIndex="0"
    复制代码

    1. {...listeners}
    复制代码
    :将所有拖拽操作的事件监听器应用到
    1. div
    复制代码
    ,例如
    1. onMouseDown
    复制代码
    1. onTouchStart
    复制代码
    ,使其能够响应用户的拖拽操作。这里是因为我整个 DOM 元素都要支持拖曳,所以我把它直接加到了最外层。如果需要只在子元素特定的区域内实现拖曳,listeners 就加到需要真正鼠标拖动的那个 DOM 上即可。
    1. style={styles}
    复制代码
    :应用定义好的
    1. styles
    复制代码
    对象,设置
    1. transform
    复制代码
    1. transition
    复制代码
    样式,使拖拽时能够实现平滑过渡。
    1. className="draggable-item"
    复制代码
    :设置组件的样式类名,用于样式定义。

实现效果

父容器和子元素全都编写完毕后,我们可以观察一下总体的实现效果如何:

可以看到,元素已经能够正常地被排序,而且列表也能够同样地被更新。结合到具体的例子,可以把这个列表 item 结合更加复杂的类型进行处理即可。只要保证每个 item 有唯一的 id 即可。

对于原有点击事件失效的处理

对于某些需要触发点击事件的拖曳 item,如果按照上述方式封装了拖曳子元素所需的一些配置,那么 原有的点击事件将会失效,因为原有的鼠标按下的点击事件被拖曳事件给覆盖掉了。当然,dnd-kit 肯定也是考虑到了这种情况。他们在其核心库
  1. @dnd-kit/core
复制代码
当中封装了一个 hook
  1. useSensors
复制代码
,用来配置 鼠标拖动多少个像素之后才触发拖曳事件,在此之前不触发拖曳事件
使用方法也非常简单,首先从核心库中导入这个 hook,之后进行如下的配置:
  1. //拖拽传感器,在移动像素5px范围内,不触发拖拽事件
  2. const sensors = useSensors(
  3.   useSensor(MouseSensor, {
  4.     activationConstraint: {
  5.       distance: 5,
  6.     },
  7.   })
  8. );
复制代码
这里配置了在 5px 范围内不触发拖曳事件,这样就可以在这个范围内进行点击事件的正常触发了。
在上面的
  1. DndContext
复制代码
的 props 中,我们也看到了其提供了这一属性的配置。我们只用将编写好的 sensors 传入即可:
  1. <DndContext onDragEnd={dragEndEvent} modifiers={[restrictToParentElement]}>
  2.   <SortableContext
  3.     items={list.map((item) => item.id)}
  4.     strategy={rectSortingStrategy}
  5.     sensors={sensors}
  6.   >
  7.     <div className="drag-container">
  8.       {list.map((item) => (
  9.         <DraggableItem key={item.id} item={item} />
  10.       ))}
  11.     </div>
  12.   </SortableContext>
  13. </DndContext>
复制代码
以上就是React中使用dnd-kit实现拖曳排序功能的详细内容,更多关于React dnd-kit拖曳排序的资料请关注脚本之家其它相关文章!

来源:https://www.jb51.net/javascript/32334737w.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
来自手机

举报 回复 使用道具