React中使用dnd-kit實現拖曳排序功能
由于前陣子需要在開發(fā) Picals 的時候,需要實現一些拖動排序的功能。雖然有原生的瀏覽器 dragger API,不過純靠自己手寫很難實現自己想要的效果,更多的是吃力不討好。于是我四處去調研了一些 React 中比較常用的拖曳庫,最終確定了 dnd-kit 作為我實現拖曳排序的工具。
當然,使用的時候肯定免不了踩坑。這篇文章的意義就是為了記錄所踩的坑,希望能夠為有需要的大家提供一點幫助。
在這篇文章中,我將帶著大家一起實現如下的拖曳排序的例子:

那讓我們開始吧。
安裝
安裝 dnd-kit 工具庫很簡單,只需要輸入下面的命令進行安裝即可:
pnpm add @dnd-kit/core @dnd-kit/sortable @dnd-kit/modifiers @dnd-kit/utilities
這幾個包分別有什么作用呢?
@dnd-kit/core:核心庫,提供基本的拖拽功能。@dnd-kit/sortable:擴展庫,提供排序功能和工具。@dnd-kit/modifiers:修飾庫,提供拖拽行為的限制和修飾功能。@dnd-kit/utilities:工具庫,提供 CSS 和實用工具函數。上述演示的平滑移動的樣式就是來源于這個包。
使用方法
首先我們需要知道的是,拖曳這個行為需要涉及到兩個部分:
- 能夠允許被拖曳的有限空間(父容器)
- 用戶真正進行拖曳的子元素
在使用 dnd-kit 時,需要對這兩個部分分別進行定義。
父容器(DraggableList)的編寫
我們首先進行拖曳父容器相關的功能配置。話不多說我們直接上代碼:
import { FC, useEffect, useState } from "react";
import type { DragEndEvent, DragMoveEvent } from "@dnd-kit/core";
import { DndContext } from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
rectSortingStrategy,
} from "@dnd-kit/sortable";
import { restrictToParentElement } from "@dnd-kit/modifiers";
import "./index.scss";
import DraggableItem from "../draggable-item";
type ImgItem = {
id: number;
url: string;
};
const DraggableList: FC = () => {
const [list, setList] = useState<ImgItem[]>([]);
useEffect(() => {
setList(
Array.from({ length: 31 }, (_, index) => ({
id: index + 1,
url: String(index),
}))
);
}, []);
const getMoveIndex = (array: ImgItem[], dragItem: DragMoveEvent) => {
const { active, over } = dragItem;
const activeIndex = array.findIndex((item) => item.id === active.id);
const overIndex = array.findIndex((item) => item.id === over?.id);
// 處理未找到索引的情況
return {
activeIndex: activeIndex !== -1 ? activeIndex : 0,
overIndex: overIndex !== -1 ? overIndex : activeIndex,
};
};
const dragEndEvent = (dragItem: DragEndEvent) => {
const { active, over } = dragItem;
if (!active || !over) return; // 處理邊界情況
const moveDataList = [...list];
const { activeIndex, overIndex } = getMoveIndex(moveDataList, dragItem);
if (activeIndex !== overIndex) {
const newDataList = arrayMove(moveDataList, activeIndex, overIndex);
setList(newDataList);
}
};
return (
<DndContext onDragEnd={dragEndEvent} modifiers={[restrictToParentElement]}>
<SortableContext
items={list.map((item) => item.id)}
strategy={rectSortingStrategy}
>
<div className="drag-container">
{list.map((item) => (
<DraggableItem key={item.id} item={item} />
))}
</div>
</SortableContext>
</DndContext>
);
};
export default DraggableList;
對應的 index.scss:
.drag-container {
position: relative;
width: 800px;
display: flex;
flex-wrap: wrap;
gap: 20px;
}
return 的 DOM 元素結構非常簡單,最主要的無外乎兩個上下文組件:DndContext 和 SortableContext。
DndContext:是 dnd-kit 的核心組件,用于提供拖放的上下文。SortableContext:是一個上下文組件,用于提供排序的功能。
在 SortableContext 組件內部包裹的,就是我們正常的需要進行排序的列表容器了。當然,dnd-kit 也不是對任何的內容都可以進行排序的。要想實現排序功能,這個被包裹的 DOM 元素必須符合以下幾個要求:
必須是可排序的元素:
SortableContext需要包裹的元素具有相同的父級容器,且這些元素需要具備可排序的能力。每個子元素應當是獨立的可拖拽項,例如一個列表項、卡片或網格中的塊。提供唯一的
id:每個可排序的子元素必須具有唯一的id。SortableContext會通過這些id來識別和管理每個拖拽項的位置。你需要確保items屬性中提供的id數組與實際渲染的子元素的id一一對應。需要是同一個父容器的直接子元素:
SortableContext內部的子元素必須是同一個父容器的直接子元素,不能有其他中間層級。這是因為排序和拖拽是基于元素的相對位置和布局來計算的。使用相同的布局策略:
SortableContext的子元素應當使用相同的布局策略,例如使用 CSS Flexbox 或 Grid 進行布局。這樣可以確保拖拽操作時,子元素之間的排列和移動邏輯一致。設置相同的樣式屬性:確保子元素具有相同的樣式屬性,例如寬度、高度、邊距等。這些屬性一致性有助于拖拽過程中視覺效果的一致性和準確性。
添加必要的樣式以支持拖拽:為了支持拖拽效果,子元素應具備必要的樣式。例如,設置
position為relative以便于絕對定位的拖拽項,設置overflow以防止拖拽項溢出。確保有足夠的拖拽空間:父容器應當有足夠的空間來允許子元素的拖拽操作。如果空間不足,可能會導致拖拽操作不順暢或無法完成。
子元素必須具備
draggable屬性:每個子元素應該具備draggable屬性,以表明該元素是可拖動的。這通常通過 dnd-kit 提供的組件如Draggable或Sortable來實現。提供合適的拖拽處理程序:為子元素添加合適的拖拽處理程序,通常通過 dnd-kit 提供的鉤子或組件實現。例如,使用
useDraggable鉤子來處理拖拽邏輯。處理子元素布局變化:確保在拖拽過程中,子元素的布局變化能夠被正確處理。例如,設置適當的動畫效果以平滑地更新布局。
在這里附加一個說明,可以看到我初始化的數據的列表 id 是從 1 開始的,因為 從 0 開始會導致第一個元素無法觸發(fā)移動 ?,F階段還不知道是什么原因,大概的猜測是在 JavaScript 和 React 中,
id為0可能會被視為“假值”(falsy value)。許多庫和框架在處理數據時,會有意無意地忽略或處理“假值”。dnd-kit 可能在某些情況下忽略了id為0的元素,導致其無法正常參與拖曳操作。總之, 避免第一個拖曳元素的 id 不要為 0 或者空字符串 。
對于 DndContext,需要傳入幾個 props 以處理拖曳事件本身。在這里,傳入了 onDragEnd 函數與 modifiers 修飾符列表。實際上,這個上下文組件能夠傳入很多的 props,我在這里簡單截個圖:

可以看到,不僅是結束回調,也接受拖曳全過程的函數回調并通過回傳值進行一些數據處理。
但是,一般用于完成拖曳排序功能我們可以不管這么多,只用管鼠標松開后的回調函數,然后拿到對象進行處理就可以了。
onDragEnd:顧名思義,就是用戶鼠標松開后觸發(fā)的拖曳事件的回調。觸發(fā)時會自動傳入類型為DragEndEvent的對象,我們可以從其中拿出active和over兩個參數來具體處理拖曳事件。active 包含 正在拖曳的元素的相關信息,over 包含最后鼠標松開時所覆蓋到的元素的相關信息。
結合到我的函數:
const dragEndEvent = (dragItem: DragEndEvent) => {
const { active, over } = dragItem;
if (!active || !over) return; // 處理邊界情況
const moveDataList = [...list];
const { activeIndex, overIndex } = getMoveIndex(moveDataList, dragItem);
if (activeIndex !== overIndex) {
const newDataList = arrayMove(moveDataList, activeIndex, overIndex);
setList(newDataList);
}
};
首先檢查 active 和 over 是否有效,避免邊界問題,之后創(chuàng)建 moveDataList 的副本,調用 getMoveIndex 函數獲取 active 和 over 項目的索引,如果兩個索引不同,使用 arrayMove 移動項目,并更新 list 狀態(tài)。
getMoveIndex 函數如下,用于獲取拖拽項目和目標位置的索引:
const getMoveIndex = (array: ImgItem[], dragItem: DragMoveEvent) => {
const { active, over } = dragItem;
const activeIndex = array.findIndex((item) => item.id === active.id);
const overIndex = array.findIndex((item) => item.id === over?.id);
// 處理未找到索引的情況
return {
activeIndex: activeIndex !== -1 ? activeIndex : 0,
overIndex: overIndex !== -1 ? overIndex : activeIndex,
};
};
通過
findIndex獲取active和over項目的索引,如果未找到,默認返回 0。modifiers:標識符,傳入一個標識符數組以限制在父組件進行拖曳的行為。主要可選的一些標識符如下:restrictToParentElement:限制在父元素內。restrictToFirstScrollableAncestor:限制在第一個可滾動祖先元素。restrictToVerticalAxis:限制在垂直軸上。restrictToHorizontalAxis:限制在水平軸上。restrictToBoundingRect:限制在指定矩形區(qū)域內。snapCenterToCursor:使元素中心對齊到光標。
在這里我選擇了一個比較普通的限制在父元素內的標識符??梢园凑站唧w的定制需要,配置不同的標識符組合來限制拖曳行為。
接下來是對 SortableContext 的配置解析。在這個組件中傳入了 items 和 strategy 兩個參數。同樣地,它也提供了很多的 props 以供個性化配置:

items:用于定義可排序項目的唯一標識符數組,它告訴 SortableContext 哪些項目可以被拖拽和排序。它的類型剛好和上述的 active 和 over 的 id 屬性的類型相同,都是 UniqueIdentifier。

這也就意味著,我們在 items 這邊傳入了什么數組來對排序列表進行唯一性表示,active 和 over 就按照什么來追蹤元素的排序索引。UniqueIdentifier 實際上是 string 和 number 的聯合類型。

因此,只要是每個 item 唯一的,無論傳字符串或者數字都是可以的。
strategy:策略,用于定義排序算法,它指定了拖拽項目在容器內如何排序和移動。它通過提供一個函數來控制項目在拖拽過程中的排序行為。它決定了拖拽項目的排序方式和在拖拽過程中如何移動。例如,它可以控制項目按行、按列或者自由布局進行排序,并且不同的排序策略可以提供不同的用戶交互體驗。例如,矩形排序、水平排序或者垂直排序等。常用的排序策略有如下幾種:
rectSortingStrategy適用場景:矩形網格布局,比如 flex 容器內部配置
flex-wrap: wrap換行之后,可以采用這種策略。說明:項目根據矩形區(qū)域進行排序,適用于二維網格布局。
horizontalListSortingStrategy適用場景:水平列表,只用于單行的 flex 布局。
說明:項目按水平順序排列,適用于水平滾動的列表。
verticalListSortingStrategy適用場景:垂直列表,只用于單列的 flex 布局,配置了
flex-direction: column之后使用。說明:項目按垂直順序排列,適用于垂直滾動的列表。
除了這幾種以外,你還可以自定義一些策略,按照你自己的需求自己寫。不過一般也用不到自己寫 www
至此,父容器組件介紹完畢,我們來看子元素怎么寫吧。
子元素(Draggable-item)的編寫
上代碼:
import { FC } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import "./index.scss";
type ImgItem = {
id: number;
url: string;
};
type DraggableItemProps = {
item: ImgItem;
};
const DraggableItem: FC<DraggableItemProps> = ({ item }) => {
const { setNodeRef, attributes, listeners, transform, transition } =
useSortable({
id: item.id,
transition: {
duration: 500,
easing: "cubic-bezier(0.25, 1, 0.5, 1)",
},
});
const styles = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
{...attributes}
{...listeners}
style={styles}
className="draggable-item"
>
<span>{item.url}</span>
</div>
);
};
export default DraggableItem;
對應的 index.scss:
.draggable-item {
width: 144px;
height: 144px;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
font-size: large;
cursor: pointer;
user-select: none;
border-radius: 10px;
overflow: hidden;
}
子元素的編寫相較于父容器要簡單得多,需要手動配置的少,引入的包更多了。
首先是引入了 useSortable 這個 hook,主要用來啟用子元素的排序功能。這個鉤子返回了一組現成的屬性和方法:
setNodeRef:用于將 DOM 節(jié)點與拖拽行為關聯。attributes:包含與可拖拽項目相關的屬性,例如role和tabIndex。listeners:包含拖拽操作的事件監(jiān)聽器,例如onMouseDown、onTouchStart。transform:包含當前項目的轉換屬性,用于設置位置和旋轉等。transition:定義項目的過渡效果,用于動畫處理。
它接受一個配置對象,其中包含了:
id:在父容器組件中提到的唯一標識符,需要和父容器中傳入 items 的列表的元素的屬性是一致的,一般直接通過 map 來一次性傳入。transition:動畫效果的配置,包含duration和easing。
之后我們定義了拖曳樣式 styles ,使用了 @dnd-kit/utilities 提供的 CSS 工具庫,用于處理 CSS 相關的樣式轉換,因為這里的 transform 是從 hook 拿到的,是其自定義的 Transform 類型,需要借助其轉為正常的 css 樣式。我們傳入了從 useSortable 中拿到的 transform 和 transition,用于處理拖曳 item 的樣式。
之后就是直接一股腦的將配置全部傳入要真正進行拖曳的 DOM 元素:
return (
<div
ref={setNodeRef}
{...attributes}
{...listeners}
style={styles}
className="draggable-item"
>
<span>{item.url}</span>
</div>
);
};
ref={setNodeRef}:通過setNodeRef將div關聯到拖拽功能。{...attributes}:將所有與可拖拽項目相關的屬性應用到div,例如role="button"和tabIndex="0"。{...listeners}:將所有拖拽操作的事件監(jiān)聽器應用到div,例如onMouseDown和onTouchStart,使其能夠響應用戶的拖拽操作。這里是因為我整個 DOM 元素都要支持拖曳,所以我把它直接加到了最外層。如果需要只在子元素特定的區(qū)域內實現拖曳,listeners 就加到需要真正鼠標拖動的那個 DOM 上即可。style={styles}:應用定義好的styles對象,設置transform和transition樣式,使拖拽時能夠實現平滑過渡。className="draggable-item":設置組件的樣式類名,用于樣式定義。
實現效果
父容器和子元素全都編寫完畢后,我們可以觀察一下總體的實現效果如何:

可以看到,元素已經能夠正常地被排序,而且列表也能夠同樣地被更新。結合到具體的例子,可以把這個列表 item 結合更加復雜的類型進行處理即可。只要保證每個 item 有唯一的 id 即可。
對于原有點擊事件失效的處理
對于某些需要觸發(fā)點擊事件的拖曳 item,如果按照上述方式封裝了拖曳子元素所需的一些配置,那么 原有的點擊事件將會失效,因為原有的鼠標按下的點擊事件被拖曳事件給覆蓋掉了。當然,dnd-kit 肯定也是考慮到了這種情況。他們在其核心庫 @dnd-kit/core 當中封裝了一個 hook useSensors,用來配置 鼠標拖動多少個像素之后才觸發(fā)拖曳事件,在此之前不觸發(fā)拖曳事件。
使用方法也非常簡單,首先從核心庫中導入這個 hook,之后進行如下的配置:
//拖拽傳感器,在移動像素5px范圍內,不觸發(fā)拖拽事件
const sensors = useSensors(
useSensor(MouseSensor, {
activationConstraint: {
distance: 5,
},
})
);
這里配置了在 5px 范圍內不觸發(fā)拖曳事件,這樣就可以在這個范圍內進行點擊事件的正常觸發(fā)了。
在上面的 DndContext 的 props 中,我們也看到了其提供了這一屬性的配置。我們只用將編寫好的 sensors 傳入即可:
<DndContext onDragEnd={dragEndEvent} modifiers={[restrictToParentElement]}>
<SortableContext
items={list.map((item) => item.id)}
strategy={rectSortingStrategy}
sensors={sensors}
>
<div className="drag-container">
{list.map((item) => (
<DraggableItem key={item.id} item={item} />
))}
</div>
</SortableContext>
</DndContext>
以上就是React中使用dnd-kit實現拖曳排序功能的詳細內容,更多關于React dnd-kit拖曳排序的資料請關注腳本之家其它相關文章!
相關文章
react16.8.0以上MobX在hook中的使用方法詳解
這篇文章主要為大家介紹了react16.8.0以上MobX在hook中的使用方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07
react國際化化插件react-i18n-auto使用詳解
這篇文章主要介紹了react國際化化插件react-i18n-auto使用詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-03-03
React?Hooks--useEffect代替常用生命周期函數方式
這篇文章主要介紹了React?Hooks--useEffect代替常用生命周期函數方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09

