react-dnd 用法詳解

暖生發表於2019-03-21

本文詳細講解了 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 被呼叫。對於每個 beginDragendDrag 都會對應。

  • canDrag(props, monitor): 可選的。用它來指定當前是否允許拖動。如果您想要始終允許它,只需省略此方法即可。注意:您可能無法呼叫monitor.canDrag() 此方法。

  • isDragging(props, monitor): 可選的。預設情況下,僅啟動拖動操作的拖動源被視為拖動。注意:您可能無法呼叫 monitor.isDragging() 此方法。

方法中的引數 props, monitor, component

  • props:當前元件的 props
  • monitor:一個 DragSourceMonitor 例項。使用它來查詢有關當前拖動狀態的資訊,例如當前拖動的專案及其型別,當前和初始座標和偏移,以及它是否已被刪除。
  • component:指定時,它是元件的例項。使用它來訪問底層DOM節點以進行位置或大小測量,或呼叫 setState 以及其他元件方法。isDraggingcanDrag 方法裡獲取不到 component 這個引數,因為它們被呼叫時例項可能不可用

collect 中的 connect 和 monitor 引數

  • connect: 一個 DragSourceConnector 例項。它有兩種方法:dragPreview()和dragSource()。

    • dragSource() => (elementOrNode, options?):常用方法,返回一個函式,傳遞給元件用來將 source DOM 和 React DnD Backend 連線起來
      • dragPreview():返回一個函式,傳遞給元件用來將拖動時預覽的 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 地址

react-dnd-dustbin

歡迎 Star!謝謝!


七、參考連結

react-dnd 官方文件 拖拽元件:React DnD 的使用