目录
- 前言
- 概念
- 核心API
- DndProvider
- Backend
- useDrag
- useDrag返回三个参数
- useDrag传入两个参数
- DragSourceMonitor对象
- useDrop
- useDrag返回两个参数
- useDrop传入一个参数
- DropTargetMonitor对象
- 数据流转
- 拖拽预览
- DragPreviewImage
- useDragLayer
- 其他使用场景
- 批量拖拽
- 拖拽排序
- 最后
前言
最近公司准备开发一个审批流系统,其中会用到拖拽工具来搭建流程,关于拖拽的实现我们选择了react-dnd这个库,本文总结了react-dnd API的详细用法,并附上不同场景的demo,希望对大家有用。
概念
React DnD 是一组 React 高阶组件,使用的时候只需要使用对应的 API 将目标组件进行包裹,即可实现拖动或接受拖动元素的功能。
在拖动的过程中,不需要开发者自己判断拖动状态,只需要在传入的 spec 对象中各个状态属性中做对应处理即可,因为react-dnd使用了redux管理自身内部的状态。
Some of these concepts resemble the Flux and Redux architectures.
This is not a coincidence, as React DnD uses Redux internally.
值得注意的是,react-dnd并不会改变页面的视图,它只会改变页面元素的数据流向,因此它所提供的拖拽效果并不是很炫酷的,我们可能需要写额外的视图层来完成想要的效果,但是这种拖拽管理方式非常的通用,可以在任何场景下使用,非常适合用来定制。
核心API
介绍实现拖拽和数据流转的核心API,这里以Hook为例。
DndProvider
如果想要使用 React DnD,首先需要在外层元素上加一个 DndProvider。
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
<DndProvider backend={HTML5Backend}>
<TutorialApp />
</DndProvider>
DndProvider 的本质是一个由 React.createContext 创建一个上下文的容器(组件),用于控制拖拽的行为,数据的共享,类似于react-redux的Provider。
Backend
上面我们给DndProvider传的参数是一个backend,那么这里来解释一下什么是backend
React DnD 将 DOM 事件相关的代码独立出来,将拖拽事件转换为 React DnD 内部的 redux action。由于拖拽发生在 H5 的时候是 ondrag,发生在移动设备的时候是由 touch 模拟,React DnD 将这部分单独抽出来,方便后续的扩展,这部分就叫做 Backend。它是 DnD 在 Dom 层的实现。
- react-dnd-html5-backend : 用于控制html5事件的backend
- react-dnd-touch-backend : 用于控制移动端touch事件的backend
- react-dnd-test-backend : 用户可以参考自定义backend
useDrag
让DOM实现拖拽能力的构子,官方用例如下
import { DragPreviewImage, useDrag } from 'react-dnd';
export const Knight: FC = () => {
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: ItemTypes.KNIGHT,
collect: (monitor) => ({
isDragging: !!monitor.isDragging()
})
}),
[]
);
return (
<>
<DragPreviewImage connect={preview} src={knightImage} />
<div
ref={drag}
>
♘
</div>
</>
);
};
useDrag返回三个参数
第一个返回值是一个对象
表示关联在拖拽过程中的变量,需要在传入useDrag的规范方法的collect属性中进行映射绑定,比如:isDraging,canDrag等
第二个返回值
代表拖拽元素的ref
第三个返回值
代表拖拽元素拖拽后实际操作到的dom
useDrag传入两个参数
- 第一个参数,是一个对象,是用于描述了drag的配置信息,常用属性
type
: 指定元素的类型,只有类型相同的元素才能进行drop操作
item
: 元素在拖拽过程中,描述该对象的数据,如果指定的是一个方法,则方法会在开始拖拽时调用,并且需要返回一个对象来描述该元素。
end(item, monitor)
: 拖拽结束的回调函数,item表示拖拽物的描述数据,monitor表示一个 DragTargetMonitor 实例
isDragging(monitor)
:判断元素是否在拖拽过程中,可以覆盖Monitor对象中的isDragging方法,monitor表示一个 DragTargetMonitor 实例
isDragging: (monitor) => {
return monitor.getItem() ? index === monitor.getItem().index : false;
},
collect: (monitor: any) => ({
//当传入isDragging方法时,monitor.isDragging()方法指代传入的方法
isDragging: monitor.isDragging(),
}),
canDrag(monitor)
:判断是否可以拖拽的方法,需要返回一个bool值,可以覆盖Monitor对象中的canDrag方法,与isDragging同理,monitor表示一个 DragTargetMonitor 实例
collect
:它应该返回一个描述状态的普通对象,然后返回以注入到组件中。它接收两个参数,一个 DragTargetMonitor 实例和拖拽元素描述信息item
- 第二个参数是一个数组,表示对方法更新的约束,只有当数组中的参数发生改变,才会重新生成方法,基于react的useMemo实现
DragSourceMonitor对象
DragSourceMonitor是传递给拖动源的收集函数的对象。它的方法允许您获取有关特定拖动源的拖动状态的信息。 常用的方法: canDrag()
:描述元素是否可以拖拽,返回一个bool值
isDragging()
:判断元素是否在拖拽过程中,返回一个bool值
getItemType()
:获取元素的类型,返回一个bool值
getItem()
:获取元素的描述数据,返回一个对象
getDropResult()
:拖拽结束,返回拖拽结果的构子,可以拿到从drop元素中返回的数据
didDrop()
: 拖拽结束,元素是否放置成功,返回一个bool值
getDifferenceFromInitialOffset()
: 获取相对于拖拽起始位置的相对偏移坐标。
useDrop
实现拖拽物放置的钩子,官方用例如下
function BoardSquare({ x, y, children }) {
const black = (x + y) % 2 === 1
const [{ isOver }, drop] = useDrop(() => ({
accept: ItemTypes.KNIGHT,
drop: () => moveKnight(x, y),
collect: monitor => ({
isOver: !!monitor.isOver(),
}),
}), [x, y])
return (
<div
ref={drop}
style={{
position: 'relative',
width: '100%',
height: '100%',
}}
>
....
</div>,
)
}
export default BoardSquare
useDrag返回两个参数
- 第一个返回值是一个对象,表示关联在拖拽过程中的变量,需要在传入useDrop的规范方法的collect属性中进行映射绑定
- 第二个返回值代表放置元素的ref
useDrop传入一个参数
用于描述drop的配置信息,常用属性
accept
: 指定接收元素的类型,只有类型相同的元素才能进行drop操作
drop(item, monitor)
: 有拖拽物放置到元素上触发的回调方法,item表示拖拽物的描述数据,monitor表示 DropTargetMonitor实例,该方法返回一个对象,对象的数据可以由拖拽物的monitor.getDropResult
方法获得
hover(item, monitor)
:当拖住物在上方hover时触发,item表示拖拽物的描述数据,monitor表示 DropTargetMonitor实例,返回一个bool值
canDrop(item, monitor)
:判断拖拽物是否可以放置,item表示拖拽物的描述数据,monitor表示 DropTargetMonitor实例,返回一个bool值
DropTargetMonitor对象
DropTargetMonitor是传递给拖放目标的收集函数的对象。它的方法允许您获取有关特定拖放目标的拖动状态的信息。 常用的方法:
canDrop()
:判断拖拽物是否可以放置,返回一个bool值
isOver(options)
: 拖拽物掠过元素触发的回调方法,options表示拖拽物的options信息
getItemType()
:获取元素的类型,返回一个bool值
getItem()
:获取元素的描述数据,返回一个对象
didDrop()
: 拖拽结束,元素是否放置成功,返回一个bool值
getDifferenceFromInitialOffset()
: 获取相对于拖拽起始位置的相对偏移坐标。
数据流转
看了API之后,实际上不能很好的认识到每个状态和每个方法的工作流程,所以,我这里画了一张图,帮助你更清晰的看到它的数据是如何流动的。
然后我们通过一个demo来更深刻的认识这个过程
这里我们定义了几个单词,然后通过拖拽,将它放入对应的分组里面
单词代码
const Word: FC = ({ type, text, id, ...props }: any) => {
const [offsetX, setOffsetX] = useState(0);
const [offsetY, setOffsetY] = useState(0);
const [{ isDragging }, drag]: any = useDrag(() => ({
type,
item: { id, type },
end(item, monitor) {
let top = 0,
left = 0;
if (monitor.didDrop()) {
const dropRes = monitor.getDropResult() as any;
//获取拖拽对象所处容器的数据,获取坐标变化
if (dropRes) {
top = dropRes.top;
left = dropRes.left;
}
//这里必须写成函数的传入方式,否则无法获取上一个state
setOffsetX((offsetX) => offsetX + left);
setOffsetY((offsetY) => offsetY + top);
} else {
// 移出则回到原位
setOffsetX(0);
setOffsetY(0);
}
},
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}));
return ... // dom
);
};
分组代码
function Classification({ type, title }: any) {
const [{ isOver, canDrop }, drop] = useDrop(
() => ({
accept: type,
drop(_item: any, monitor: any) {
// 获取每一次放置相对于上一次的偏移量
const delta = monitor.getDifferenceFromInitialOffset();
const left = Math.round(delta.x);
const top = Math.round(delta.y);
// 回传给drag
return { top, left };
},
canDrop: (_item, monitor) => {
const item = monitor.getItem() as any;
return item.type === type;
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: !!monitor.canDrop(),
}),
}),
[],
);
return ... // dom
)
完整demo戳链接:github.com/AdolescentJ…
拖拽预览
受限于浏览器API的控制,拖拽元素在开始拖拽之后,只能保持其本身的一个样式,不能使用其他样式预览,针对此,react-dnd为我们提供了两种解决方法。
DragPreviewImage
react-dnd提供的DragPreviewImage组件,让我们在拖拽的时候,可以以图片的形式预览拖拽的元素
它接收两个参数,一个是useDrag返回的预览ref,一个是需要预览的图片
使用
import { useDrag, DragPreviewImage } from 'react-dnd';
import apple from '../../assets/apple.png';
const DragPreviewImg = () => {
const [{ isDragging }, drag, preview] = useDrag({
type: 'DragDropBox',
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
return (
<>
<DragPreviewImage connect={preview} src={apple} />
<div
className='card_drag'
ref={drag}
style={{
opacity: isDragging ? 0.5 : 1,
}}
>
drag item prview an apple
</div>
</>
);
};
export default DragPreviewImg;
效果
完整demo戳链接:github.com/AdolescentJ…
使用图片来预览拖拽元素确实可以解决一部分问题,但在实际场景中,拖拽的元素可能会很多,我们也不能找UI把所有类型的图都给一张,并且很多比较复杂的dom图片是不能取代的
所以要展示更加定制化的预览样式,我们可以使用下面这种。
useDragLayer
useDragLayer是一个钩子,它允许你使用dom的方式自定义拖拽元
它的原理是,监听你的拖拽状态,在拖拽的时候,可以取到你拖拽状态的数据,并且它会创建一个你想预览的dom,然后我们可以可以通过拖拽的状态来改变它的样式,达到取代预览的效果。
使用
const CustomDragLayer = (props: any) => {
//monitor 是drag的 monitor
const { itemType, isDragging, item, initialOffset, currentOffset } = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(),
}));
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div className='card_drag'>这里是预览样式</div>
</div>
);
}
预览
完整demo戳链接:github.com/AdolescentJ…
其他使用场景
除了上面的例子,还有非常多的案例
批量拖拽
可以选择多个元素进行拖拽
拖拽排序
可以拖拽元素放置排序
完整demo戳链接:github.com/AdolescentJ…
最后
感谢你能看到这里,本文总结了react-dnd的API的使用以及常见的场景,希望对你有所帮助,后续会一直更新,当然,如果可以的话不妨留一个赞再走呢。
参考链接
react-dnd.github.io/react-dnd/d…
https://www.jb51.net/article/265220.htm