likes
comments
collection
share

思考如何设计一个通用的拖拽排序库,从sortablejs到0开

作者站长头像
站长
· 阅读数 35

如何判断你是否需要0开这样一个东西?

以我个人的感受来说,大多数场景下,拖拽这个功能本身就是一个,付出和收益完全不成正比的东西,因为单纯的拖拽本身很好做到,简单来说就是让某个盒子附着到鼠标上进行移动,但是想要做好做完善却相当难

对于用户来说,拖拽使用本身很简单,可是要求和场景可能是各式各样千奇百怪的。这意味着只是单纯的做到还不行,还得考虑各种边界条件,和用户体验的问题,而为了做到这些,过程本身和已有的功能可能还会产生某种程度的冲突

所以尽量不要因为某些库不尽如人意就轻易放弃了,能用就多坚持下吧,如果没有做好准备就打算0开自己写,个人经验这就是个火坑

sortablejs 从使用到放弃

因为公司业务对拖拽有着强需求和高要求,我开始了相关尝试,以我目前找到的可选项来说,sortablejs 是最里边最轻量最灵活的。公司项目用的 Vue3 可是相关 vue3 的拖拽库我认为当下还是有所欠缺,所以 sortablejs 是我最初的选择

此处对场景的分类进行精简,我目前的业务如下

  • 拖拽收藏夹中,连续又普通列表里的手柄,进行交换
  • 拖拽表格,列或行,进行交换

难点如下

  • 收藏夹列表里每一条点击是会跳转的,这要求拖拽手柄时要停止时间冒泡,意味着我需要能在拖拽时拿到事件对象进行某些操作
  • 表格列哪些能拖我需要是动态可配,它可能是不连续的,比如4个列,我只能拖动1和4两列
  • 不是每个UI库的表格,都有办法知道什么时候渲染完成了所有显示列,可是我需要初始化绑拖拽逻辑我又必须得能知道
  • 当列表内容发生变化,dom节点本身,以及列表数量可能就变掉了,需要初始化一次,后续每次都要动态走一遍绑定流程或者判断
  • 因为能拖拽的内容可能不连续,所以需要在拖完后自己拿开始和结束的节点,自定义找下标进行交换
  • 需要在拖拽前能动态根据自定义事件来过滤,否则那些和被拖拽的内容本身存在冲突,需要动态决定当前下是否允许拖拽
  • sortablejs 在交换表格列时,有些库会出现内容错位,在使用虚拟滚动表格时,越是靠后的元素,越是百分百复现(有可能是ui库表格的计算冲突,因为基本都会监听表格的尺寸和位置变化的)

以上问题基本处于叠加态,大多数场景下我会需要同时处理以上所有问题

sortablejs 本身使用起来很简单,而对于我的需求来说,它会变成一个模棱两可的状态

因为它本身提供了诸多开箱即用功能,可是配套的相关文档和例子并不详细,文档我看的是github的仓库里的,如果说这不是最新的我也无法反驳了

sortablejs 写时各种辛酸泪,也是无力吐槽了,最终导致的结果是,看文档我觉得它做不到,0开自己写,无意间翻看源码,渐渐的感觉应该是能推导出,通过某些特殊的做法,可能,也许,大概,也能做到我的需求吧,不过这个时间点我已经做了个小型的,能解决我所有问题的小库了

明确最终的拖拽驱动方式

设计结构前必须要明确一个前提,就是我们最终想要的效果,它的驱动按照我自己的花可以分为

  1. dom 驱动
  2. 关系 驱动

DOM驱动本质是,先拖拽DOM在通过关系同步数据

好处在于,能够提供更多开箱即用的,我们大概率会用得到的功能。 sortablejs 就属于此类

缺点是对于特殊要求的支持比较弱,因为拖动的逻辑本身是写死在库中的,拖动过程还会伴随着各种动画效果。尽管库中会提供各种配置,如果恰巧有些功能没有,那就只能自行发挥或者妥协了

关系驱动本质是,他只处理拖动前后的位置关系,而不会改变相关的 DOM

好处是它和数据驱动的框架会更加契合(Vue,React),因为只是处理关系结构,拿到最终改变后的关系用户能够随意处理,不存在所谓 DOM 变化了,数据同步异常的问题,灵活性会相当的高

缺点是变得不在那么的开箱即用了,比如没有了动画,不会交换DOM,很多功能都得自己手写,用起来很麻烦

关系驱动的补足

拖拽驱动方式我没找到类似的说法,目前应该是属于是我发明的词,这也是我最终的选择

对于大多数需求来说,变换dom驱动的 sortablejs 使用方式也是勉强能做的,只是对于我的需求来说这种方式会更加适合我,对于相比之下缺失的功能,如果动画,交换,等等的解决方式也很简单

说白了就是在设计好的拖拽结构的每个节点上,设置相应的插件系统,通过不同的插件我也能做到dom级别立即生效的结果

这里最特殊的是动画,因为关系驱动的动画,基本只能交给开发者自己搞,不过好在vuetransition-group 动画组件

可以认为如果设计的足够好,关系驱动的结构是能覆盖dom驱动绝大多数的场景,反之则很困难,甚至做不到,但灵活的代价就是复杂和麻烦了

拖拽的结构设计

明确了驱动行为,我们就可以针对性的做相关的结构设计,根据我观察到的经验可以分成以下部分

  • 容器
  • 筛选
  • 惰性
  • 属性
  • 事件
  • 滚动
  • 预设
  • 插件

这里每个部分都能做单独的设计,其中我认为必须要解决的是

  • 容器
  • 筛选

为了扩展要考虑的是

  • 插件
  • 预设

为了更好的使用体验要考虑的是

  • 惰性
  • 属性
  • 事件
  • 滚动

容器

容器可以分成 3 个

  1. 临时范围容器
  2. 拖拽容器
  3. 滚动容器

比如我提到的,不知道表格的列是在什么时候渲染完成的,就可以通过惰性判断的方式实现

我们可以配置一个临时范围容器,这里可以是最外层的表格。我们给表格加鼠标按下事件,触发的同时在进行可拖拽内容的收集,以及拖拽事件的初始化

拖拽容器就是包含了一坨子能拖拽元素的盒子,拖拽排序的前提是有序,而一个盒子内的个别可拖拽元素则是最小排序单位

滚动容器通常就是拖拽容器,我们需要判断是否和滚动容器的可视化边界靠的足够近,此时需要能够自动滚动才行

惰性筛选

对于能否拖拽的筛选类型可能有很多,大的方向有

  1. 我的哪些列在这次排序中,是允许被拖拽的
  2. 我当前操作的元素是否允许被拽拖,如果允许,那我应该合适开始拖

为了更好的用户体验,设计 API 时可以提供些内置的预设

通过配置 class/style/attrs/... 选项就能自动筛选出,哪些是能拖拽的,哪些是不能的

拖拽行为

实现拖拽有 2 种方式

  1. 使用浏览器的拖拽事件(我用的)
  2. 自己搞一套,这会更加的灵活

浏览器提供的拖拽事件,搭配上一些其他自定义事件,我个人认为是完全够用了

我想也许很多人没用过自带的拖拽事件,这里简单概述下都能做到哪些行为

  1. 通过 dom 属性允许节点能被拖拽,拖拽样式是现成的,是否禁用的判断也是现成的
  2. 能知道在拖拽的是哪个节点
  3. 能知道被拖放到哪个节点上了(不是所有地方都能放置的)
  4. 能知道,哪些走浏览器的拖拽事件的元素,是否正在被拖拽,进入到了自己的头上,或者离开了

通过这些事件我们就能知道,从拖拽开始,到移动,到进入离开,到最终放下

那么我们要做的就是在这些阶段前后设置插件机制即可,各种行为大多都可以交给插件实现

会不会放出来给大家用

目前是做了一版,只能说是,如果只针对我的场景来说,够用,好不好用不见得

写了很多,设计了很多,但其实我也没有全部都做到,对于各种边界条件和设计还需要时间和业务上的沉淀,当然没空专心搞也是一方面

抽空写篇文章也想看看有没有对我想法进行补足,或者不同见解的,欢迎讨论

转载自:https://juejin.cn/post/7387999151413215267
评论
请登录