前言(学习原因)
近期在工作中遇到一个新的需求:具体大概效果看下面.gif
当时看到这个需求经师兄提醒知道了React-Dnd,想到了表格 Table - Ant Design里有一个可拖拽表格的效果,照搬到了项目里发现不太能满足我们的需求,就去搜了一些资料了解了React-Dnd的一下原理
React-Dnd
React DnD 是一个专注于数据变更的 React 拖拽库, 对 HTML拖拽API进行了封装,通俗的将,你拖拽改变的不是页面视图,而是数据。React DnD 不提供炫酷的拖动体验,而是通过帮助我们管理拖拽中的数据变化,再由我们根据这些数据进行渲染。
这样子在处理拖拽的时候就可以关注于数据方面的变化,而不用费心去维护拖拽中的一些中间状态,更不用自己去添加,移除事件。
先知道咋安装
tnpm(npm) install react-dnd -S // react-dnd包,其核心包
tnpm(npm) install react-dnd-html5-backend -S // 拖拽的底层实现所需要的库
除了react-dnd外还需要react-dnd-html5-backend这个包,它存在才会允许 React DnD 底层的 HTML5 拖放 AP,使得HTML 拖放界面使应用程序能够在浏览器中使用拖放功能
引入使用
//引入
import {DndProvider, useDrag, useDrop} from 'react-dnd'
import {HTML5Backend} from 'react-dnd-html5-backend'
三个核心点
通过使用React DnD这个库,我认为里面最有用的部分包含一个组件和两个Hook API,它们分别是:
- DndProvider组件
- useDrag函数
- useDrop函数
React-DnD实现
DndProvider注入
如果想要使用 React DnD,首先需要在外层元素上加一个 DndProvider,得先声明一下拖拽的对象
比较官方解释:
DndProvider组件为您的应用程序提供React-DnD功能。必须通过backend参数将其注入后端,但是也可以将其注入window对象。
- backend: 必填,一个 React DnD 后端,实现DnD的方式,目前官方文档有三个,分别为:react-dnd-html5-backend、react-dnd-touch-backend、react-dnd-test-backend,但是常用的还是react-dnd-html5-backend,或者也可以自己写backend后端。。
- context: 选填,用户配置后端的上下文,这取决于后端的实现。
- options: 选填,配置后端对象,自定义时可以传入backend。
比较通俗解释:
DndProvider
本质是一个由 React.createContext 创建一个上下文的容器(组件),用于控制拖拽的行为,数据的共享。DndProvider 的入参是一个 Backend。
Backend 是什么呢?
React DnD 抽象了后端的概念,我们可以使用 HTML5 拖拽后端,也可以自定义 touch、mouse 事件模拟的后端实现,后端主要用来抹平浏览器差异,处理 DOM 事件,同时把 DOM 事件转换为 React DnD 内部的 redux action。
//引入
import {DndProvider, useDrag, useDrop} from 'react-dnd'
import {HTML5Backend} from 'react-dnd-html5-backend'
//DndProvider注入
export const DndCheckBoxGroup = (props) => {
const options = props.options
const setOptions = props.setOptions
const value = props.value
const onChange = props.onChange
return <OptionsContextProvider options={options} setOptions={setOptions} value={value} onChange={onChange}>
<DndProvider backend={HTML5Backend}>
//此处将放拖拽相关内容
<DndCheckbox/>
</DndProvider>
</OptionsContextProvider>
}
DndCheckbox组件
写一个组件把列表渲染一下,可以看出我们要将实现的拖拽效果应用到DraggableCheckbox组件上
const DndCheckbox = () => {
const {options, value, onChange} = useContext(OptionsContext);
return <Checkbox.Group value={value} onChange={onChange}>
{
options.map((item, index) => {
return <DraggableCheckbox key={item.value} item={item} index={index}/>
})
}
</Checkbox.Group>
}
useDrag 声明拖动源
一个元素想要被拖拽需要了解的就是从什么位置进行拖拽,该库提供了useDrag hook API,该元素可以让一个DOM元素实现拖拽效果。
代码格式
import { useDrag } from 'react-dnd'
/**
* 返回的参数
* collected:一个对象,包含从collect函数收集的属性,如果collect未定义函数,则返回一个空对象
* drag:拖动器的连接器功能,必须附加到DOM的可拖动部分
* dragPreview:用于拖动预览的连接器功能,可以附加到DOM的预览部分
*/
const [collected, drag, dragPreview] = useDrag(() => ( () => ({
// 只有drop和此值相同才可以进行放置
type,
// 描述要拖动的数据
item,
// 监听功能
collect: (monitor,props) => ({
isDragging: monitor.isDragging()
})
}), [deps])
入参
- spec 规范对象或创建规范对象的函数,关键内容包括:
-
- item: 必填。一个普通的JavaScript对象,描述了要拖动的数据。这是可用于放置目标的有关拖动源的唯一信息
- type: 必填,并且必须是字符串,ES6符号。useDrop的accept为相同类型,target才会对该项作出反应
- isDragging(monitor):选填。默认情况下,只有启动拖动操作的拖动源才被视为拖动
- options:可选的,一个简单对象
- collect:选填,收集功能,它接收两个参数,monitor和props。(useDrag也有)
另外还有一些像拖动开始时begin(monitor)、结束时end(item, monitor)、是否允许拖动canDrag(monitor)等相关内容,可以在更多学习链接里学习。
- deps
用于记忆的依赖数组。这类似于内置的useMemo钩子。默认值为函数规范的空数组,以及包含对象规范的规范的数组。
返回值
- arguments[0]: 一个对象,其中包含从collect函数收集的属性。如果collect未定义函数,则返回一个空对象。
- arguments[1]: DragSource Ref,拖动源的连接器功能。这必须附加到DOM的可拖动部分。
- arguments[2]: DragPreview Ref,用于拖动预览的连接器功能。这可以附加到DOM的预览部分。
useDrop 声明放置源
为了将内容放置到目标位置,提供了useDrop Hooks函数,
useDrop将放置目标元素和DnD系统连接起来。通过将规范的拖拽对象作为入参传入useDrop,你可以定义放置目标接受的数据项accept,使用哪些collect等等。该函数返回一个数组,其中包含一个要附加到Drop Target节点的ref和collected的props。
代码格式
const [collectedProps, drop] = useDrop(() => (
//spec
{
// 此放置目标将仅对于指定类型的拖动源产生的项目作出反应
accept,
// 当兼容项目放在目标时调用
drop: (item) => {
},
// 监听功能
collect: monitor => ({
//isOver: !!monitor.isOver(),
// 是否重叠
isOver: monitor.isOver(),
// 是否可以放置
canDrop: monitor.canDrop(),
}),
}),
[deps]
}))
入参
- spec 规范对象或创建规范对象的函数,关键内容包括:
-
- accept: 必填。字符串,ES6符号,其中一个的数组或返回给定组件的其中一个的函数props。此放置目标将仅对由指定类型的拖动源产生的项目作出反应,比如说你useDrag拖动的是复选框,useDrop接收到也要是复选框。
- options: 选填。一个普通的对象。
- drop(item, monitor): 选填。可选的,当兼容项目放在目标时被调用;
- collect: 可选的,监听功能
另外还有一些像拖动停止时end(item, monitor)、是否允许拖动canDrag(monitor),options等相关内容,可以在更多学习链接里学习。
- deps
deps用于记忆的依赖数组。这类似于内置的useMemo钩子。默认值为函数规范的空数组,以及包含对象规范的规范的数组(坑踩在这里)
返回值
- arguments[0]: 一个对象,其中包含从collect函数收集的属性。如果collect未定义函数,则返回一个空对象。
- arguments[1]: 拖动源的连接器功能。这必须附加到DOM的可拖动部分。
写到这里,可以看出实现拖拽效果,需要有三个要点,首先是用Dnd来包裹我们需要的拖拽的ref元素,再声明拖动的它(拖动源)和可以放置它的他(放置源)
具体实现
DndCheckboxGroupExample组件定义了拖放项,使用 useDrag 和 useDrop 包裹,调用父组件传过来的拖放的索引to,moveOption 方法来处理拖拽。
const CHECKBOX_TYPE = 'dndCheckbox';
function DraggableCheckbox(props) {
const ref = useRef(null);
const to = props.index
const {moveOption} = useContext(OptionsContext);
const [, drag] = useDrag(
() => ({
type: CHECKBOX_TYPE,
item: {index: to},
collect: (monitor) => ({
isDragging: monitor.isDragging()
})
}), [to]
)
const [, drop] = useDrop(() => ({
accept: CHECKBOX_TYPE,
drop: (item) => {
let from = item.index;
moveOption(from, to)
},
collect: monitor => ({
isOver: !!monitor.isOver(),
}),
}),
[to, moveOption]
)
drop(drag(ref))
if(typeof props?.item ==='string'){
return <span ref={ref}><Checkbox value={props.item}>{to}{props.item}</Checkbox> </span>
}
else{
return <span ref={ref}><Checkbox value={props.item.value}>{to}{props.item.label}</Checkbox> </span>
}
}
- 绑定ref,获取到我们拖动的组件(小小坑,封装的组件不能获取到直接获取ref,需要在外面套一层容器)
- 拿到我们拖动的数据在列表中index(to)
- 通过useDrag来调用moveOption (from,to)方法(这个方法很关键)写在上面的OptionsContextProvider
组件中传递出来
/**
* useCallback 用来返回一个函数,在父子组件传参或者通用函数封装中
* 返回的函数a会根据b的变化而变化,如果b始终未发生变化,a也不会重新生成,避免函数在不必要的情况下更新。
* @param {number} oldIndex - 旧的索引
* @param {number} newIndex - 新的索引
* @returns {function} - 返回一个函数
*/
const moveOption = useCallback((oldIndex, newIndex) => {
// 获取需要移动的元素
const movedItem = options[oldIndex]
// 根据旧的索引过滤出新的元素
const sortedOptions = options.filter((item, index) => {
return index!== oldIndex
})
// 将需要移动的元素插入到新的位置
sortedOptions.splice(newIndex, 0, movedItem)
// 根据新的元素排序
const sortedValue = value.sort((a,b)=> {
return sortedOptions.findIndex((item)=>item.value === a) -
sortedOptions.findIndex((item)=>item.value === b)
})
// 更新选项
onChange(sortedValue)
// 更新选项
setOptions(sortedOptions)
}, [options, setOptions, value])
踩坑教训
刚开始小菜的useDrop依赖只写了
于是发现自己更新的数据,拖拽的数据会变成初始默认选中的值,后选的选中态会取消,描述不出来,大家看一下截图吧
可以看出拖拽了index为2的复选框,应该移动到第一位
然而拖拽后
恢复到了默认(哭了哭了)
遇到错就要解决啊,然后换方法写useMemo,useEffect,结局可想而知。。。。一度怀疑自己的数据传错了,debug,debug,解决不出来,就去重新翻了文档,发现deps这个地方,也要把moveOption放到依赖里进行监听,及时更新数据,不然数据总会恢复到默认值。。。
demo:https://github.com/Py-spj/syxDemo
更多学习(参考)链接:
- React DnD
- 拖拽组件:React-DnD用法及源码解析 - 掘金