拖拽组件:React-DnD用法及源码解析

·  阅读 11608
拖拽组件:React-DnD用法及源码解析

技术必会过时,设计思想永存

一、使用场景

拖拽的业务使用场景已经渗透在了我们各个方面,尤其在移动端方便。侧面拖拽菜单,拖拽卡片交互,拖拽评分等等。

这样的交互使得我们在有限的事件场景中,给予用户更方便的交互操作。大大的提升了用户体验、产品的流畅度。

二、React-DnD简介

React-DnD是一组React实用程序,可帮助您构建复杂的拖放界面,同时保持组件之间的耦合。它非常适合TrelloStorify之类的应用程序,其中拖动可在应用程序的不同部分之间传输数据,并且组件可以响应拖放事件更改其外观和应用程序状态。

如上图的团队任务合作平台很多公司都在使用。React-DnD是这一类业务场景的优秀开源解决方案。

接下来我们先介绍一下它的使用方法。

三、使用方法

3.1 安装

安装的时候我们需要同时安装backendreact-dnd

为什么与要这样设计呢,后面源码解析的时候会详细说明。

3.2 DndProvider注入

DndProvider组件为您的应用程序提供React-DnD功能。必须通过backendc参数将其注入后端,但是也可以将其注入window对象。

backend后端是React-DnD中非常好的一种设计方法。可以理解为具体拖拽的实现方式。

DndProvider api

  • backend: 必填,dnd后端可以使用官方的提供的两个 HTML5Backend or TouchBackend,或者也可以自己写backend后端。
  • context: 选填,用户配置后端的上下文,这取决于后端的实现。
  • options: 配置后端对象,自定义时可以传入backend。后面有例子。

3.3 useDrag 声明拖动源

userDrag用于将当前组件用作拖动源的钩子。

其中useDrag返回的参数有

  • arguments[0]: 一个对象,其中包含从collect函数收集的属性。如果collect未定义函数,则返回一个空对象。
  • arguments[1]: 拖动源的连接器功能。这必须附加到DOM的可拖动部分。
  • arguments[2]: 用于拖动预览的连接器功能。这可以附加到DOM的预览部分。

然后useDrag传入的参数有

  • item: 必填。一个普通的JavaScript对象,描述了要拖动的数据。这是可用于放置目标的有关拖动源的唯一信息
  • item.type: 必填,并且必须是字符串,ES6符号。只有注册为相同类型的放置目标才会对此项目做出反应
  • previewOptions: 选填。描述拖动预览选项的普通JavaScript对象
  • options: 选填,一个普通的对象。如果组件的某些道具不是标量的(即不是原始值或函数),则arePropsEqual(props, otherProps)在options对象内部指定自定义函数可以提高性能。除非您有性能问题,否则不要担心。
  • begin(monitor):选填,拖动操作开始时触发。不需要返回任何内容,但是如果返回对象,它将覆盖item规范的默认属性。
  • end(item, monitor):选填,拖动停止的时候,end将会被调用。
  • canDrag(monitor):选填。使用它可以指定当前是否允许拖动。默认允许
  • isDragging(monitor):选填。默认情况下,只有启动拖动操作的拖动源才被视为拖动
  • collect:选填,收集功能。

3.4 useDrop 声明放置源

useDrop用于使用当前组件作为放置目标的钩子。

其中useDrop返回的参数有

  • arguments[0]: 一个对象,其中包含从collect函数收集的属性。如果collect未定义函数,则返回一个空对象。
  • arguments[1]: 拖动源的连接器功能。这必须附加到DOM的可拖动部分。

然后useDrag传入的参数有

  • accept: 必填。字符串,ES6符号,其中一个的数组或返回给定组件的其中一个的函数props。此放置目标将仅对由指定类型的拖动源产生的项目作出反应。
  • options: 选填。一个普通的对象。如果组件的某些道具不是标量的(即不是原始值或函数),则arePropsEqual(props, otherProps)在options对象内部指定自定义函数可以提高性能。除非您有性能问题,否则不要担心。
  • drop(item, monitor): 选填。当兼容项目放在目标上时调用。您可以返回undefined或纯对象。如果返回一个对象,它将成为放置结果,并且可用于其拖动源中的endDrag方法monitor.getDropResult()。如果您要根据接收到目标的目标执行不同的操作,这很有用。如果您有嵌套的放置目标,则可以drop通过检查monitor.didDrop()和来测试嵌套目标是否已经处理monitor.getDropResult()。此方法和源endDrag方法都是触发Flux动作的好地方。如果canDrop()已定义并返回,则不会调用此方法false。
  • hover(item, monitor): 选填。将项目悬停在组件上时调用。您可以检查monitor.isOver({ shallow: true })测试悬停是否发生过只是当前的目标,或通过嵌套一个。与drop()此方法不同的是,即使canDrop()已定义并返回该方法也将被调用false。您可以检查monitor.canDrop()是否是这种情况。
  • canDrop(item, monitor): 选填。使用它来指定放置目标是否能够接受该物品。如果要始终允许它,则只需忽略此方法。
  • collect:选填,收集功能。

3.5 效果展示

如上面的代码展示,所能达到的效果为下方gif图片所示。

完整的Demo示例: 传送门

四、源码分析。

从源码上来讲解一下React-DnD中的优秀设计模式及原理。

4.1 设计架构

我们先从源码目录上来解析一下。

分为了三个部分

  • backend 后端部分。(就是具体场景的dom操作)
  • dnd-core 核心。
  • react-dnd 封装react插件。

这样我们先从上面例子中使用的api为起点看一下源码是如何封装的,然后慢慢深入核心。

4.2 DndProvider源码

代码整体为typescript写的。我们来分模块看一下。

DndProvider组件可以看出,核心返回的是DndContext组件。跟其他的容器组件一样,封装了一层React Context(上下文),为了传递共享值下去。这让我想起了redux的Provider是一样的设计模式。

(React.useEffect)这里还一个优化逻辑,如果全局检测已经有了实例,那么清除本次实例。

Provider传递的值为manager,manager是通过getDndContextValue方法获取的。

如上代码所示,getDndContextValue方法创建了一个获取manager单例,属性有 backend(后端)context(后端上下文),options(后端options),debugMode。

接下来为了验证我们的猜测,看一下DndContext组件代码。

果然是通过React.createContext创建上下文。

并且创建了拖动拖放实例,可以理解为总实例,后面我都将用总实例来代替这个名称

这里面可以看到dnd-core核心组件,从dnd-core中解析除了DragDropManager(总实例)BackendFactory(后端工厂)createDragDropManager(创建总实例)

dnd-core源码后面会讲到,我们还是先根据使用的api慢慢深入讲解。

4.3 useDrag

useDrag 拖动源,上面使用useDrag的时候可以看到的是返回了三个参数,我们看下函数

三个参数分别为result,connectDragSource(拖动源的连接器),connectDragPreview(链接预览功能);

先说一下预览功能,这是React-DnD提供的一个优化拖拽体验的一个api(ragPreviewImage),这个api描述为:

将HTML Image元素呈现为断开的拖动预览的组件。

使用Demo为:

展现效果为:

上面拖动时候展示的马头图片即为preview模块所提供的功能。

我们继续讲,useDrag部分我们会接触到一个叫connector连接器的模块,连接器是什么呢,大家可以先想像成一个数据的总线的钩子。上述代码中通过useDragSourceMonitor方法中获取。

上面这个图是两个不一样地方的方法拼在一起,为了方便一起观看,可以看出useDragSourceMonitor方法返回了两个模块,monitor(监听),connector(连接器,并且获取的是manager.getBackend后端的数据)。

manager实例是通过useContext获取的,就是上面Provider注入的总实例。

这样我们可以知道的是,dragSource拖动源实例是通过connector连接器中钩子获取到的,连接器在useDragSourceMonitor中通过sourceConnector传入总实例中的backend实例获取到。

sourceConnector中通过React.Ref参数获取到了dragSourceRef即DOM节点。

同时后面还有很多私有的边界处理方法,例如:

看到这里有些人可能会注意到一个用法,reconnect,这个模式比较有意思,重新链接,每次更新完数据都要重新建立链接钩子,这一块可以在dnd-core模块讲解的时候说到,个人猜测是因为dnd-core模块使用了redux

上面这一套就打通了拖动源 - 后端 - 总实例。

4.4 useDrop

useDrop 放置源,还是从使用的api上来看,函数返回了两个参数,我们看一下方法。

源码中返回了result,connectDropTarget。与useDrag一样,只不过少了个preview实例,放置源不需要这个参数。

connectDropTarget与connectDragSource原理一样

打通了 放置源 - 后端 - 总实例。

接下来我们继续深入分析dnd-core源码模块。

4.5 DragDropManager

React-DnD使用数据而不是视图作为事实来源,当在屏幕拖动某些东西的时候,并不是正在拖动组件或者DOM节点。而是通过数据模拟preview让拖动源“正在被”拖动。dnd-core正式围绕着数据为核心,并且React-DnD内部使用了Redux。我们继续分析。

dnd-core核心模块最主要的就是总实例了,看一下总实例的构建函数。

其中总实例是通过 DragDropManagerImpl(总实例的实现类) 继承下来的,并传入了debugMode开关。

后端实例backedn通过后端工厂创建。manager安装后端实例。

看一下DragDropManagerImpl源码部分,有点长,本想删一点,但确实都很精髓。

从上往下拆分来看。

makeStoreInstance,创建Redux实例,想必能看到这里但都应该知道redux状态管理的原理。这里面哟一个debugMode参数用来调试redux的,在创建总实例的时候传入。reduxDevTools是谷歌扩展程序,需要自行安装。

DragDropManagerImpl 是通过DragDropManager接口严格继承而来。

看一下实现类中的构造函数

首先创建了redux实例获取store并绑定到成员属性,继承DragDropMonitorImpl监听实现类,传入store,传入注册实现类。获取注册信息并绑定到store中。订阅handleRefCountChange函数。

订阅的handleRefCountChange函数如下

这里面的count为后端拖动拖动源的总次数。

接下来,处理actions逻辑,并添加DragDropActions

绑定dispatch成员函数

这样,Redux三件套处理完毕,可以正常运转了。

其他的位置都是绑定成员属性函数。

我们来继续顺着添加DragDropActions逻辑深入,绑定了哪些Actions呢?我们来看一下。

可以看出,绑定了的Actions有

  • beginDrag(开始拖动)
  • publishDragSource(发布当前拖动源)
  • hover(是否经过)
  • drop(落下动作)
  • endDrag(拖拽结束)

重点看一下beginDrag模块。

可以看出beginDrag为action格式。

其中我们看到了React-DnD数据驱动的核心概念XYCoord坐标,修改坐标方法 setClientOffset

看一下setClientOffset方法,也是action。

坐标接口(interface

总结上面我们分析的片段,ReactDnD通过坐标形式的接口,来控制拖拽源的preview位置,如果判断可以落下再把拖拽源移动过去。

配合边界函数和多数逻辑判断,封装了dnd-core核心逻辑(数据驱动)

那么具体实现拖拽的实现和事件是在哪里处理的呢?

4.6 backend

React DnD建立在HTML5拖放API之上。这是一个合理的默认值,因为它可以对已拖动的DOM节点进行屏幕快照,并将其用作开箱即用的“拖动预览”。方便的是,您不必在光标移动时进行任何绘制。该API也是处理文件删除事件的唯一方法。

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

这就是为什么在React DnD中以可插入方式实现HTML5拖放支持的原因。您不必使用它。您可以根据触摸事件,鼠标事件或完全其他事件来编写其他实现。这种可插拔的实现在React DnD中称为后端。该库仅随附HTML5后端,但将来可能会添加更多。

后端的作用类似于React的综合事件系统:它们抽象化浏览器差异并处理本地DOM事件。尽管有相似之处,但是React DnD后端并不依赖于React或其合成事件系统。在后台,所有后端都将DOM事件转换为React DnD可以处理的内部Redux动作。

后端的设计模式就是为了处理上述场景,并且React-DnD支持自定义backend,如果你的业务场景足够特殊的化,不妨自己写个后端。

后端就是事件的实现

五、END

拖拽的形式多种多样,插件种类层出不穷。看到还有人造了React-DnD的轮子,非常优秀。如果本篇文章对您有所帮助,请点赞👍支持下~, 你的赞是我持续创作的动力~

感谢贡献开源项目的开发者

往期文章推荐:

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改