本文详细讲解了 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 (
);
}
}
export default App;
复制代码
运行项目,查看效果
运行项目:
$ npm run start
复制代码
浏览器会自动打开 http://localhost:3000/
窗口,此时可以操作浏览器上的 Box 组件,结合项目代码,查看效果。 预览效果如下:
六、本文 Demo 地址
react-dnd-dustbin
欢迎 Star!谢谢!
七、参考链接
react-dnd 官方文档 拖拽组件:React DnD 的使用