React-dnd

1. 安装

安装

# yarn
tyarn add react-dnd react-dnd-html5-backend immutability-helper

immutability-helper 是一个深度复制的代码

卸载

tyarn remove react-dnd react-dnd-html5-backend immutability-helper

常见问题:如果发现一直显示不了页面,那么要删除.umi临时文件夹,如果把不保险,把node_modules也删除了。 重新运行环境。

2. 基本概念

React DnD 与大多数拖放库不同,如果您以前从未使用过它,它可能会令人生畏。但是,一旦您了解了其设计核心的一些概念,它就会开始变得有意义。

2.1 项目与类型

React DnD 使用数据而不是视图作为事实的来源。当您在屏幕上拖动某物时,我们并不是说正在拖动组件或 DOM 节点。相反,我们说某种类型项目正在被拖动。

① Items

例如下面每个图片的格子或者棋盘上的格子,都可以称作 Item。

当您拖动卡片时,项目可能看起来像{ cardId: 42 }。在国际象棋游戏中,当您拿起棋子时,该物品可能看起来像{ fromCell: 'C5', piece: 'queen' }将拖动的数据描述为普通对象可以帮助您保持组件解耦并且彼此不知道。

② Types

每个 Item 都会属于一个类型。类型的作用是什么呢?

相同 type 的 Item 可以被统一的逻辑进行处理。

  • 例如某个区域,可以接受的拖放类型。

2.2 监视器

监视器 Monitors 允许您更新组件的道具以响应拖放状态的变化。

对于需要跟踪拖放状态的每个组件,您可以定义一个收集函数【collect】,从监视器检索它的相关位。React DnD 然后负责及时调用您的收集函数并将其返回值合并到组件的 props 中。

下面代码,使用了 Hooks,其中定义了一个collect收集函数,并将监视器monitor中的内容返回到最上层[{isDragging}, drag],然后将这些内容,传递给组件。

const [{ isDragging }, drag] = useDrag(() => ({
type: ItemTypes.KNIGHT,
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}));

2.3 连接器

React-dnd 中又两种方式:Connect 与 useHooks,推荐是用 Hooks 作为与现有组件的挂钩。

例如下面的代码,使用ref就与drag进行挂钩了。

return (
<div
ref={drag}
>
</div>,
)

当然在程序中,会经常看到这样的代码,思考一下又什么作用?:一些组件可以是拖动组件也可以是容器组件。

drag(drop(ref));

2.4 拖动与放下

【Drag Sources and Drop Targets】拖动源与放置目标。

2.5 后端

Backends,React DnD 使用HTML5 拖放 API。这是一个合理的默认设置,因为它会截取拖动的 DOM 节点并将其用作开箱即用的“拖动预览”。当光标移动时,您不必进行任何绘图,这很方便。此 API 也是处理文件放置事件的唯一方法。

不幸的是,HTML5 拖放 API 也有一些缺点。它不适用于触摸屏,并且它在 IE 上提供的定制机会比其他浏览器少。

这就是为什么在 React DnD中以可插拔方式实现 HTML5 拖放支持的原因。你不必使用它。您可以根据触摸事件、鼠标事件或其他完全不同的事件编写不同的实现。这种可插拔的实现在 React DnD 中被称为后端

<DndProvider backend={HTML5Backend}>.....</DndProvider>

2.6 Hooks

为了是 Hooks 的天下,忘记高阶函数与装饰器吧。

如果您不熟悉 React hooks,请参阅 React 博客文章Introducing Hooks

3 国际象棋案例

官方的例子详细解释,这里只把重点的给出来。 完整的代码

完成静态部分后,再添加拖放交互。

3.1 前期静态部分

主要有以下步骤:

  • 创建项目:例如 create-react-app
  • 构建游戏:不考虑拖放交互,先写一个静态的内容
    • 识别组件:做概念上设计 Knight,我们孤独的骑士片;Square, 棋盘上的一个正方形;Board,整板有 64 个方格。
    • 创建组件:写 React 脚本
    • 添加游戏状态:通过 state 让棋子可以动起来。

3.2 设置拖放上下文

将棋盘Board中用DndProvider包裹。

import React from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
function Board() {
/* ... */
return <DndProvider backend={HTML5Backend}>...</DndProvider>;
}

3.3 定义拖动类型

可以单独定义一个文件,叫做ItemTypes.ts

export const ItemTypes = {
KNIGHT: 'knight',
};

3.4 使骑士可拖动

使用了useDrag,并用ref={drag}将骑士的div关联了起来。

import React from 'react'
import { ItemTypes } from './Constants'
import { useDrag } from 'react-dnd'
function Knight() {
const [{isDragging}, drag] = useDrag(() => ({
type: ItemTypes.KNIGHT,
collect: monitor => ({
isDragging: !!monitor.isDragging(),
}),
}))
return (
<div
ref={drag}
style={{
opacity: isDragging ? 0.5 : 1,
fontSize: 25,
fontWeight: 'bold',
cursor: 'move',
}}
>
</div>,
)
}
export default Knight

实现完毕的效果如下:

3.5 使棋盘方块可放置

现在Knight是一个拖动源,但还没有放置目标来处理放置。我们现在要制作Square一个放置目标。

import React from 'react'
import Square from './Square'
import { canMoveKnight, moveKnight } from './Game'
import { ItemTypes } from './Constants'
import { useDrop } from 'react-dnd'
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%',
}}
>
<Square black={black}>{children}</Square>
{isOver && (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
zIndex: 1,
opacity: 0.5,
backgroundColor: 'yellow',
}}
/>
)}
</div>,
)
}
export default BoardSquare

实现的效果如下:

3.6 添加拖动预览图像

useDrag中添加preview预览效果

const [{ isDragging }, drag, preview] = useDrag(() => ({
type: ItemTypes.KNIGHT,
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}));

如果将preview与组件关联呢?

<DragPreviewImage connect={preview} src={knightImage} />react-dnd中定义了一个DragPreviewImage组件

import { CSSProperties, FC } from 'react';
import { DragPreviewImage, useDrag } from 'react-dnd';
import { ItemTypes } from './ItemTypes';
import { knightImage } from './knightImage';
const knightStyle: CSSProperties = {
fontSize: 40,
fontWeight: 'bold',
cursor: 'move',
};
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}
style={{
...knightStyle,
opacity: isDragging ? 0.5 : 1,
}}
>
</div>
</>
);
};

也可以使用自定义的一个 div,例如下面的例子

mport { useDrag } from 'react-dnd'
function DraggableComponent(props) {
const [collected, drag, dragPreview] = useDrag(() => ({
type,
item: { id }
}))
return collected.isDragging ? (
<div ref={dragPreview} />
) : (
<div ref={drag} {...collected}>
...
</div>
)
}

4. 函数一览

分类名称说明备注
通用组件DndProviderDnd 提供者
DragPreviewImage拖动预览图像
钩子 APIuseDrag使用拖拽钩子
useDrop使用放置钩子
useDragLayer使用拖动层钩子
useDragDropManager使用拖放管理器钩子开发/测试挂钩
监控状态DragSourceMonitor拖动源监视器
DropTargetMonitor放置目标监视器
DragLayerMonitor拖动层监视器

5. 官方例子

例子源代码

> yarn install
> yarn build
> yarn start

5.1 放置

  • 单一目标
    • 最简单的拖放示例
  • 在 iframe 内
    • 同上示例,但嵌套在 iframe 中。
  • 复制或移动
    • 演示了可以接受复制和移动放置效果的放置目标,用户可以通过在拖动时按住或释放 alt 键在这些效果之间进行切换。例如,在待办事项列表应用程序中,默认拖放操作可用于对列表进行排序,同时在拖放时按住 alt 键可以将待办事项项复制到放置目标而不是移动它。
  • 多个目标
    • 它演示了单个放置目标如何接受多种类型,以及这些类型如何从 props 派生。它还演示了本机文件和 URL 的处理(尝试将它们放到最后两个垃圾箱中)。
  • 压力测试
    • 这个例子与上一个类似,但拖拽源和放置目标的 props 每秒都在变化。它表明 React DnD 会跟踪变化的 props,如果组件接收到新的 props,React DnD 会重新计算拖放状态。它还展示了自定义isDragging实现如何使拖动源显示为已拖动,即使启动拖动的组件已收到新的道具。

5.2 拖动

  • 幼稚的
  • 自定义拖动层
    • 该库提供了一个 DragLayer可用于在应用程序顶部实现一个固定层,您可以在其中绘制自定义拖动预览组件。请注意,如果我们愿意,我们可以在拖动层上绘制一个完全不同的组件。这不仅仅是一个截图。

5.3 嵌套

  • 拖动源

    • 您可以将拖动源相互嵌套。如果嵌套拖动源false从返回canDrag,则将询问其父级,直到找到并激活可拖动源。只有激活拖动源将拥有beginDrag()endDrag()调用。
  • 放置目标

    • 放置目标也可以相互嵌套。与拖动源不同,多个放置目标可能会对被拖动的同一个项目做出反应。React DnD 的设计没有提供停止传播的方法。

5.4 可排序

  • 简单的
    • 提供了能够创建可排序列表的工具。为此,请使同一组件同时成为拖动源和放置目标,并在hover处理程序中重新排序数据。
  • 外出时取消
    • 在这个例子中,我们不是在放置目标的drop()处理程序中移动卡片,而是在拖动源的endDrag()处理程序中进行。这让我们检查monitor.didDrop()并恢复拖动操作,如果卡片被放置在其容器之外。
  • 压力测试
    • React DnD 可以同时处理多少个项目?此列表中有一千个项目。通过一些优化,比如在requestAnimationFrame回调中更新状态,它可以处理几千个项目而不会滞后。在那之后,你最好使用像fixed-data-table这样的虚拟列表 。

5.5 定制

  • 处理和预览
    • 允许您选择可拖动节点,以及组件render函数中的拖动预览节点。
    • 拖动把手或者预览图片
  • 掉落效果
    • 某些浏览器允许您为可拖动项目指定“放置效果”。在兼容的浏览器中,当您将第一个框拖到放置区上时,您将看到一个“复制”图标。

5.6 拖动文件

① ② ③④⑤⑥⑦⑧⑨