react-dnd?API拖拽工具詳細(xì)用法示例
前言
最近公司準(zhǔn)備開發(fā)一個(gè)審批流系統(tǒng),其中會(huì)用到拖拽工具來(lái)搭建流程,關(guān)于拖拽的實(shí)現(xiàn)我們選擇了react-dnd這個(gè)庫(kù),本文總結(jié)了react-dnd API的詳細(xì)用法,并附上不同場(chǎng)景的demo,希望對(duì)大家有用。
概念
React DnD 是一組 React 高階組件,使用的時(shí)候只需要使用對(duì)應(yīng)的 API 將目標(biāo)組件進(jìn)行包裹,即可實(shí)現(xiàn)拖動(dòng)或接受拖動(dòng)元素的功能。
在拖動(dòng)的過(guò)程中,不需要開發(fā)者自己判斷拖動(dòng)狀態(tài),只需要在傳入的 spec 對(duì)象中各個(gè)狀態(tài)屬性中做對(duì)應(yīng)處理即可,因?yàn)閞eact-dnd使用了redux管理自身內(nèi)部的狀態(tài)。
Some of these concepts resemble the Flux and Redux architectures.
This is not a coincidence, as React DnD uses Redux internally.
值得注意的是,react-dnd并不會(huì)改變頁(yè)面的視圖,它只會(huì)改變頁(yè)面元素的數(shù)據(jù)流向,因此它所提供的拖拽效果并不是很炫酷的,我們可能需要寫額外的視圖層來(lái)完成想要的效果,但是這種拖拽管理方式非常的通用,可以在任何場(chǎng)景下使用,非常適合用來(lái)定制。
核心API
介紹實(shí)現(xiàn)拖拽和數(shù)據(jù)流轉(zhuǎn)的核心API,這里以Hook為例。
DndProvider
如果想要使用 React DnD,首先需要在外層元素上加一個(gè) DndProvider。
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
<DndProvider backend={HTML5Backend}>
<TutorialApp />
</DndProvider>
DndProvider 的本質(zhì)是一個(gè)由 React.createContext 創(chuàng)建一個(gè)上下文的容器(組件),用于控制拖拽的行為,數(shù)據(jù)的共享,類似于react-redux的Provider。
Backend
上面我們給DndProvider傳的參數(shù)是一個(gè)backend,那么這里來(lái)解釋一下什么是backend
React DnD 將 DOM 事件相關(guān)的代碼獨(dú)立出來(lái),將拖拽事件轉(zhuǎn)換為 React DnD 內(nèi)部的 redux action。由于拖拽發(fā)生在 H5 的時(shí)候是 ondrag,發(fā)生在移動(dòng)設(shè)備的時(shí)候是由 touch 模擬,React DnD 將這部分單獨(dú)抽出來(lái),方便后續(xù)的擴(kuò)展,這部分就叫做 Backend。它是 DnD 在 Dom 層的實(shí)現(xiàn)。
- react-dnd-html5-backend : 用于控制html5事件的backend
- react-dnd-touch-backend : 用于控制移動(dòng)端touch事件的backend
- react-dnd-test-backend : 用戶可以參考自定義backend
useDrag
讓DOM實(shí)現(xiàn)拖拽能力的構(gòu)子,官方用例如下
import { DragPreviewImage, useDrag } from 'react-dnd';
export const Knight: FC = () => {
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: ItemTypes.KNIGHT,
collect: (monitor) => ({
isDragging: !!monitor.isDragging()
})
}),
[]
);
return (
<>
<DragPreviewImage connect={preview} src={knightImage} />
<div
ref={drag}
>
?
</div>
</>
);
};
useDrag返回三個(gè)參數(shù)
第一個(gè)返回值是一個(gè)對(duì)象 表示關(guān)聯(lián)在拖拽過(guò)程中的變量,需要在傳入useDrag的規(guī)范方法的collect屬性中進(jìn)行映射綁定,比如:isDraging,canDrag等
第二個(gè)返回值 代表拖拽元素的ref
第三個(gè)返回值 代表拖拽元素拖拽后實(shí)際操作到的dom
useDrag傳入兩個(gè)參數(shù)
- 第一個(gè)參數(shù),是一個(gè)對(duì)象,是用于描述了drag的配置信息,常用屬性
type: 指定元素的類型,只有類型相同的元素才能進(jìn)行drop操作
item: 元素在拖拽過(guò)程中,描述該對(duì)象的數(shù)據(jù),如果指定的是一個(gè)方法,則方法會(huì)在開始拖拽時(shí)調(diào)用,并且需要返回一個(gè)對(duì)象來(lái)描述該元素。
end(item, monitor): 拖拽結(jié)束的回調(diào)函數(shù),item表示拖拽物的描述數(shù)據(jù),monitor表示一個(gè) DragTargetMonitor 實(shí)例
isDragging(monitor):判斷元素是否在拖拽過(guò)程中,可以覆蓋Monitor對(duì)象中的isDragging方法,monitor表示一個(gè) DragTargetMonitor 實(shí)例
isDragging: (monitor) => {
return monitor.getItem() ? index === monitor.getItem().index : false;
},
collect: (monitor: any) => ({
//當(dāng)傳入isDragging方法時(shí),monitor.isDragging()方法指代傳入的方法
isDragging: monitor.isDragging(),
}),
canDrag(monitor):判斷是否可以拖拽的方法,需要返回一個(gè)bool值,可以覆蓋Monitor對(duì)象中的canDrag方法,與isDragging同理,monitor表示一個(gè) DragTargetMonitor 實(shí)例
collect:它應(yīng)該返回一個(gè)描述狀態(tài)的普通對(duì)象,然后返回以注入到組件中。它接收兩個(gè)參數(shù),一個(gè) DragTargetMonitor 實(shí)例和拖拽元素描述信息item
- 第二個(gè)參數(shù)是一個(gè)數(shù)組,表示對(duì)方法更新的約束,只有當(dāng)數(shù)組中的參數(shù)發(fā)生改變,才會(huì)重新生成方法,基于react的useMemo實(shí)現(xiàn)
DragSourceMonitor對(duì)象
DragSourceMonitor是傳遞給拖動(dòng)源的收集函數(shù)的對(duì)象。它的方法允許您獲取有關(guān)特定拖動(dòng)源的拖動(dòng)狀態(tài)的信息。 常用的方法: canDrag():描述元素是否可以拖拽,返回一個(gè)bool值
isDragging():判斷元素是否在拖拽過(guò)程中,返回一個(gè)bool值
getItemType():獲取元素的類型,返回一個(gè)bool值
getItem():獲取元素的描述數(shù)據(jù),返回一個(gè)對(duì)象
getDropResult():拖拽結(jié)束,返回拖拽結(jié)果的構(gòu)子,可以拿到從drop元素中返回的數(shù)據(jù)
didDrop(): 拖拽結(jié)束,元素是否放置成功,返回一個(gè)bool值
getDifferenceFromInitialOffset(): 獲取相對(duì)于拖拽起始位置的相對(duì)偏移坐標(biāo)。
useDrop
實(shí)現(xiàn)拖拽物放置的鉤子,官方用例如下
function BoardSquare({ x, y, children }) {
const black = (x + y) % 2 === 1
const [{ isOver }, drop] = useDrop(() => ({
accept: ItemTypes.KNIGHT,
drop: () => moveKnight(x, y),
collect: monitor => ({
isOver: !!monitor.isOver(),
}),
}), [x, y])
return (
<div
ref={drop}
style={{
position: 'relative',
width: '100%',
height: '100%',
}}
>
....
</div>,
)
}
export default BoardSquare
useDrag返回兩個(gè)參數(shù)
- 第一個(gè)返回值是一個(gè)對(duì)象,表示關(guān)聯(lián)在拖拽過(guò)程中的變量,需要在傳入useDrop的規(guī)范方法的collect屬性中進(jìn)行映射綁定
- 第二個(gè)返回值代表放置元素的ref
useDrop傳入一個(gè)參數(shù)
用于描述drop的配置信息,常用屬性
accept: 指定接收元素的類型,只有類型相同的元素才能進(jìn)行drop操作
drop(item, monitor): 有拖拽物放置到元素上觸發(fā)的回調(diào)方法,item表示拖拽物的描述數(shù)據(jù),monitor表示 DropTargetMonitor實(shí)例,該方法返回一個(gè)對(duì)象,對(duì)象的數(shù)據(jù)可以由拖拽物的monitor.getDropResult方法獲得
hover(item, monitor):當(dāng)拖住物在上方hover時(shí)觸發(fā),item表示拖拽物的描述數(shù)據(jù),monitor表示 DropTargetMonitor實(shí)例,返回一個(gè)bool值
canDrop(item, monitor):判斷拖拽物是否可以放置,item表示拖拽物的描述數(shù)據(jù),monitor表示 DropTargetMonitor實(shí)例,返回一個(gè)bool值
DropTargetMonitor對(duì)象
DropTargetMonitor是傳遞給拖放目標(biāo)的收集函數(shù)的對(duì)象。它的方法允許您獲取有關(guān)特定拖放目標(biāo)的拖動(dòng)狀態(tài)的信息。 常用的方法:
canDrop():判斷拖拽物是否可以放置,返回一個(gè)bool值
isOver(options): 拖拽物掠過(guò)元素觸發(fā)的回調(diào)方法,options表示拖拽物的options信息
getItemType():獲取元素的類型,返回一個(gè)bool值
getItem():獲取元素的描述數(shù)據(jù),返回一個(gè)對(duì)象
didDrop(): 拖拽結(jié)束,元素是否放置成功,返回一個(gè)bool值
getDifferenceFromInitialOffset(): 獲取相對(duì)于拖拽起始位置的相對(duì)偏移坐標(biāo)。
數(shù)據(jù)流轉(zhuǎn)
看了API之后,實(shí)際上不能很好的認(rèn)識(shí)到每個(gè)狀態(tài)和每個(gè)方法的工作流程,所以,我這里畫了一張圖,幫助你更清晰的看到它的數(shù)據(jù)是如何流動(dòng)的。

然后我們通過(guò)一個(gè)demo來(lái)更深刻的認(rèn)識(shí)這個(gè)過(guò)程

這里我們定義了幾個(gè)單詞,然后通過(guò)拖拽,將它放入對(duì)應(yīng)的分組里面
單詞代碼
const Word: FC = ({ type, text, id, ...props }: any) => {
const [offsetX, setOffsetX] = useState(0);
const [offsetY, setOffsetY] = useState(0);
const [{ isDragging }, drag]: any = useDrag(() => ({
type,
item: { id, type },
end(item, monitor) {
let top = 0,
left = 0;
if (monitor.didDrop()) {
const dropRes = monitor.getDropResult() as any;
//獲取拖拽對(duì)象所處容器的數(shù)據(jù),獲取坐標(biāo)變化
if (dropRes) {
top = dropRes.top;
left = dropRes.left;
}
//這里必須寫成函數(shù)的傳入方式,否則無(wú)法獲取上一個(gè)state
setOffsetX((offsetX) => offsetX + left);
setOffsetY((offsetY) => offsetY + top);
} else {
// 移出則回到原位
setOffsetX(0);
setOffsetY(0);
}
},
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}));
return ... // dom
);
};
分組代碼
function Classification({ type, title }: any) {
const [{ isOver, canDrop }, drop] = useDrop(
() => ({
accept: type,
drop(_item: any, monitor: any) {
// 獲取每一次放置相對(duì)于上一次的偏移量
const delta = monitor.getDifferenceFromInitialOffset();
const left = Math.round(delta.x);
const top = Math.round(delta.y);
// 回傳給drag
return { top, left };
},
canDrop: (_item, monitor) => {
const item = monitor.getItem() as any;
return item.type === type;
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: !!monitor.canDrop(),
}),
}),
[],
);
return ... // dom
)
完整demo戳鏈接:github.com/AdolescentJ…
拖拽預(yù)覽
受限于瀏覽器API的控制,拖拽元素在開始拖拽之后,只能保持其本身的一個(gè)樣式,不能使用其他樣式預(yù)覽,針對(duì)此,react-dnd為我們提供了兩種解決方法。
DragPreviewImage
react-dnd提供的DragPreviewImage組件,讓我們?cè)谕献У臅r(shí)候,可以以圖片的形式預(yù)覽拖拽的元素
它接收兩個(gè)參數(shù),一個(gè)是useDrag返回的預(yù)覽ref,一個(gè)是需要預(yù)覽的圖片
使用
import { useDrag, DragPreviewImage } from 'react-dnd';
import apple from '../../assets/apple.png';
const DragPreviewImg = () => {
const [{ isDragging }, drag, preview] = useDrag({
type: 'DragDropBox',
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
return (
<>
<DragPreviewImage connect={preview} src={apple} />
<div
className='card_drag'
ref={drag}
style={{
opacity: isDragging ? 0.5 : 1,
}}
>
drag item prview an apple
</div>
</>
);
};
export default DragPreviewImg;
效果

完整demo戳鏈接:github.com/AdolescentJ…
使用圖片來(lái)預(yù)覽拖拽元素確實(shí)可以解決一部分問(wèn)題,但在實(shí)際場(chǎng)景中,拖拽的元素可能會(huì)很多,我們也不能找UI把所有類型的圖都給一張,并且很多比較復(fù)雜的dom圖片是不能取代的
所以要展示更加定制化的預(yù)覽樣式,我們可以使用下面這種。
useDragLayer
useDragLayer是一個(gè)鉤子,它允許你使用dom的方式自定義拖拽元
它的原理是,監(jiān)聽你的拖拽狀態(tài),在拖拽的時(shí)候,可以取到你拖拽狀態(tài)的數(shù)據(jù),并且它會(huì)創(chuàng)建一個(gè)你想預(yù)覽的dom,然后我們可以可以通過(guò)拖拽的狀態(tài)來(lái)改變它的樣式,達(dá)到取代預(yù)覽的效果。
使用
const CustomDragLayer = (props: any) => {
//monitor 是drag的 monitor
const { itemType, isDragging, item, initialOffset, currentOffset } = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(),
}));
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div className='card_drag'>這里是預(yù)覽樣式</div>
</div>
);
}
預(yù)覽

完整demo戳鏈接:github.com/AdolescentJ…
其他使用場(chǎng)景
除了上面的例子,還有非常多的案例
批量拖拽
可以選擇多個(gè)元素進(jìn)行拖拽

拖拽排序
可以拖拽元素放置排序

完整demo戳鏈接:github.com/AdolescentJ…
最后
感謝你能看到這里,本文總結(jié)了react-dnd的API的使用以及常見(jiàn)的場(chǎng)景,希望對(duì)你有所幫助,后續(xù)會(huì)一直更新,當(dāng)然,如果可以的話不妨留一個(gè)贊再走呢。
參考鏈接
react-dnd.github.io/react-dnd/d…
http://www.fzitv.net/article/265220.htm
以上就是react-dnd API拖拽工具詳細(xì)用法示例的詳細(xì)內(nèi)容,更多關(guān)于react-dnd API拖拽工具的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解如何封裝和使用一個(gè)React鑒權(quán)組件
JavaScript?和?React?提供了靈活的事件處理機(jī)制,特別是通過(guò)利用事件的捕獲階段來(lái)阻止事件傳播可以實(shí)現(xiàn)精細(xì)的權(quán)限控制,本文將詳細(xì)介紹這一技術(shù)的應(yīng)用,并通過(guò)實(shí)踐案例展示如何封裝和使用一個(gè)?React?鑒權(quán)組件,需要的可以參考下2024-03-03
使用Webpack打包React項(xiàng)目的流程步驟
隨著React應(yīng)用日益復(fù)雜,開發(fā)者需要借助模塊打包工具來(lái)管理項(xiàng)目依賴、轉(zhuǎn)換代碼和優(yōu)化性能,Webpack是一款功能強(qiáng)大的模塊打包器,它可以將React項(xiàng)目中的JavaScript、CSS、圖片等資源打包成瀏覽器友好的文件,本文將全面介紹如何使用Webpack打包React項(xiàng)目2025-03-03
聊聊ant?design?charts?獲取后端接口數(shù)據(jù)展示問(wèn)題
今天在做項(xiàng)目的時(shí)候遇到幾個(gè)讓我很頭疼的問(wèn)題,一個(gè)是通過(guò)后端接口成功訪問(wèn)并又返回?cái)?shù)據(jù),但拿不到數(shù)據(jù)值。其二是直接修改state中的data,console中數(shù)組發(fā)生變化但任然數(shù)據(jù)未顯示,這篇文章主要介紹了ant?design?charts?獲取后端接口數(shù)據(jù)展示,需要的朋友可以參考下2022-05-05
React如何使用sortablejs實(shí)現(xiàn)拖拽排序
這篇文章主要介紹了React如何使用sortablejs實(shí)現(xiàn)拖拽排序問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01
React實(shí)現(xiàn)多個(gè)場(chǎng)景下鼠標(biāo)跟隨提示框詳解
這篇文章主要為大家介紹了React實(shí)現(xiàn)多個(gè)場(chǎng)景下鼠標(biāo)跟隨提示框詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
React使用Electron開發(fā)桌面端的詳細(xì)流程步驟
React是一個(gè)流行的JavaScript庫(kù),用于構(gòu)建Web應(yīng)用程序,結(jié)合Electron框架,可以輕松地將React應(yīng)用程序打包為桌面應(yīng)用程序,本文詳細(xì)介紹了使用React和Electron開發(fā)桌面應(yīng)用程序的步驟,需要的朋友可以參考下2023-06-06

