react-dnd 用法详解

31,972 阅读13分钟

本文详细讲解了 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 的使用