原文链接:React DnD官方文档
概述
React DND 和大多数其他的拖拽组件不同,你刚开始使用的时候你可能会有些害怕。但是,一旦你了解了其设计核心的一些概念,它就会变得很好理解。我建议你在阅读该文档剩余部分之前先看看这些概念。 其中一些概念类似于Flux和Redux架构。这不是巧合,因为React DnD在内部使用Redux。
Backends(对原生拖拽事件的封装库)
React DnD建立在 HTML5 drag and drop API之上。 因为它可以对已拖动的DOM节点进行屏幕快照,并将其用作“拖动预览”,这样你就不必在光标移动时进行任何绘制相关的操作,同时这个API也是处理文件删除事件的唯一方法。
但是,HTML5拖放API也有一些缺点。 它在触摸屏上不起作用,并且与其他浏览器相比,它在IE上提供的定制化更少。
这就是为什么在React DnD中以可插入方式实现HTML5 drag and drop API的原因。 你不必非得用它,你完全可以根据原生的触摸事件,鼠标事件等来封装自己的实现。这种可插拔的实现在React DnD中称为Backends。 该库现在只在HTML backend中使用,未来可能会有更多地方会用到。
backends的作用类似于React的综合事件系统:它们都是把浏览器差异抽象出来并处理本地的DOM事件。不同的是,Backends并不依赖React或者React的综合事件系统。底层实现中,backends做的就是原生的Dom事件转换成React DnD能处理的内部 Redux 的actions。
Items and Types
与Flux(或Redux)一样,React DnD操作的是数据,而不是视图。当你在屏幕上拖动某个对象时,我们并不是理解为正在拖动某个组件或DOM节点。相反,我们理解成一个有某种Type的Item正在被拖动。
Item是什么呢?Item就是一个普通的js对象,描述了被拖动节点信息的对象。举个栗子,在一个看板应用中,你拖动一个卡片的时候,Item可能就长得像{cardId:42}。在象棋游戏中,当你拿起一个棋子的时候,Item可能就像是{fromCell:'C5',piece:'queen'}。使用对象对拖动的数据描述可以保持组件间的解耦。
什么是type呢?type就是一个字符串(或者一个Symbol),它是某一类Item在项目中的唯一标志符。在看板应用中,你可能有一个叫'card'的type代表你拖动的卡片,可能有一个叫做'list'的type管理这些可拖动卡片的列表。在象棋应用中,你可能只有一个'piece'的type。
Types很有用,在你的项目越来越大的时候,你可能有很多组件是可拖拽,但是你并不想让所有现有的drop target突然对新的Item有反应。Types可以让你明确的知道哪些Drag Sources和Drop Targets是匹配的。你可能会在应用程序中枚举类型常量来保存所有的Type,就像Redux中保存action的type一样。
Monitors
拖放一定是有状态的。是否在进行拖动,当前是否正在有Item被操作这些状态都需要保存在一个地方。
React DnD通过monitors(对内部状态存储的封装)将这些状态暴露给你的组件。monitors让你可以更新你组件的props来更改拖动的状态。
对于每一个需要跟踪拖动状态的组件,你可以定义一个collecting function来从monitor中追踪和组件相关的状态。然后,React DnD会及时调用你的collecting function并将其返回值合并到组件的props中。
比方说当你拖动一枚棋子的时候,你想高亮棋盘格。一个为Cell(棋盘格子)定做的collecting function大概就长这样子:
function collect(monitor) {
return {
highlighted: monitor.canDrop(),
hovered: monitor.isOver()
}
}
它表示让React DnD将highlighted和hovered的最新值作为props传给所有的Cell(棋盘格子)实例
Connectors
如果用backend来处理Dom事件,但是组件用React来描述Dom,backend要如何知道去监听哪个Dom节点呢?答案是Connectors。Connector可以让你的render方法中注册一个预定义的角色(a drag source,a drop preview,a drop target)
事实上,connector要作为我们上面提到的collect function中的第一个参数,让我们来看看要怎样使用它来明确一个drop target:
function collect(connect, monitor) {
return {
highlighted: monitor.canDrop(),
hovered: monitor.isOver(),
connectDropTarget: connect.dropTarget()
}
}
在这个组件的render方法中,我们可以拿到从monitor中包含的数据和从connector中包含的方法了
render() {
const { highlighted, hovered, connectDropTarget } = this.props;
return connectDropTarget(
<div className={classSet({
'Cell': true,
'Cell--highlighted': highlighted,
'Cell--hovered': hovered
})}>
{this.props.children}
</div>
);
}
connectDropTarget可以通知React DnD当前组件挂载Dom节点可以作为一个有效的drop target,并且它的hover和drop事件会在backend中处理。具体实现是通过把一个callback ref(ref={element=>this.xxx=element})附加到你提供的React element上。connector中返回的方法是被缓存过的,所以不会中断shouldComponentUpdate的优化。
Drag Sources and Drop Targets
到目前为止,我们已经介绍了用来解决Dom事件相关的backends、用来表示数据的Items和Types,用来描述哪些props需要被React DnD注入到你组件中的collecting function(monitors,connectors)。
但是我们如何配置我们组件的props?我们如何表现拖放事件的效果?接下来让我们来认识一下Drag Sources 和 Drop Targets-两个React DnD主要的抽象单元。它们两个把types,items,拖拽效果还有collecting functions和你的组件绑定在一起
当你想让一个组件可以拖动,你需要将该组件用drag source declaration包装。每一个drag source都需要注册一个确定的type,需要实现一个从组件的props中生成item的方法,还可以选择指定一些方法来处理drag和 drop事件。drag source declaration还能让被包装组件指定一个collecting function
drop targets和drag sources类似,唯一不同点就是drop target可能一次性注册多个item和type,另外drop target可以处理它的hover和drop而不是生成item。
高阶组件和装饰器
你要如何包装你的组件呢?包装到底是什么意思?如果你从来没有接触过高阶组件,去看看这篇文章(其实去React官网看看就好),它讲的很详细。
高阶组件就是一个函数,参数是一个React组件,返回值是另外一个组件库提供的包裹组件还在自己的render方法中渲染你的组件,传递props,同时可以添加一些有用的行为等。
在React DnD中,DragSource 和 DropTarget,还有其他高级的暴露方法,实际上都是高阶组件。它们赐予了你的组件一些drag和drop的魔法。
有一点需要注意的是,使用他们的时候需要调用两次。举例来说,下面是如果用DragSource来包裹你的组件:
import { DragSource } from 'react-dnd'
class YourComponent {
/* ... */
}
export default DragSource(/* ... */)(YourComponent)
注意,在指定DragSource的第一个参数并调用后,还有第二次函数调用,参数为你的组件。这叫做柯里化,或者叫做函数部分应用,这对修饰器的开箱即用是很有必要的:
import { DragSource } from 'react-dnd'
@DragSource(/* ... */)
export default class YourComponent {
/* ... */
}
你不是非得用这个语法,但是如果你喜欢用,你可以配置你的Babel,并将{"stage":1}添加进你的.babelrc file
如果你不打算用修饰器,函数部分应用仍然可以派上用场。因为您可以使用_.flow 之类的函数组合助手在JavaScript中组合多个DragSource和DropTarget声明。 使用装饰器,您可以堆叠装饰器以达到相同的效果。使用装饰器可以达到同样的效果
import { DragSource, DropTarget } from 'react-dnd'
import flow from 'lodash/flow'
class YourComponent {
render() {
const { connectDragSource, connectDropTarget } = this.props
return connectDragSource(
connectDropTarget()
/* ... */
)
}
}
export default flow(DragSource(/* ... */), DropTarget(/* ... */))(YourComponent)
变形金刚合体
下面就是一个封装Card组件的例子
import React from 'react'
import { DragSource } from 'react-dnd'
// Drag sources and drop targets only interact
// if they have the same string type.
// You want to keep types in a separate file with
// the rest of your app's constants.
const Types = {
CARD: 'card'
}
/**
* Specifies the drag source contract.
* Only `beginDrag` function is required.
*/
const cardSource = {
beginDrag(props) {
// Return the data describing the dragged item
const item = { id: props.id }
return item
},
endDrag(props, monitor, component) {
if (!monitor.didDrop()) {
return
}
// When dropped on a compatible target, do something
const item = monitor.getItem()
const dropResult = monitor.getDropResult()
CardActions.moveCardToList(item.id, dropResult.listId)
}
}
/**
* Specifies which props to inject into your component.
*/
function collect(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource(),
// You can ask the monitor about the current drag state:
isDragging: monitor.isDragging()
}
}
function Card(props) {
// Your component receives its own props as usual
const { id } = props
// These two props are injected by React DnD,
// as defined by your `collect` function above:
const { isDragging, connectDragSource } = props
return connectDragSource(
<div>
I am a draggable card number {id}
{isDragging && ' (and I am being dragged now)'}
</div>
)
}
// Export the wrapped version
export default DragSource(Types.CARD, cardSource, collect)(Card)
现在你对React DnD学的差不多了,接下来你可以从tutorial开始