React中使用dnd-kit實(shí)現(xiàn)拖曳排序功能
由于前陣子需要在開(kāi)發(fā) Picals 的時(shí)候,需要實(shí)現(xiàn)一些拖動(dòng)排序的功能。雖然有原生的瀏覽器 dragger API,不過(guò)純靠自己手寫(xiě)很難實(shí)現(xiàn)自己想要的效果,更多的是吃力不討好。于是我四處去調(diào)研了一些 React 中比較常用的拖曳庫(kù),最終確定了 dnd-kit 作為我實(shí)現(xiàn)拖曳排序的工具。
當(dāng)然,使用的時(shí)候肯定免不了踩坑。這篇文章的意義就是為了記錄所踩的坑,希望能夠?yàn)橛行枰拇蠹姨峁┮稽c(diǎn)幫助。
在這篇文章中,我將帶著大家一起實(shí)現(xiàn)如下的拖曳排序的例子:

那讓我們開(kāi)始吧。
安裝
安裝 dnd-kit 工具庫(kù)很簡(jiǎn)單,只需要輸入下面的命令進(jìn)行安裝即可:
pnpm add @dnd-kit/core @dnd-kit/sortable @dnd-kit/modifiers @dnd-kit/utilities
這幾個(gè)包分別有什么作用呢?
@dnd-kit/core:核心庫(kù),提供基本的拖拽功能。@dnd-kit/sortable:擴(kuò)展庫(kù),提供排序功能和工具。@dnd-kit/modifiers:修飾庫(kù),提供拖拽行為的限制和修飾功能。@dnd-kit/utilities:工具庫(kù),提供 CSS 和實(shí)用工具函數(shù)。上述演示的平滑移動(dòng)的樣式就是來(lái)源于這個(gè)包。
使用方法
首先我們需要知道的是,拖曳這個(gè)行為需要涉及到兩個(gè)部分:
- 能夠允許被拖曳的有限空間(父容器)
- 用戶(hù)真正進(jìn)行拖曳的子元素
在使用 dnd-kit 時(shí),需要對(duì)這兩個(gè)部分分別進(jìn)行定義。
父容器(DraggableList)的編寫(xiě)
我們首先進(jìn)行拖曳父容器相關(guān)的功能配置。話不多說(shuō)我們直接上代碼:
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;
對(duì)應(yīng)的 index.scss:
.drag-container {
position: relative;
width: 800px;
display: flex;
flex-wrap: wrap;
gap: 20px;
}
return 的 DOM 元素結(jié)構(gòu)非常簡(jiǎn)單,最主要的無(wú)外乎兩個(gè)上下文組件:DndContext 和 SortableContext。
DndContext:是 dnd-kit 的核心組件,用于提供拖放的上下文。SortableContext:是一個(gè)上下文組件,用于提供排序的功能。
在 SortableContext 組件內(nèi)部包裹的,就是我們正常的需要進(jìn)行排序的列表容器了。當(dāng)然,dnd-kit 也不是對(duì)任何的內(nèi)容都可以進(jìn)行排序的。要想實(shí)現(xiàn)排序功能,這個(gè)被包裹的 DOM 元素必須符合以下幾個(gè)要求:
必須是可排序的元素:
SortableContext需要包裹的元素具有相同的父級(jí)容器,且這些元素需要具備可排序的能力。每個(gè)子元素應(yīng)當(dāng)是獨(dú)立的可拖拽項(xiàng),例如一個(gè)列表項(xiàng)、卡片或網(wǎng)格中的塊。提供唯一的
id:每個(gè)可排序的子元素必須具有唯一的id。SortableContext會(huì)通過(guò)這些id來(lái)識(shí)別和管理每個(gè)拖拽項(xiàng)的位置。你需要確保items屬性中提供的id數(shù)組與實(shí)際渲染的子元素的id一一對(duì)應(yīng)。需要是同一個(gè)父容器的直接子元素:
SortableContext內(nèi)部的子元素必須是同一個(gè)父容器的直接子元素,不能有其他中間層級(jí)。這是因?yàn)榕判蚝屯献腔谠氐南鄬?duì)位置和布局來(lái)計(jì)算的。使用相同的布局策略:
SortableContext的子元素應(yīng)當(dāng)使用相同的布局策略,例如使用 CSS Flexbox 或 Grid 進(jìn)行布局。這樣可以確保拖拽操作時(shí),子元素之間的排列和移動(dòng)邏輯一致。設(shè)置相同的樣式屬性:確保子元素具有相同的樣式屬性,例如寬度、高度、邊距等。這些屬性一致性有助于拖拽過(guò)程中視覺(jué)效果的一致性和準(zhǔn)確性。
添加必要的樣式以支持拖拽:為了支持拖拽效果,子元素應(yīng)具備必要的樣式。例如,設(shè)置
position為relative以便于絕對(duì)定位的拖拽項(xiàng),設(shè)置overflow以防止拖拽項(xiàng)溢出。確保有足夠的拖拽空間:父容器應(yīng)當(dāng)有足夠的空間來(lái)允許子元素的拖拽操作。如果空間不足,可能會(huì)導(dǎo)致拖拽操作不順暢或無(wú)法完成。
子元素必須具備
draggable屬性:每個(gè)子元素應(yīng)該具備draggable屬性,以表明該元素是可拖動(dòng)的。這通常通過(guò) dnd-kit 提供的組件如Draggable或Sortable來(lái)實(shí)現(xiàn)。提供合適的拖拽處理程序:為子元素添加合適的拖拽處理程序,通常通過(guò) dnd-kit 提供的鉤子或組件實(shí)現(xiàn)。例如,使用
useDraggable鉤子來(lái)處理拖拽邏輯。處理子元素布局變化:確保在拖拽過(guò)程中,子元素的布局變化能夠被正確處理。例如,設(shè)置適當(dāng)?shù)膭?dòng)畫(huà)效果以平滑地更新布局。
在這里附加一個(gè)說(shuō)明,可以看到我初始化的數(shù)據(jù)的列表 id 是從 1 開(kāi)始的,因?yàn)?從 0 開(kāi)始會(huì)導(dǎo)致第一個(gè)元素?zé)o法觸發(fā)移動(dòng) 。現(xiàn)階段還不知道是什么原因,大概的猜測(cè)是在 JavaScript 和 React 中,
id為0可能會(huì)被視為“假值”(falsy value)。許多庫(kù)和框架在處理數(shù)據(jù)時(shí),會(huì)有意無(wú)意地忽略或處理“假值”。dnd-kit 可能在某些情況下忽略了id為0的元素,導(dǎo)致其無(wú)法正常參與拖曳操作??傊?避免第一個(gè)拖曳元素的 id 不要為 0 或者空字符串 。
對(duì)于 DndContext,需要傳入幾個(gè) props 以處理拖曳事件本身。在這里,傳入了 onDragEnd 函數(shù)與 modifiers 修飾符列表。實(shí)際上,這個(gè)上下文組件能夠傳入很多的 props,我在這里簡(jiǎn)單截個(gè)圖:

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

items:用于定義可排序項(xiàng)目的唯一標(biāo)識(shí)符數(shù)組,它告訴 SortableContext 哪些項(xiàng)目可以被拖拽和排序。它的類(lèi)型剛好和上述的 active 和 over 的 id 屬性的類(lèi)型相同,都是 UniqueIdentifier。

這也就意味著,我們?cè)?items 這邊傳入了什么數(shù)組來(lái)對(duì)排序列表進(jìn)行唯一性表示,active 和 over 就按照什么來(lái)追蹤元素的排序索引。UniqueIdentifier 實(shí)際上是 string 和 number 的聯(lián)合類(lèi)型。

因此,只要是每個(gè) item 唯一的,無(wú)論傳字符串或者數(shù)字都是可以的。
strategy:策略,用于定義排序算法,它指定了拖拽項(xiàng)目在容器內(nèi)如何排序和移動(dòng)。它通過(guò)提供一個(gè)函數(shù)來(lái)控制項(xiàng)目在拖拽過(guò)程中的排序行為。它決定了拖拽項(xiàng)目的排序方式和在拖拽過(guò)程中如何移動(dòng)。例如,它可以控制項(xiàng)目按行、按列或者自由布局進(jìn)行排序,并且不同的排序策略可以提供不同的用戶(hù)交互體驗(yàn)。例如,矩形排序、水平排序或者垂直排序等。常用的排序策略有如下幾種:
rectSortingStrategy適用場(chǎng)景:矩形網(wǎng)格布局,比如 flex 容器內(nèi)部配置
flex-wrap: wrap換行之后,可以采用這種策略。說(shuō)明:項(xiàng)目根據(jù)矩形區(qū)域進(jìn)行排序,適用于二維網(wǎng)格布局。
horizontalListSortingStrategy適用場(chǎng)景:水平列表,只用于單行的 flex 布局。
說(shuō)明:項(xiàng)目按水平順序排列,適用于水平滾動(dòng)的列表。
verticalListSortingStrategy適用場(chǎng)景:垂直列表,只用于單列的 flex 布局,配置了
flex-direction: column之后使用。說(shuō)明:項(xiàng)目按垂直順序排列,適用于垂直滾動(dòng)的列表。
除了這幾種以外,你還可以自定義一些策略,按照你自己的需求自己寫(xiě)。不過(guò)一般也用不到自己寫(xiě) www
至此,父容器組件介紹完畢,我們來(lái)看子元素怎么寫(xiě)吧。
子元素(Draggable-item)的編寫(xiě)
上代碼:
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;
對(duì)應(yīng)的 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;
}
子元素的編寫(xiě)相較于父容器要簡(jiǎn)單得多,需要手動(dòng)配置的少,引入的包更多了。
首先是引入了 useSortable 這個(gè) hook,主要用來(lái)啟用子元素的排序功能。這個(gè)鉤子返回了一組現(xiàn)成的屬性和方法:
setNodeRef:用于將 DOM 節(jié)點(diǎn)與拖拽行為關(guān)聯(lián)。attributes:包含與可拖拽項(xiàng)目相關(guān)的屬性,例如role和tabIndex。listeners:包含拖拽操作的事件監(jiān)聽(tīng)器,例如onMouseDown、onTouchStart。transform:包含當(dāng)前項(xiàng)目的轉(zhuǎn)換屬性,用于設(shè)置位置和旋轉(zhuǎn)等。transition:定義項(xiàng)目的過(guò)渡效果,用于動(dòng)畫(huà)處理。
它接受一個(gè)配置對(duì)象,其中包含了:
id:在父容器組件中提到的唯一標(biāo)識(shí)符,需要和父容器中傳入 items 的列表的元素的屬性是一致的,一般直接通過(guò) map 來(lái)一次性傳入。transition:動(dòng)畫(huà)效果的配置,包含duration和easing。
之后我們定義了拖曳樣式 styles ,使用了 @dnd-kit/utilities 提供的 CSS 工具庫(kù),用于處理 CSS 相關(guān)的樣式轉(zhuǎn)換,因?yàn)檫@里的 transform 是從 hook 拿到的,是其自定義的 Transform 類(lèi)型,需要借助其轉(zhuǎn)為正常的 css 樣式。我們傳入了從 useSortable 中拿到的 transform 和 transition,用于處理拖曳 item 的樣式。
之后就是直接一股腦的將配置全部傳入要真正進(jìn)行拖曳的 DOM 元素:
return (
<div
ref={setNodeRef}
{...attributes}
{...listeners}
style={styles}
className="draggable-item"
>
<span>{item.url}</span>
</div>
);
};
ref={setNodeRef}:通過(guò)setNodeRef將div關(guān)聯(lián)到拖拽功能。{...attributes}:將所有與可拖拽項(xiàng)目相關(guān)的屬性應(yīng)用到div,例如role="button"和tabIndex="0"。{...listeners}:將所有拖拽操作的事件監(jiān)聽(tīng)器應(yīng)用到div,例如onMouseDown和onTouchStart,使其能夠響應(yīng)用戶(hù)的拖拽操作。這里是因?yàn)槲艺麄€(gè) DOM 元素都要支持拖曳,所以我把它直接加到了最外層。如果需要只在子元素特定的區(qū)域內(nèi)實(shí)現(xiàn)拖曳,listeners 就加到需要真正鼠標(biāo)拖動(dòng)的那個(gè) DOM 上即可。style={styles}:應(yīng)用定義好的styles對(duì)象,設(shè)置transform和transition樣式,使拖拽時(shí)能夠?qū)崿F(xiàn)平滑過(guò)渡。className="draggable-item":設(shè)置組件的樣式類(lèi)名,用于樣式定義。
實(shí)現(xiàn)效果
父容器和子元素全都編寫(xiě)完畢后,我們可以觀察一下總體的實(shí)現(xiàn)效果如何:

可以看到,元素已經(jīng)能夠正常地被排序,而且列表也能夠同樣地被更新。結(jié)合到具體的例子,可以把這個(gè)列表 item 結(jié)合更加復(fù)雜的類(lèi)型進(jìn)行處理即可。只要保證每個(gè) item 有唯一的 id 即可。
對(duì)于原有點(diǎn)擊事件失效的處理
對(duì)于某些需要觸發(fā)點(diǎn)擊事件的拖曳 item,如果按照上述方式封裝了拖曳子元素所需的一些配置,那么 原有的點(diǎn)擊事件將會(huì)失效,因?yàn)樵械氖髽?biāo)按下的點(diǎn)擊事件被拖曳事件給覆蓋掉了。當(dāng)然,dnd-kit 肯定也是考慮到了這種情況。他們?cè)谄浜诵膸?kù) @dnd-kit/core 當(dāng)中封裝了一個(gè) hook useSensors,用來(lái)配置 鼠標(biāo)拖動(dòng)多少個(gè)像素之后才觸發(fā)拖曳事件,在此之前不觸發(fā)拖曳事件。
使用方法也非常簡(jiǎn)單,首先從核心庫(kù)中導(dǎo)入這個(gè) hook,之后進(jìn)行如下的配置:
//拖拽傳感器,在移動(dòng)像素5px范圍內(nèi),不觸發(fā)拖拽事件
const sensors = useSensors(
useSensor(MouseSensor, {
activationConstraint: {
distance: 5,
},
})
);
這里配置了在 5px 范圍內(nèi)不觸發(fā)拖曳事件,這樣就可以在這個(gè)范圍內(nèi)進(jìn)行點(diǎn)擊事件的正常觸發(fā)了。
在上面的 DndContext 的 props 中,我們也看到了其提供了這一屬性的配置。我們只用將編寫(xiě)好的 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實(shí)現(xiàn)拖曳排序功能的詳細(xì)內(nèi)容,更多關(guān)于React dnd-kit拖曳排序的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react16.8.0以上MobX在hook中的使用方法詳解
這篇文章主要為大家介紹了react16.8.0以上MobX在hook中的使用方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
react國(guó)際化化插件react-i18n-auto使用詳解
這篇文章主要介紹了react國(guó)際化化插件react-i18n-auto使用詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
React?Hooks--useEffect代替常用生命周期函數(shù)方式
這篇文章主要介紹了React?Hooks--useEffect代替常用生命周期函數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09
react項(xiàng)目如何運(yùn)行在微信公眾號(hào)
這篇文章主要介紹了react項(xiàng)目如何運(yùn)行在微信公眾號(hào),幫助大家更好的理解和學(xué)習(xí)使用react,感興趣的朋友可以了解下2021-04-04
React不能將useMemo設(shè)置為默認(rèn)方法原因詳解
這篇文章主要為大家介紹了React不能將useMemo設(shè)置為默認(rèn)方法原因詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2022-07-07
從頭寫(xiě)React-like框架的工程搭建實(shí)現(xiàn)
這篇文章主要介紹了從頭寫(xiě)React-like框架的工程搭建實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04

