本文詳細講解了 react-dnd 的 API 以及用法,並且附上了可供參考的 Demo,希望能夠給需要的朋友提供一下幫助。
一、概念
React DnD 是一組 React 高階元件,使用的時候只需要使用對應的 API 將目標元件進行包裹,即可實現拖動或接受拖動元素的功能。將拖動的事件轉換成物件中對應狀態的形式,不需要開發者自己判斷拖動狀態,只需要在傳入的 spec 物件中各個狀態屬性中做對應處理即可。剛剛接觸可能難以理解,真正熟悉用法之後會感覺很方便。
本文
Demo
地址:react-dnd-dustbin。如有幫助,歡迎 Star。
二、DragSource:使元件能夠被拖拽
使用
DragSource
包裹住元件,使其可以進行拖動。
使用方式
import React, { Component } from 'react';
import { DragSource } from 'react-dnd';
const spec = {
beginDrag(props, monitor, component) {
// 這裡 return 出去的物件屬性自行選擇,這裡只是用 id 作為演示
return { id: props.id }
}
endDrag(props, monitor, component) {
...
}
canDrag(props, monitor) {
...
}
isDragging(props, monitor) {
...
}
}
const collect = (connect, monitor) => ({
// 這裡返回一個物件,會將物件的屬性都賦到元件的 props 中去。這些屬性需要自己定義。
connectDropTarget: connect.dropTarget(),
id: monitor.getItem().id
})
@DragSource(type, spec, collect)
class MyComponent extends Component {
/* ... */
}
export default MyComponent;
複製程式碼
引數講解:
- type: 必填。字串,ES6符號或返回給定元件的函式props。只有為相同型別註冊的
drop targets
才會對此拖動源生成的專案做出反應 - spec:必填。一個普通的JavaScript物件,上面有一些允許的方法。它描述了拖動源如何對拖放事件做出反應。
- collect:必填。收集功能。它應該返回一個普通的物件注入你的元件。它接收兩個引數:connect和monitor。
- options:可選的。一個普通的物件。
spec 物件中的方法
-
beginDrag(props, monitor, component)
:必填。當拖動開始時,beginDrag
被呼叫。您必須返回描述被拖動資料的純JavaScript
物件。您返回的內容會被放置到monitor.getItem()
獲取到的物件中。 -
endDrag(props, monitor, component)
:可選的。當拖動停止時,endDrag
被呼叫。對於每個beginDrag
,endDrag
都會對應。 -
canDrag(props, monitor)
: 可選的。用它來指定當前是否允許拖動。如果您想要始終允許它,只需省略此方法即可。注意:您可能無法呼叫monitor.canDrag()
此方法。 -
isDragging(props, monitor)
: 可選的。預設情況下,僅啟動拖動操作的拖動源被視為拖動。注意:您可能無法呼叫monitor.isDragging()
此方法。
方法中的引數 props, monitor, component
props
:當前元件的props
monitor
:一個DragSourceMonitor
例項。使用它來查詢有關當前拖動狀態的資訊,例如當前拖動的專案及其型別,當前和初始座標和偏移,以及它是否已被刪除。component
:指定時,它是元件的例項。使用它來訪問底層DOM節點以進行位置或大小測量,或呼叫setState
以及其他元件方法。isDragging
、canDrag
方法裡獲取不到component
這個引數,因為它們被呼叫時例項可能不可用
collect 中的 connect 和 monitor 引數
-
connect
: 一個DragSourceConnector
例項。它有兩種方法:dragPreview()和dragSource()。- dragSource() => (elementOrNode, options?):常用方法,返回一個函式,傳遞給元件用來將 source DOM 和 React DnD Backend 連線起來
- dragPreview():返回一個函式,傳遞給元件用來將拖動時預覽的 DOM 節點 和 React DnD Backend 連線起來
- dragSource() => (elementOrNode, options?):常用方法,返回一個函式,傳遞給元件用來將 source DOM 和 React DnD Backend 連線起來
-
monitor:一個
DragSourceMonitor
例項。包含下面各種方法:
方法 | 含義 |
---|---|
canDrag() |
是否可以被拖拽。如果沒有正在進行拖動操作,則返回 true |
isDragging() |
是否正在被拖動。如果正在進行拖動操作,則返回 true |
getItemType() |
返回標識當前拖動項的型別的字串或ES6符號。 如果沒有拖動專案,則返回 null |
getItem() |
返回表示當前拖動項的普通物件。 每個拖動源都必須通過從其beginDrag()方法返回一個物件來指定它。 如果沒有拖動專案,則返回 null |
getDropResult() |
返回表示最後記錄的放置 drop result 物件 |
didDrop() |
如果某個 drop target 處理了 drop 事件,則返回 true,否則返回 false。即使 target 沒有返回 drop 結果,didDrop() 也會返回true。 在 endDrag() 中使用它來測試任何放置目標是否已處理掉落。 如果在 endDrag() 之外呼叫,則返回 false |
getInitialClientOffset() |
返回當前拖動操作開始時指標的{x,y} client 偏移量。 如果沒有拖動專案,則返回 null |
getInitialSourceClientOffset() |
返回當前拖動操作開始時 drag source 元件的根DOM節點的{x,y}client 偏移量。 如果沒有拖動專案,則返回 null |
getClientOffset() |
拖動操作正在進行時,返回指標的最後記錄的{x,y}client 偏移量。 如果沒有拖動專案,則返回 null |
getDifferenceFromInitialOffset() |
返回當前拖動操作開始時滑鼠的最後記錄 client 偏移量與 client 偏移量之間的{x,y}差異。 如果沒有拖動專案,則返回 null |
getSourceClientOffset() |
返回 drag source 元件的根DOM節點的預計{x,y} client 偏移量,基於其在當前拖動操作開始時的位置以及移動差異。 如果沒有拖動專案,則返回 null |
三、DropTarget:使元件能夠放置拖拽元件
使用
DropTarget
包裹住元件,使其對拖動,懸停或 dropped 的相容專案做出反應。
使用方式
import React, { Component } from 'react';
import { DropTarget } from 'react-dnd';
const spec = {
drop(props, monitor, component) {
// 這裡 return 出去的物件屬性自行選擇,這裡只是用 id 作為演示
return { id: props.id }
}
hover(props, monitor, component) {
...
}
canDrop(props, monitor) {
...
}
}
const collect = (connect, monitor) => ({
// 這裡返回一個物件,會將物件的屬性都賦到元件的 props 中去。這些屬性需要自己定義。
connectDropTarget: connect.dropTarget()
})
@DropTarget(type, spec, collect)
class MyComponent extends Component {
/* ... */
}
export default MyComponent;
複製程式碼
引數講解:
- type: 必填。字串,ES6符號或返回給定元件的函式props。此放置目標僅對指定型別的
drag sources
專案做出反應 - spec:必填。一個普通的JavaScript物件,上面有一些允許的方法。它描述了放置目標如何對拖放事件做出反應。
- collect:必填。收集功能。它應該返回一個普通的道具物件注入你的元件。它接收兩個引數:connect 和 monitor。
- options:可選的。一個普通的物件。
spec 物件中的方法
-
drop(props, monitor, component)
: 可選的。在目標上放置相容專案時呼叫。可以返回undefined
或普通物件。如果返回一個物件,它將成為放置結果,可以使用monitor.getDropResult()
獲取到。 -
hover(props, monitor, component)
: 可選的。當專案懸停在元件上時呼叫。您可以檢查monitor.isOver({ shallow: true })
以測試懸停是僅發生在當前目標上還是巢狀上。 -
canDrop(props, monitor)
: 可選的。使用它來指定放置目標是否能夠接受該專案。如果想要始終允許它,只需省略此方法即可。
文件沒有提供按目的處理進入或離開事件的方法。而是
monitor.isOver()
從收集函式返回撥用結果,以便我們可以使用componentDidUpdateReact
鉤子函式來處理元件中的進入和離開事件。
方法中的引數 props, monitor, component
props
:當前元件的props
monitor
:一個DropTargetMonitor
例項。使用它來查詢有關當前拖動狀態的資訊,例如當前拖動的專案及其型別,當前和初始座標和偏移,是否超過當前目標,以及是否可以刪除它。component
:指定時,它是元件的例項。使用它來訪問底層DOM節點以進行位置或大小測量,或呼叫setState
以及其他元件方法。canDrag
方法裡獲取不到component
這個引數,因為它們被呼叫時例項可能不可用。
collect 中的 connect 和 monitor 引數
-
connect
: 一個DropTargetConnector
例項。它只有一種dropTarget()
方法。dropTarget() => (elementOrNode)
:常用方法,返回一個函式,傳遞給元件用來將 target DOM 和 React DnD Backend 連線起來。通過{ connectDropTarget: connect.dropTarget() }從收集函式返回,可以將任何React元素標記為可放置節點。
-
monitor:一個
DropTargetMonitor
例項。包含下面各種方法:
方法 | 含義 |
---|---|
canDrop() |
是否可以被放置。如果正在進行拖動操作,則返回true |
isOver(options) |
drag source 是否懸停在 drop target 區域。可以選擇傳遞{ shallow: true } 以嚴格檢查是否只有 drag source 懸停,而不是巢狀目標 |
getItemType() |
返回標識當前拖動項的型別的字串或ES6符號。如果沒有拖動專案則返回 null |
getItem() |
返回表示當前拖動項的普通物件,每個拖動源都必須通過從其beginDrag()方法返回一個物件來指定它。如果沒有拖動專案則返回 null |
getDropResult() |
返回表示最後記錄的放置 drop result 物件 |
didDrop() |
如果某個 drop target 處理了 drop 事件,則返回 true,否則返回 false。即使 target 沒有返回 drop 結果,didDrop() 也會返回true。 在 endDrag() 中使用它來測試任何放置目標是否已處理掉落。 如果在 endDrag() 之外呼叫,則返回 false |
getInitialClientOffset() |
返回當前拖動操作開始時指標的{x,y} client 偏移量。 如果沒有拖動專案,則返回 null |
getInitialSourceClientOffset() |
返回當前拖動操作開始時 drag source 元件的根DOM節點的{x,y}client 偏移量。 如果沒有拖動專案,則返回 null |
getClientOffset() |
拖動操作正在進行時,返回指標的最後記錄的{x,y}client 偏移量。 如果沒有拖動專案,則返回 null |
getDifferenceFromInitialOffset() |
返回當前拖動操作開始時滑鼠的最後記錄 client 偏移量與 client 偏移量之間的{x,y}差異。 如果沒有拖動專案,則返回 null |
getSourceClientOffset() |
返回 drag source 元件的根DOM節點的預計{x,y} client 偏移量,基於其在當前拖動操作開始時的位置以及移動差異。 如果沒有拖動專案,則返回 null |
四、DragDropContext & DragDropContextProvider
注意: 使用 DragSource 和 DropTarget 包裹的元件,必須放在: DragDropContext 包裹的根元件內部,或者 DragDropContextProvider 根標籤的內部。
DragDropContext
使用 DragDropContext
包裝應用程式的根元件以啟用 React DnD。
用法
import React, { Component } from 'react';
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';
@DragDropContext(HTML5Backend)
class YourApp extends Component {
/* ... */
}
export default YourApp;
複製程式碼
引數
-
backend:必填。一個 React DnD 後端。除非您正在編寫自定義的,否則建議使用 React DnD 附帶的 HTML5Backend。
-
context:backend 依賴。用於自定義後端的上下文物件。例如,HTML5Backend可以為iframe場景注入自定義視窗物件。
DragDropContextProvider
作為 DragDropContext
的替代方法,您可以使用 DragDropContextProvider
元素為應用程式啟用React DnD。與 DragDropContext
類似,這可以通過 backendprop
注入後端,但也可以注入一個 window
物件。
用法
import React, { Component } from 'react';
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContextProvider } from 'react-dnd';
export default class YourApp extends Component {
render() {
return (
<DragDropContextProvider backend={HTML5Backend}>
/* ... */
</DragDropContextProvider>
)
}
}
複製程式碼
引數
-
backend:必填。一個 React DnD 後端。除非您正在編寫自定義的,否則建議使用 React DnD 附帶的 HTML5Backend。
-
context:backend 依賴。用於自定義後端的上下文物件。例如,HTML5Backend可以為iframe場景注入自定義視窗物件。
五、react-dnd 的簡單示例
本示例參照官方的 Dustbin 示例進行講解。
專案準備
當前專案使用 create-react-app
腳手架進行搭建,而且使用 react-dnd
時都是使用裝飾器語法進行編寫。所以需要先在專案裡新增一些配置。
啟用裝飾器的配置方式可以參考我的上一篇文章:在 create-react-app 中啟用裝飾器語法。
新建 components
資料夾,用來存放編寫的元件。新建 types
資料夾,用來存放 type
字串常量,在 types
目錄下建立 index.js
檔案宣告對應的 type
值。
types/index.js
export default {
BOX: 'box'
}
複製程式碼
所以當前專案 src
目錄下檔案結構如下:
src
├── components/
├── types/
└── index.js
├── App.js
├── index.css
└── index.js
複製程式碼
建立 Box 元件,作為 DragSource
在 components
目錄下,建立 Box.js
檔案,編寫 Box
元件,使其可以進行拖動
components/Box.js
import React from 'react';
import PropTypes from 'prop-types';
import { DragSource } from 'react-dnd';
import ItemTypes from '../types';
const style = {
border: '1px dashed gray',
backgroundColor: 'white',
padding: '0.5rem 1rem',
marginRight: '1.5rem',
marginBottom: '1.5rem',
cursor: 'move',
float: 'left',
}
const boxSource = {
/**
* 開始拖拽時觸發當前函式
* @param {*} props 元件的 props
*/
beginDrag(props) {
// 返回的物件可以在 monitor.getItem() 中獲取到
return {
name: props.name,
}
},
/**
* 拖拽結束時觸發當前函式
* @param {*} props 當前元件的 props
* @param {*} monitor DragSourceMonitor 物件
*/
endDrag(props, monitor) {
// 當前拖拽的 item 元件
const item = monitor.getItem()
// 拖拽元素放下時,drop 結果
const dropResult = monitor.getDropResult()
// 如果 drop 結果存在,就彈出 alert 提示
if (dropResult) {
alert(`You dropped ${item.name} into ${dropResult.name}!`)
}
},
}
@DragSource(
// type 標識,這裡是字串 'box'
ItemTypes.BOX,
// 拖拽事件物件
boxSource,
// 收集功能函式,包含 connect 和 monitor 引數
// connect 裡面的函式用來將 DOM 節點與 react-dnd 的 backend 建立聯絡
(connect, monitor) => ({
// 包裹住 DOM 節點,使其可以進行拖拽操作
connectDragSource: connect.dragSource(),
// 是否處於拖拽狀態
isDragging: monitor.isDragging(),
}),
)
class Box extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
isDragging: PropTypes.bool.isRequired,
connectDragSource: PropTypes.func.isRequired
}
render() {
const { isDragging, connectDragSource } = this.props
const { name } = this.props
const opacity = isDragging ? 0.4 : 1
// 使用 connectDragSource 包裹住 DOM 節點,使其可以接受各種拖動 API
// connectDragSource 包裹住的 DOM 節點才可以被拖動
return connectDragSource && connectDragSource(
<div style={{ ...style, opacity }}>
{name}
</div>
);
}
}
export default Box;
複製程式碼
建立 Dustbin 元件,作為 DropTarget
在 components
目錄下,建立 Dustbin.js
檔案,編寫 Dustbin
元件,使其可以接受對應的拖拽元件。
components/Dustbin.js
import React from 'react';
import PropTypes from 'prop-types';
import { DropTarget } from 'react-dnd';
import ItemTypes from '../types';
const style = {
height: '12rem',
width: '12rem',
marginRight: '1.5rem',
marginBottom: '1.5rem',
color: 'white',
padding: '1rem',
textAlign: 'center',
fontSize: '1rem',
lineHeight: 'normal',
float: 'left',
}
const boxTarget = {
// 當有對應的 drag source 放在當前元件區域時,會返回一個物件,可以在 monitor.getDropResult() 中獲取到
drop: () => ({ name: 'Dustbin' })
}
@DropTarget(
// type 標識,這裡是字串 'box'
ItemTypes.BOX,
// 接收拖拽的事件物件
boxTarget,
// 收集功能函式,包含 connect 和 monitor 引數
// connect 裡面的函式用來將 DOM 節點與 react-dnd 的 backend 建立聯絡
(connect, monitor) => ({
// 包裹住 DOM 節點,使其可以接收對應的拖拽元件
connectDropTarget: connect.dropTarget(),
// drag source是否在 drop target 區域
isOver: monitor.isOver(),
// 是否可以被放置
canDrop: monitor.canDrop(),
})
)
class Dustbin extends React.Component {
static propTypes = {
canDrop: PropTypes.bool.isRequired,
isOver: PropTypes.bool.isRequired,
connectDropTarget: PropTypes.func.isRequired
}
render() {
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
let backgroundColor = '#222';
// 拖拽元件此時正處於 drag target 區域時,當前元件背景色變為 darkgreen
if (isActive) {
backgroundColor = 'darkgreen';
}
// 當前元件可以放置 drag source 時,背景色變為 pink
else if (canDrop) {
backgroundColor = 'darkkhaki';
}
// 使用 connectDropTarget 包裹住 DOM 節點,使其可以接收對應的 drag source 元件
// connectDropTarget 包裹住的 DOM 節點才能接收 drag source 元件
return connectDropTarget && connectDropTarget(
<div style={{ ...style, backgroundColor }}>
{isActive ? 'Release to drop' : 'Drag a box here'}
</div>
);
}
}
export default Dustbin;
複製程式碼
在 App.js 檔案中使用 DragDropContext
App.js
import React, { Component } from 'react';
import { DragDropContext } from 'react-dnd';
import HTMLBackend from 'react-dnd-html5-backend';
import Dustbin from './components/Dustbin';
import Box from './components/Box';
// 將 HTMLBackend 作為引數傳給 DragDropContext
@DragDropContext(HTMLBackend)
class App extends Component {
render() {
return (
<div style={{ paddingLeft: 200, paddingTop: 50 }}>
<div style={{ overflow: 'hidden', clear: 'both' }}>
<Box name="Glass" />
<Box name="Banana" />
<Box name="Paper" />
</div>
<div style={{ overflow: 'hidden', clear: 'both' }}>
<Dustbin />
</div>
</div>
);
}
}
export default App;
複製程式碼
執行專案,檢視效果
執行專案:
$ npm run start
複製程式碼
瀏覽器會自動開啟 http://localhost:3000/
視窗,此時可以操作瀏覽器上的 Box 元件,結合專案程式碼,檢視效果。
預覽效果如下:
六、本文 Demo 地址
歡迎 Star!謝謝!