react-dnd 的使用问题:简易实践、关键名词
总结:
- 建立排查逻辑流:从外向内,先 UI 观察差异/关注任何异常,不要忽略 chrome 工具的价值 - 热区问题的排查 - 直接修改 节点/属性值来定位 - 更快速高效
- React-dnd 的简易实践:useDrag 记录源数据 id,主要是通过配置 item/type/collect/end -> useDragLayer 进行位置计算 -> useDrop 配置 accept/drop
- 关键名词记忆
- 项目:被拖动的内容
- 监视器:拖放操作的状态 + 当前项目信息,可以定义一个收集函数 collect - dnd 会及时调用收集函数
- 连接器:作为收集函数的第一个参数,预定义角色
一、React-dnd 的基本概念论述
1、功能
构建复杂的拖放问题,同时保持组件的解耦
- 拖动会在应用的不同部分之间 传输数据,而组件根据 拖放事件 改变其外观、状态
2、依赖安装 + 使用
npm install react-dnd react-dnd-html5-backend
--
import { useDrag } from 'react-dnd';
export default function MyCon({ isDragging, text }) {
const [{opacity}, dragRef] = useDrag(() => ({
type: ItemTypes.CARD, // 场景少的话,可以直接写字符串,例如 ‘DragRow’
item: { text },
collect: (monitor) => ({
opacity: monitor.isDragging() ? 0.5 : 1
})
}), [])
return (<div ref={dragRef} style=> {text} </div>)
}
3、理解
不封装组件,而是将道具注入你的组件
- 内部使用了 redux 实现,架构相似
- 名词 - 项目:这个名词用于描述 被拖动的内容 - 这么描述有助于保持组件间的分离和互不关联
通过 监视器 和 连接器 可以描述 React DnD 应该向组件注入哪些道具:
- 名词 - 监视器:这个名词是存储了 拖放操作的状态 - 是否正在进行拖拽、当前项目信息
- 每个需要跟踪拖放状态的组件,都可以定义一个 收集函数 ,用于在监视器中检索相关内容 - DnD 会及时调用收集函数
- 名词 - 连接器:(后端处理 DOM 事件)用于为 render 函数中的 DOM 节点指定一个预定义角色(拖动源、拖动预览、下拉目标)
- 作为收集函数的第一个参数
function collect(connect, monitor) {
return {
highlighted: monitor.canDrop(),
hovered: monitor.isOver(),
connectDropTarget: connect.dropTarget(),
}
}
【主要抽象单元】 拖动源和拖放目标 - 将类型、项目、副作用和收集功能与您的组件联系在一起
- 解决的问题: 如何配置 注入这些道具呢?如何执行副作用来响应拖放事件?
二、HOOKS
主要三个 hooks: useDrag、useDrop、useDragLayer
- useDrop
作用: 将您的组件作为 投放目标 接入 DnD 系统, 指定下拉目标将接受哪些类型的数据项、收集哪些道具
返回值: 返回一个数组,其中包含附加到下拉目标节点的 ref 和收集的道具
三、完整工作流
step1 拖拽开始: useDrag 记录源项 index 和 id
step2 位置计算 + 状态更新:拖拽移动 handlePosition 通过鼠标位置计算 targetRow + 实时更新标记线位置
step3 放置确认:放置 drop 回调获取 源项 item.index 和 目标项 markLineState.targetRow
- 再调用 onRowDragEnd(sourceIndex, targetRow) 修改数据
简易最佳实践
// step1:配置 useDrag
const DraggableItem = ({ index, rowHeight, handlePosition, dragId}) => {
const [{ isDragging }, drag, preview] = useDrag(
{
type: 'DragRow',
item: { id: dragId, index},
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
end: (item, monitor) => {
if (!monitor.didDrop()) { handlePosition(dragId) }
},
});
const { items, currentOffset } = useDragLayer((monitor) => {
let items = monitor.getItem();
if (monitor.isDragging()) {
handlePosition(dragId, currentOffset, items);
}
return {items, currentOffset: monitor.getSourceClientOffset()}
});
return (
<div ref={preview} key={dragId} >
<NavCell key={dragId} dragRef={drag} rowDrag={true} rowHeight={rowHeight} />
</div>
);
};
// step2:计算位置 - 应该放在下方的 dragCol 中,但是逻辑其实和拖拽无关,是业务逻辑 - 所以抽出来展示理解
const handlePosition = useCallback((dragId, params, items) => {
if (!params) {
setMarkLineState({ top: null })
return;
}
if(items.id !== dragId) { // react-dnd 中monitor 唯一,需要通过组件 id 来判定是否重新计算 markLineState 的值
return;
}
let clientInfo = refs.bodyRef.current.getOffset();
let info = canvasCtx?.selectCellAreaMouseDown({clientX: params.x, clientY:params.y }, clientInfo);
const { lastRow, lastTop } = dataRef.current;
if (lastRow !== info?.row) { // 简单计算高度
let row = info?.row === 0 ? 1 : info.row;
const top = rowHeight * row;
setMarkLineState({
lastRow: row,
lastTop: markLineState?.top,
top
})
}
})
// step3:放置位置,触发 drop 实现拖拽
export default function index(props) {
const [, drop] = useDrop({
accept: 'DragRow',
drop: (item, monitor) => {
onDrop(item?.index) // 即 useDrag 中传递的 item 信息
}
}
);
const onDrop = (sourceIndex) => {
let targetRow = markLineState.lastRow;
let sourceRow = sourceIndex; // 目标行 - 取 onDrop 监听到被拖拽的行
if (sourceRow > targetRow) { onRowDragEnd(sourceIndex, targetRow) } // 上移
if (sourceRow < targetRow) { onRowDragEnd(sourceIndex, targetRow - 1) } // 下移
setMarkLineState({ lastRow: 0, lastTop: null, top: null })
}
return ( <div className={styles['drop-wrapper']} ref={drop}>
{data.map((v, i) => (
<DraggableItem
key={v.id} dragId={v.id} index={i} rowHeight={rowHeight} handlePosition={handlePosition}/> ))
}
</div>)
}