最近在研究用 React 绘制拓扑图的时候涉及到了 HTML5 拖放 API,了解到了 React DnD 这个拖放神器。React DnD 帮我们封装了一系列的拖放 API,大大简化了拖放 API 的使用方式,今天就结合下面这个示例给大家介绍下 React DnD 的用法。

React Dnd 提供了几个重要的 API 供我们使用:
DragSource 是一个高阶组件,使用 DragSource 高阶组件包裹的组件可以进行拖拽操作。
基本用法:
import { DragSource } from 'react-dnd'
class MyComponent {
/* ... */
}
export default DragSource(type, spec, collect)(MyComponent)
参数:
string、 symbol 或者 func ,只有具有相同type类型的元素才能被 drop target 所响应。drag source 如何对拖动事件进行响应。
方法中的参数解释:props 参数。DragSourceMonitor 实例。通过它可以获取当前的拖拽信息,比如可以获取当前被拖拽的项目及其类型,当前和初始坐标和偏移,以及它是否已被删除。setState 操作。有时候在 isDragging、 canDrag 方法里可能获取不到 component 这个参数,因为它们被调用时实例可能不可用。{ id: props.id },通过monitor.getItem() 方法可以获取到返回结果。monitor.didDrop() 可以判断 drag source 是否已经被 drop target 处理完毕。如果在 drop target 的 drop 方法中返回了一个对象,在这里可以通过 monitor.getDropResult() 获取到返回结果。monitor.isDragging()。props,接收两个参数 connect 和 monitor。DragSourceConnector 的实例,包括 dragPreview() 和 dragSource() 两个方法,常用的是 dragSource() 这个方法。source DOM 和 React DnD Backend 连接起来。DOM 节点 和 React DnD Backend 连接起来。DragSourceMonitor 的实例,包含的具体方法可以参考这里。DropTarget 是一个高阶组件,被 DropTarget 包裹的组件能够放置拖拽组件,能够对 hover 或者 dropped 事件作出响应。
基本用法:
import { DropTarget } from 'react-dnd'
class MyComponent {
/* ... */
}
export default DropTarget(types, spec, collect)(MyComponent)
参数:
string、 symbol 或者 array ,drop target 只接受具有相同 type 类型的 drag source。props 参数。DropTargetMonitor 实例。通过它可以获取当前的拖拽信息,比如可以获取当前被拖拽的项目及其类型,当前和初始坐标和偏移,以及它是否已被删除。setState 操作。有时候在 isDragging、 canDrag 方法里可能获取不到 component 这个参数,因为它们被调用时实例可能不可用。drag source 的 endDrag 方法里面,调用 monitor.getDropResult() 可以获得返回结果。drop target 的时候被调用。可以通过 monitor.isOver({ shallow: true }) 方法来检查悬停是仅发生在当前目标上还是嵌套上。drop target 是否接受 item。props,接收两个参数 connect 和 monitor。DropTargetConnector 的实例,包括 dropTarget 一个方法。source DOM 和 React DnD Backend 连接起来。DropTargetMonitor 的实例,包含的具体方法可以参考这里。使用 DragSource 和 DropTarget 包裹的组件必须放置在 DragDropContext 或者 DragDropContextProvider 组件内部。
基本用法:
import Backend from 'react-dnd-html5-backend'
import { DndProvider } from 'react-dnd'
export default function MyReactApp() {
return (
<DndProvider backend={Backend}>
/* your drag-and-drop application */
</DndProvider>
)
}
参数:
了解了上述 API 的基本使用,现在我们就来实现下开头的demo。
本示例是基于 create-react-app 开发的,通过create-react-app的CLI工具创建我们的demo工程:
$ create-react-app react-dnd-demo
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Container from './Container'
import { DndProvider } from 'react-dnd'
import Backend from 'react-dnd-html5-backend'
function App() {
return (
<div className="App">
<DndProvider backend={Backend}>
<Container />
</DndProvider>
</div>
)
}
const rootElement = document.getElementById('root')
ReactDOM.render(<App />, rootElement)
src/Container.js
import React from 'react';
import { DropTarget } from 'react-dnd';
import DraggableBox from './DraggableBox';
import Types from './types'
const styles = {
width: '500px',
height: '300px',
position: 'relative',
border: '1px solid black',
}
@DropTarget(
Types.Box,
{
drop: (props, monitor, component) => {
if(!component) {
return;
}
const delta = monitor.getDifferenceFromInitialOffset();
const item = monitor.getItem();
const left = Math.round(delta.x + item.left);
const top = Math.round(delta.y + item.top);
component.moveBox(item.id, left, top);
},
},
(connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
})
)
class Container extends React.Component {
state = {
boxes: {
a: { top: 20, left: 80, title: 'Drag me around' },
b: { top: 180, left: 20, title: 'Drag me too' },
},
}
moveBox = (id, left, top) => {
const { boxes } = this.state;
this.setState({
boxes: {
...boxes,
[id]: {
...boxes[id],
left,
top
}
}
})
}
render() {
const { isOver, canDrop, connectDropTarget} = this.props;
const { boxes } = this.state;
const isActive = isOver && canDrop;
let backgroundColor = '#ccc';
// 拖拽组件此时正处于 drag target 区域时,当前组件背景色变为 darkgreen
if (isActive) {
backgroundColor = '#453467';
}
console.log('qqqq', this.state.boxes)
return connectDropTarget && connectDropTarget(
<div style={{ ...styles, backgroundColor}}>
{Object.keys(boxes).map(item => <DraggableBox {...boxes[item]} id={item} />)}
</div>
)
}
}
export default Container;
可以看到,在 drop 方法里,通过 monitor.getDifferenceFromInitialOffset() 方法计算出每次 drop 的时候,当前元素与拖拽前元素位置的偏移量,monitor.getItem() 方法可以获得当前 哪个元素被拖拽(必须要在 drag source 的 beginDrag 方法中返回),调用 component 上的 moveBox 方法重新设置拖拽之后的最新位置,从而实现元素的移动。
collect 的 connect 方法中通过 monitor.isOver() 和 monitor.canDrop() 方法将 isOver 和 canDrop 参数传递到组件的 props 中来判断当前组件是否处于拖拽状态中,这里可以用来设置拖拽时容器的背景色。
这里有个细节需要注意的是,Container 容器的 position 属性被设置为了 relative,这样被拖拽的元素就会相对于该容器进行定位。
src/DraggableBox.js
import React from 'react';
import { DragSource } from 'react-dnd';
import Box from './Box';
import Types from './types'
@DragSource(
Types.Box,
{
beginDrag: (props) => {
const { id, title, left, top } = props
return { id, title, left, top }
}
},
(connect, monitor)=> ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
})
)
class DraggableBox extends React.Component {
getStyle = () => {
const { left, top } = this.props;
const transform = `translate(${left}px, ${top}px)`
return {
position: 'absolute',
transform,
}
}
render() {
const { connectDragSource } = this.props;
return connectDragSource(
<div style={this.getStyle()}><Box {...this.props}/></div>
)
}
}
export default DraggableBox;
beginDrag 方法必须返回一个对象,以前在 drop 方法中获取到当前被拖拽组件的信息。positon 属性必须被设置为 absolute,以方便相对容器进行定位。元素的移动是通过 css 的 transform 属性进行控制的。
src/Box.js
import React from 'react';
const styles = {
border: '1px dashed gray',
backgroundColor: 'white',
padding: '0.5rem 1rem',
marginRight: '1.5rem',
marginBottom: '1.5rem',
cursor: 'move',
display: 'inline-block'
}
class Box extends React.Component {
render() {
const { title, left, right } = this.props;
return (
<div style={{...styles}}>
{title}
</div>
)
}
}
export default Box;
关于 React DnD 的介绍,这里只是做了一个基本介绍,更多的示例大家可以参考官网,本示例的代码大家在这里可以找到。