为什么前端需要拖拽库?

151 阅读7分钟

前言

拖拽是做前端经常会接触到的一个需求,在处理拖拽相关问题的时候,基本都是用各种各样的库解决的。而很少会用在初学前端阶段了解到的浏览器原生的拖拽API,于是我很好奇,这些拖拽库与原生API之间究竟有哪些差别呢?拖拽库究竟帮我处理了什么问题呢?出于这个目的把探索中的一些发现记录在这里。接下来的内容会先对浏览器原生API进行简单介绍,然后分析为什么需要更高层的封装来方便用户使用。

HTML5 drag and drop API简介

拖拽交互有规范的 html5 drag & drop API(下文中简称原生API),在元素的DOM节点上添加 draggable 属性就可以把一个节点变成可拖拽的元素,并且在拖拽开始时会触发dragstart事件,拖拽过程中会不断触发 drag 事件,拖拽结束时会触发 dragend 事件。当拖拽这个节点到可放置的目标节点上时会先触发 dragenter 事件,在这之后如果没有离开就会持续触发 dragover 事件,而如果节点在此时释放则会在目标节点触发 drop 事件。而如果进入后又离开则会触发 dropleave 事件。通过dataTransfer对象可以获取本次拖拽中的数据信息。更详细的介绍可以查阅 MDN 了解。

image.png

接下来首先会从触发方式,事件系统,交互效果三个方面来分析原生API存在哪些问题,然后会从使用场景的角度分析拖拽库除了单纯拖拽以外还需要提供哪些能力。

触发方式

在原生API的实现中,想要触发 dragstart 事件,需要通过 mouse 鼠标来触发,也就是说移动端的触摸屏设备是触发不了拖拽事件的。如果需要支持触碰设备,那么需要用户自行通过 move 事件来模拟拖拽相关的事件。而构建这样一套模拟是比较复杂的,如果有兴趣了解如何通过 move 来模拟 drag 行为推荐阅读 react-dnd 库中 packages 目录下的 backend-touch 这个文件夹,虽然这个库是给 react 使用的,但是 backend-touch 的实现是与框架无关的。

事件系统

原生API提供的事件精细程度要超过绝大多数拖拽库向外暴露出来的拖拽事件。不过精细的背后也是更高的理解成本以及使用成本,原生API最大的问题集中在放置目标有关的dragover这个事件上

dragover触发频率

如果尝试使用drag&drop API的话,会发现dragover这个事件的触发次数非常的高,可能只是短暂经过某个元素就发现这个事件触发了好几百次。如果对应事件的处理函数复杂的话会有非常多重复的计算,所以一般需要对这个事件进行防抖或者节流处理。

dragover触发对象

第一个问题是直接使用原生API时dragover事件在经过任意dom元素的时候都会触发,但在实际的应用场景中,需要感知draggable元素的droppable元素是有限的。这也就意味着直接使用原生API时,需要在事件处理函数中根据该元素是否为我们预先指定的droppable元素来决定是否执行具体的拖拽业务逻辑。也因此大部分拖拽库都提供了方法让我们可以指定可以放置的容器。

另外一个问题是如果存在两个嵌套的droppable容器,在直接使用原生API的时候,我们会分别接收到两个容器各自传来的draggover事件,这时其实无法分辨元素是同时触发了这两个元素的dragover事件还是draggable元素先经过了某个droppable元素而后又经过了另一个droppable元素。对于这种嵌套的场景,可以考虑将这两个事件整合成一个,并把over的对象以数组的形式传给用户,这样会更加容易处理。

image.png

交互效果

交互效果主要讨论拖拽预览、拖拽范围两个方面的问题。事实上原生API还会设计到cursor样式控制的一些问题,但是这个效果个人感觉比较小众这里不作讨论。

拖拽预览

原生的拖拽预览元素的形状与原来dom的样式基本一致,区别在于加上了不可控制的大约数值0.8的 opacity 透明度,以及阴影效果。这种效果本身表现是可接受的,不过如果UX有另外的要求,定制起来会比较复杂。原生API可以通过拖拽事件的 dataTransfer 对象上的 setImageDrag 方法来定制拖拽效果,不过 setImageDrag 在 dragstart 后就没有办法修改再修改拖拽预览了,也就是说无法在拖拽的过程中再让元素的样式动态变化。鉴于原生API的种种限制,有复杂拖拽过程效果需求的话,最后采用的方案往往是舍弃浏览器默认的拖拽效果,自行创建一个dom元素作为拖拽预览。

拖拽范围

在某些拖拽场景中,可能用户会希望不要把某个可拖拽元素拖出某个容器之外,或者是希望可拖拽元素只沿着水平方向或者垂直方向拖拽。如果使用浏览器原生API是不提供这种能力的,想要实现需要自行创建dom元素作为拖拽预览,才能在其上增加相关的限制功能。

使用场景

滚动场景

当需要拖拽的元素数量比较多时,就经常会遇到容器可以滚动的情况。这时拖拽库是需要额外做一些处理才能让用户拥有丝滑体验的。

自动滚动

在列表中进行拖拽时,直接使用原生API拖拽的话,同时直接触发滚动条移动到自己想要的位置是一个操作难度挺高的事情。也就是说如果想把第一个元素拖到最后一个,用户最保险的操作是需要先把它移动到当前可视区域的底部,然后向下滚动滚动条,继续循环上面的步骤。这种交互在真实世界里是无法接受的,所以很多拖拽库提供了当用户进行拖拽时滚动条自动滚动的能力。当拖拽位置即将到达底部或者顶部时,会通过js代码改变滚动条的位置,拖拽位置越靠近底部滚动条改变的速度越快。

虚拟滚动

虚拟滚动是一种只渲染可见元素的优化技术,想要了解更多的读者可以参考《虚拟滚动原理解析》。在虚拟滚动中,由于只渲染可见的元素,那么随着拖拽中发生的滚动操作,可能会发现我们之前拖拽的元素的dom已经被卸载掉了。这个时候由于可拖拽元素已经消失,dragend事件不会再触发,而可放置元素仍然存在,dragover和drop事件仍然能够触发。为了让整个拖拽的效果与常规拖拽保持一致,也就是dragend中的逻辑仍然被执行,需要在drop事件中手动触发dragend事件的处理函数。

动画效果

在拖拽过程中,为了让用户感觉更加自然,UX往往会希望会有一些空间位置交换的动画效果,提供好的动画效果也能有效提升一个拖拽库的竞争力。

参考资料