React Dnd不同于其他的拖拽库,如果你以前没有用过它可能会被吓到。然而,一旦你了解了它设计的一些核心概念,它将变得有意义。我建议你在阅读文档其他部分之前,先阅读这些核心概念。
这些核心概念和flux/redux架构相似。这并不是巧合,因为React DnD内部就是用的Redux。
React DnD是基于HTML5的拖放API搭建的。基本原因是这个API能够截取拖拽的dom节点作为一个拖拽预览。它的便利性就在于你不必为拖拽时的鼠标绘制样式。这个API也是处理文件拖拽上传的唯一方式。
不幸的是,HTML5拖拽API也有缺点。它不支持触摸屏,并且它在IE上提供的可定制机会要少于其他浏览器。
这就是为什么React DnD把HTML5的拖拽支持作为一种可插拔的方式来实现。你不必用它。你可以自己做一个不同的实现,基于触摸事件,鼠标事件,甚至完全不同的东西。一些可插拔的(拖拽接口)实现在React DnD被称作“backends”。React DnD目前仅提供了HTML5 backends,但是以后可能会添加更多的实现。
Backends承担的角色有点像React的事件合成系统:他们都抽象了浏览器的事件和DOM事件实现的区别。虽然有相似点,React DnD的backends却并不依赖React或事件合成系统。在底层,backends做的事情就是把DOM事件转换成React DnD可以处理的Redux actions。
像Flux(或Redux),React DnD用数据作为真实的处理源,而不是界面。当你在屏幕上拖动一些东西时,我们不说是一个组件或者一个DOM节点被拖动了。事实上,我们说是某个特定的type(类型)的item(项)被拖动了。
什么是item?Item就是一个纯js对象,用以描述什么被拖动了。举个例子,在看板应用中,当你拖动一个卡片,item看起来可能像{cardId:42}
。在一个象棋游戏中,当你捡起一个棋子,item看起来可能像{fromCell: 'C5', piece: 'queen'}
。用纯对象描述拖拽数据,可以帮助你解耦组件和使组件间互无干扰。
什么是type?type是一个字符串(或一个symbol)在应用中唯一标识items的完整类型。在看板应用中,可能有一个“card”类型代表可拖动的卡片,同时有一个“list”类型用以表示由这些卡片组成的可拖动的列表。在象棋应用中,可能只有一个“piece”类型。
Types(类型)是有用的,随着你应用的增长,你可能想让更多东西可拖动,但是你不一定想放置目标突然接受新的items(项)。Types能让你指定哪些拖拽源和放置目标可兼容。你可能会枚举出所有的type常量,就像枚举Redux的action types一样。
拖拽本身是有状态的。要么拖动操作正在进行,或者没有。要么有一个类型和一个项,或者没有。状态一定存在于某处。
React DnD通过一些被称作monitors的包装了内部状态存储的封装器把这些状态暴露给组件。这些封装器让你更新组件props来响应拖拽状态的改变。
每个组件都需要跟踪拖放状态,你可以定义一个收集函数来获取从监控器返回的一些细节。React DnD负责及时调用收集函数将返回结果合并到你的组件props中。
比如说你想在某个棋子被拖动后高亮棋盘格子。为Cell组件定义的收集函数可能像这样:
function collect(monitor) {
return {
highlighted:monitor.canDrop(),
hovered: monitor.isOver()
};
}
这个函数会指示React DnD传递最新的highlighted和hovered的值给所有的Cell组件实例的属性。
如果backend掌控的是DOM事件,而组件用的是React(虚拟DOM)描述DOM,那么backend怎么知道哪些DOM节点应该被监听勒?通过输入connectors。连接器让你在render函数中分配一条预定规则(拖拽源,拖拽预览,放置目标)给DOM节点。
事实上,connector作为第一个参数被传递给我们上面提到的收集函数。我们来看看怎样用它指定一个放置目标:
function collect(connect, monitor){
return{
highlighted:monitor.canDrop(),
hovered: monitor.isOver(),
connectDropTarget: connect.dropTarget()
};
}
在组件的render方法中,能同时获得从监控器放回的数据,和从连接器返回的函数。
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组件的根节点是一个有效的放置目标,并且backend应该掌控它的hover和drop事件。在内部它通过你指定的React元素的引用回调来工作。这个函数通过连接器memoized后返回的,所以它不会中断shouldComponentUpdate 的优化。
截止目前我们已经介绍了和DOM打交道的backends,代表项和类型的数据,以及收集函数,得益于监控器和连接器,你能描述React DnD应该将哪些props注入到组件中。
但是我们如何配置组件来实际接受这些注入的props呢?我们怎样执行具有副作用的拖放事件呢?是时候来会会drag sources(拖拽源)和drop targets(放置目标)了,这是React DnD主要的抽象单位。它们才是真正把类型、项、副作用操作,和收集函数连接到你组件中的东西。
无论何时你想让你的组件或者它的一部分可拖动,你都需要用拖拽源声明来包装这个组件。每个拖拽源都需要注册一个特定的类型,并且必须实现一个通过组件props生成项的方法。它也可以选择性的指定其他一些方法来处理拖放事件。拖放源声明还允许你为给定组件指定收集函数。
放置目标和拖拽源十分相似。唯一的区别在于一个放置目标可以同时注册几个项类型,而不是提供一个项,它能掌控自己的hover和drop事件。
你怎么包装你的组件?包装纠结是什么意思?如果你以前没有用过高阶组件,先读读这篇文章,它详细介绍了这个概念。
所谓高阶组件就是一个函数,获取一个React组件类并返回另一个不同的组件类。
The wrapping component provided by the library renders your component in its render method and forwards the props to it, but also adds some useful behavior.(这句目前不知道怎么翻译,只好原文引用了。)
在React DnD中,DragSource和DropTarget,以及其他的一些顶级的对外函数,实际上都是高阶组件。他们把拖放魔法带入到你的组件里。
一个关于使用它们的警告就是他们需要两个应用函数。举个例子,这里是如何用DragSource包装YourComponent:
import { DragSource } from 'react-dnd';
class YourComponent { /* ... */ }
export default DragSource(/* ... */)(YourComponent);
注意,在指定DragSource的参数的时候发生了第一次调用,然后再最后传递你的组件类的地方,发生了第二次调用。这被称为函数柯里化,或者偏函数用法,并且有必要创造性的使用ES7的修饰符语法。
你不必使用这个语法,但是如果你喜欢,你就能用Babel转换你的代码,并且在.babelrc文件里设置{ "stage": 1 }
。
即使你不打算用ES7,偏函数用法也是依然有益的,因为它可以帮你在ES5或ES6中用有合并功能的辅助工具如 _.flow,合并几个DragSource 和 DropTarget 声明。在ES7中,你只需堆叠使用这些修饰符就能达到同样效果。
import { DragSource } from 'react-dnd';
@DragSource(/* ... */)
@DropTarget(/* ... */)
export default class YourComponent {
render() {
const { connectDragSource, connectDropTarget } = this.props
return connectDragSource(connectDropTarget(
/* ... */
))
}
}
下面是一个包装现有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);
}
};
// Use the decorator syntax
@DragSource(Types.CARD, cardSource, (connect, monitor) => ({
// 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()
}))
export default class Card {
render() {
// Your component receives its own props as usual
const { id } = this.props;
// These two props are injected by React DnD,
// as defined by your `collect` function above:
const { isDragging, connectDragSource } = this.props;
return connectDragSource(
<div>
I am a draggable card number {id}
{isDragging && ' (and I am being dragged now)'}
</div>
);
}
}
现在你已经掌握了足够的信息去探索文档余下的部分了。
这个实例将是一个不错的开始。
参考链接:https://liunianmou.gitbooks.io/react-dnd/content/chapter1.html