在這篇文章中將深入探討如何使用react-dnd,從基礎的拖拽操作到更復雜的自定義功能帶你一步步走向實現流暢、可控且用戶友好的拖拽體驗,無論你是剛接觸拖拽功能的初學者還是想要精細化拖拽交互的經驗開發者,都能從中找到適合自己的靈感和解決方案。
目錄
react-dnd操作
拖拽排序操作
拖拽移動操作
react-beautiful-dnd操作
dnd-kit操作
react-dnd操作
????????react-dnd:一個用于在react應用中實現拖拽和放置功能的庫,它為開發者提供了一套靈活且可擴展的工具使得在react中處理拖拽交互變得簡單且高效,通過react-dnd開發者可以輕松地創建拖拽組件、設置拖拽目標以及處理拖拽過程中各個階段的事件(如開始拖拽、拖拽過程中、放置等),它不僅支持基礎的拖拽功能還允許開發者自定義拖拽行為、指定可放置區域、調整拖拽元素的樣式等,詳情請閱讀官方文檔:地址?,當然也可以去看看源碼:地址?進行學習,接下來我們終端執行如下命令進行安裝react-dnd,以下是使用拖拽的具體步驟:
npm install react-dnd react-dnd-html5-backend
包裹容器:接下來我們開始實現拖拽操作,react-dnd提供一個上下文提供者DndProvider,它用于在應用中啟用拖拽功能并且是實現拖拽交互的基礎,所有需要拖拽行為的組件都必須包裹在DndProvider中
HTML5Backend是react-dnd的一個后端實現,用于處理瀏覽器中的拖拽操作,通過引入react-dnd可以在瀏覽器中啟用標準的拖拽行為,代碼如下所示:
import Drag from './components/drag'
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';const App = () => {return (<DndProvider backend={ HTML5Backend }><Drag></Drag></DndProvider>)
}export default App
拖拽源:接下來我們開始設置拖拽源操作,react-dnd提供了useDrag用來提供對拖拽元素的一些操作,具體的代碼邏輯如下所示:
import { useDrag } from "react-dnd";
import "./index.less";const Draggable = () => {/*** 參數1:返回值是一個對象,主要放一些拖拽物的狀態* 參數2:ref實例,只要將它注入到DOM中該DOM就會變成一個可拖拽的DOM*/const [{ isDragging }, dragRef]: any = useDrag(() => ({type: "box", // 拖拽的類型,要和接收者那邊的類型對應上item: { id: "1" }, // 拖拽的數據,要和接收者那邊的數據對應上collect: (monitor) => ({ // 拖拽過程中的一些狀態,要和接收者那邊的數據對應上isDragging: monitor.isDragging(),}),}));return <div ref={dragRef} className="drag"></div>;
};export default Draggable;
通過如上代碼,調整一下css樣式之后,我們就可以對我們定義的div盒子進行拖拽操作了,如下:
如果想隱藏在拖拽過程中原本為啥的元素,可以通過參數1中的對象來確定是否在拖拽中,如果拖拽的話直接隱藏原本元素就好了,代碼如下:
import { useDrag } from "react-dnd";
import "./index.less";const Draggable = () => {/*** 參數1:返回值是一個對象,主要放一些拖拽物的狀態* 參數2:ref實例,只要將它注入到DOM中該DOM就會變成一個可拖拽的DOM*/const [{ isDragging }, dragRef]: any = useDrag(() => ({type: "box", // 拖拽的類型,要和接收者那邊的類型對應上item: { id: "1" }, // 拖拽的數據,要和接收者那邊的數據對應上collect: (monitor) => ({ // 拖拽過程中的一些狀態,要和接收者那邊的數據對應上isDragging: monitor.isDragging(),}),}));if (isDragging) {return <div ref={dragRef}></div>;}return <div ref={dragRef} className="drag"></div>;
};export default Draggable;
當然我們也可以通過props傳參的方式來實現不同拖拽的id的拖拽源,如下所示:
拖拽區域:接下來我們開始設置拖拽區域操作,react-dnd提供了useDrop用來提供對拖拽區域的一些操作,具體的代碼邏輯如下所示:
import { useDrop } from "react-dnd";
import "./index.less";const Droppable = () => {const [{ isOver }, drop]: any = useDrop(() => ({accept: 'box', // 只接受box類型的數據collect: (monitor) => ({ // 收集器,用來獲取拖拽過程中的一些信息isOver: monitor.isOver() }),drop: (item) => { // 放置事件處理函數console.log(item)}}))return <div ref={drop} className="drop"></div>;
};export default Droppable;
然后接下面我們定義幾個拖拽源,然后將拖拽元素拖拽到拖拽區域里面,可以執行放置時間的處理函數從而打印一下對應的數據,如下所示:
drop這個回調事件可以寫的很復雜這里我們可以將其抽離出去通過props來實現調用,useDrop支持第二個參數[state]作為依賴項,當你通過[state]將狀態傳入useDrop,你實際上是在告訴react當state發生變化時重新執行useDrop鉤子,useDrop就能根據最新的state來決定目標區域的行為或者重新計算是否可以接受拖拽等,如下所示:
import { useDrop } from "react-dnd";
import "./index.less";const Droppable = ({ handleDrop, state, text, children }: any) => {const [{ isOver }, drop]: any = useDrop(() => ({accept: 'box', // 只接受box類型的數據collect: (monitor) => ({ // 收集器,用來獲取拖拽過程中的一些信息isOver: monitor.isOver() }),drop: (item) => handleDrop(item) // 放置事件處理函數}), [state])return <div ref={drop} className="drop">{text}{children}</div>;
};export default Droppable;
拖拽排序操作
????????拖拽排序:說白了就是改變原數據的位置順序,這里我們使用了immutability-helper用于簡化js中不可變數據操作的工具包,它提供一些簡單API來更新嵌套的對象或數組而不會直接修改原數據,這樣可以避免直接變更數據確保數據的不可變性,終端執行如下命令安裝:
npm install immutability-helper
immutability-helper通常用于React中尤其是更新狀態時,幫助避免直接改變state的問題,比如更新嵌套對象或者數組的某個值時immutability-helper會返回一個新的對象而不是修改原對象例如使用它更新數組中的元素:
import update from 'immutability-helper';const state = [1, 2, 3];
const newState = update(state, { 1: { $set: 4 } });console.log(newState); // [1, 4, 3]
接下來我們在拖拽排序中使用,其中useCallback確保了moveCard和 renderCard函數不會在每次組件渲染時被重新創建,這樣避免了不必要的重新渲染和性能開銷。如下所示:
import update from 'immutability-helper'
import { useCallback, useState } from 'react'
import { Card } from './Card'const Index = () => {const [cards, setCards] = useState([{ id: 1, text: 'Write a cool JS library' },{ id: 2, text: 'Make it generic enough' },{ id: 3, text: 'Write README' },{ id: 4, text: 'Create some examples' },{ id: 5, text: 'Spam in Twitter and IRC to promote it (note that this element is taller than the others)' },{ id: 6, text: '???' },{ id: 7, text: 'PROFIT' },])const moveCard = useCallback((dragIndex: number, hoverIndex: number) => {setCards((prevCards: any) => update(prevCards, {$splice: [[dragIndex, 1],[hoverIndex, 0, prevCards[dragIndex]],],}))}, [])const renderCard = useCallback((card: any, index: number) => {return (<Card type='card' key={card.id} index={index} id={card.id} text={card.text} moveCard={moveCard} />)}, [])return (<><div style={{ width: '400px' }}>{cards.map((card, i) => renderCard(card, i))}</div></>)
}export default Index
然后接下來我們將拖拽源和放置源寫在同一個div區域內,這樣標簽內容就既可以拖拽又可以放置,具體代碼如下所示,這里我們通過在div元素設置data-handler-id來唯一地標識該div元素,方便對其進行操作:
import { useRef } from 'react'
import { useDrag, useDrop } from 'react-dnd'const style = {border: '1px dashed gray',padding: '0.5rem 1rem',marginBottom: '.5rem',backgroundColor: 'white',cursor: 'move',
}export const Card = ({ id, text, index, moveCard, type }: any) => {const ref = useRef<HTMLDivElement>(null)const [{ handlerId }, drop] = useDrop({accept: type,collect: (monitor) => ({ // 收集器,用來獲取拖拽過程中的一些信息handlerId: monitor.getHandlerId(), // 設置拖拽源的唯一標識,用來區分不同的拖拽源}),hover(item: any, monitor) {if (!ref.current) returnconst dragIndex = item.index // 拖拽元素的索引位置const hoverIndex = index // 鼠標懸停元素的索引位置if (dragIndex === hoverIndex) returnconst hoverBoundingRect = ref.current?.getBoundingClientRect() // 獲取鼠標懸停元素的邊界信息const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 // 鼠標懸停元素的中點位置const clientOffset: any = monitor.getClientOffset() // 獲取鼠標在頁面中的位置信息const hoverClientY = clientOffset.y - hoverBoundingRect.topif (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return // 如果拖拽元素在鼠標懸停元素的上方,并且鼠標位置在元素的中點下方,則不執行移動操作if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return // 如果拖拽元素在鼠標懸停元素的下方,并且鼠標位置在元素的中點上方,則不執行移動操作moveCard(dragIndex, hoverIndex) // 調用moveCard函數,實現拖拽排序效果item.index = hoverIndex // 更新拖拽元素的索引位置},})const [{ isDragging }, drag] = useDrag({type,item: () => ({ id, index }),collect: (monitor: any) => ({isDragging: monitor.isDragging(),}),})const opacity = isDragging ? 0 : 1drag(drop(ref))return (<div ref={ref} style={{ ...style, opacity }} data-handler-id={handlerId}>{text}</div>)
}
最終呈現的效果如下所示:
拖拽移動操作
????????拖拽移動:說白了就是改變原數據的left和top值,借助依賴項的變化來控制拖拽源不斷改變其位置信息,如下我們定義一個拖拽源并且設置其依賴項的內容,如下所示:
import { useDrag } from 'react-dnd'const style: any = {position: 'absolute',border: '1px dashed gray',backgroundColor: 'white',width: '50px',height: '50px',cursor: 'move',
}
export const Box = ({ id, left, top, children, type }: any) => {const [{ isDragging }, drag]: any = useDrag(() => ({type,item: { id, left, top },collect: (monitor) => ({isDragging: monitor.isDragging(),}),}), [id, left, top])if (isDragging) return <div ref={drag} />return (<div ref={drag} style={{ ...style, left, top }}>{children}</div>)
}
然后我們通過如下代碼來設置其規定只能在范圍內進行移動,超出范圍自動回到原本位置:
import update from 'immutability-helper'
import { useCallback, useState } from 'react'
import { useDrop } from 'react-dnd'
import { Box } from './Box'const styles: any = {width: 300,height: 300,border: '1px solid black',position: 'relative',
}const Index = () => {const [boxes, setBoxes] = useState<any>({a: { top: 20, left: 80, title: 'box1' },b: { top: 180, left: 20, title: 'box2' },})const moveBox = useCallback((id: string, left: number, top: number) => {setBoxes(update(boxes, {[id]: { $merge: { left, top } },}),)}, [boxes, setBoxes])const [, drop]: any = useDrop(() => ({accept: 'box',drop(item: any, monitor) { // 拖拽結束時觸發的事件處理函數const delta: any = monitor.getDifferenceFromInitialOffset() // 獲取鼠標移動的距離let left = Math.round(item.left + delta.x) // 計算新的left值let top = Math.round(item.top + delta.y) // 計算新的top值// 最小和最大邊界限制left = Math.max(1, Math.min(left, 250));top = Math.max(1, Math.min(top, 250));moveBox(item.id, left, top) // 更新box的位置return undefined // 返回undefined,表示拖拽結束},}), [moveBox])return (<div style={{ display: 'flex', margin: '100px', gap: '30px' }}><div ref={drop} style={styles}>{Object.keys(boxes).map((key) => {const { left, top, title } = boxes[key]return (<Box key={key} id={key} left={left} top={top} type='box'>{title}</Box>)})}</div><div>box1: x坐標:{ boxes.a.left } - y坐標:{ boxes.a.top }<br/>box2: x坐標:{ boxes.b.left } - y坐標:{ boxes.b.top }</div></div>)
}export default Index
react-beautiful-dnd操作
????????react-beautiful-dnd:是一個用于react的拖放(drag-and-drop)庫,旨在幫助開發者在react應用中實現漂亮且易于使用的拖放交互,它提供了一個高效流暢且可訪問的拖放體驗,常用于實現類似列表排序、卡片拖動等功能,終端執行如下命令安裝:
npm install react-beautiful-dnd --save
它和react-dnd的區別主要在于其專注于排序方面的內容,優勢如下,缺點就是React-beautiful-dnd 不支持React 高版本和嚴格模式,并且也是好幾年沒有維護了,大家需要根據自身情況選擇是否去使用
1)拖放排序:支持列表項(如任務卡、文件等)的排序,可以拖動列表項改變其順序。
2)跨列拖放:可以在多個列或容器之間拖動元素。
3)響應式:它的設計考慮了響應式和可訪問性,使得即使是在移動設備上或使用鍵盤的用戶也能夠順利使用拖放功能。
4)流暢動畫:提供平滑的動畫效果,使拖放過程更自然和易于理解。
5)高效:優化了性能,能夠在大量元素的情況下依然流暢運行。
我們可以通過訪問 鏈接?來查看其具體的實現案例操作,可以看到拖拽非常的絲滑,右側菜單還提供了各種場景下的案例操作:
接下來我們就寫一個簡單的示例進行演示一下,代碼如下所示:
import { useState, useCallback } from 'react';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';// 設置數組的初始元素
const getItems = (count: any) =>Array.from({ length: count }, (_: any, k) => k).map(k => ({ id: `item-${k}`, content: `item ${k}`}));// 重新排序數組元素
const reorder = (list: any, startIndex: any, endIndex: any) => {const result = Array.from(list);const [removed] = result.splice(startIndex, 1);result.splice(endIndex, 0, removed);return result;
};
const grid = 8;
// 獲取拖拽元素的樣式
const getItemStyle = (isDragging: any, draggableStyle: any) => ({userSelect: 'none',padding: grid * 2,margin: `0 ${grid}px 0 0`,background: isDragging ? 'lightgreen' : 'grey',...draggableStyle,
});
// 獲取列表的樣式
const getListStyle = (isDraggingOver: any) => ({background: isDraggingOver ? 'lightblue' : 'lightgrey',display: 'flex',padding: grid,overflow: 'auto',
});const App = () => {const [items, setItems] = useState(getItems(6));const onDragEnd = useCallback((result: any) => {// 是否拖拽到了其他位置if (!result.destination) return;const reorderedItems: any = reorder(items,result.source.index,result.destination.index);setItems(reorderedItems);}, [items]);return (<DragDropContext onDragEnd={onDragEnd}><Droppable droppableId="droppable" direction="horizontal">{(provided: any, snapshot: any) => (<div ref={provided.innerRef} style={getListStyle(snapshot.isDraggingOver)} {...provided.droppableProps}>{items.map((item, index) => (<Draggable key={item.id} draggableId={item.id} index={index}>{(provided: any, snapshot: any) => (<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}style={getItemStyle( snapshot.isDragging, provided.draggableProps.style)}>{item.content}</div>)}</Draggable>))}{provided.placeholder}</div>)}</Droppable></DragDropContext>);
};export default App;
dnd-kit操作
??????? dnd-kit: 是一個用于實現拖放(drag-and-drop)交互的react庫,它提供了一組高效靈活的API使開發者能夠輕松構建具有拖放功能的應用,通過在瀏覽器中直接操作DOM元素并處理拖動放置以及元素重排的過程,使得用戶能夠在界面中拖動元素動態地改變其位置或順序,終端執行如下命令安裝:
npm install @dnd-kit/core
其主要優勢如下所示:
1)簡化拖放功能:封裝了拖放的核心邏輯開發者無需從頭開始編寫復雜的拖放機制
2)高度自定義:提供了豐富的API開發者可以自定義拖動行為、動畫效果、邊界限制、拖動過程中元素的樣式等
3)支持觸摸屏和桌面設備:同時支持鼠標和觸摸事件,適應不同設備。
4)性能優化:設計注重性能,通過高效的狀態管理和渲染機制保證即使在復雜場景下也能流暢運行。
我們可以通過訪問 鏈接?來查看其具體的實現案例操作,可以看到拖拽非常的絲滑,右側菜單還提供了各種場景下的案例操作:
接下來我們就寫一個簡單的示例進行演示一下,代碼如下所示:
import { useState } from 'react'
import { DndContext, useDroppable, useDraggable } from "@dnd-kit/core";// 設置放置和拖拽組件
const Droppable = (props: any) => {const {isOver, setNodeRef} = useDroppable({ id: props.id });return (<div ref={setNodeRef}>{props.children}</div>);
}
const Draggable = (props: any) => {const {attributes, listeners, setNodeRef, transform} = useDraggable({ id: props.id });const style = transform ? {transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,} : undefined;return (<div ref={setNodeRef} style={style} {...listeners} {...attributes}>{props.children}</div>);
}
export default function Index() {const containers = ['A', 'B', 'C','D','E'];const [parent, setParent] = useState(null);const draggableMarkup = (<Draggable id="draggable"><div style={{width:100,height:100,background:'pink',cursor:'move'}}>可拖拽組件</div></Draggable>);const handleDragEnd=(event: any)=> {const {over} = event;setParent(over ? over.id : null);}return (<DndContext onDragEnd={handleDragEnd}><div style={{display:'flex',justifyContent: 'space-between',paddingTop:50}}>{containers.map((id) => (<Droppable key={id} id={id}><div style={{width:200,height:200,border:'1px solid #000'}}>{parent === id ? draggableMarkup : '放置源'}</div></Droppable>))}</div>{parent === null ? draggableMarkup : null}</DndContext>);
}