技术必会过时,设计思想永存
一、使用场景
拖拽的业务使用场景已经渗透在了我们各个方面,尤其在移动端方便。侧面拖拽菜单,拖拽卡片交互,拖拽评分等等。
这样的交互使得我们在有限的事件场景中,给予用户更方便的交互操作。大大的提升了用户体验、产品的流畅度。
二、React-DnD简介
React-DnD是一组React实用程序,可帮助您构建复杂的拖放界面,同时保持组件之间的耦合。它非常适合Trello和Storify之类的应用程序,其中拖动可在应用程序的不同部分之间传输数据,并且组件可以响应拖放事件更改其外观和应用程序状态。
如上图的团队任务合作平台很多公司都在使用。React-DnD是这一类业务场景的优秀开源解决方案。
接下来我们先介绍一下它的使用方法。
三、使用方法
3.1 安装
安装的时候我们需要同时安装backend与react-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的轮子,非常优秀。如果本篇文章对您有所帮助,请点赞👍支持下~, 你的赞是我持续创作的动力~
感谢贡献开源项目的开发者。
往期文章推荐: