思考如何设计一个通用的拖拽排序库,从sortablejs到0开
如何判断你是否需要0开这样一个东西?
以我个人的感受来说,大多数场景下,拖拽这个功能本身就是一个,付出和收益完全不成正比的东西,因为单纯的拖拽本身很好做到,简单来说就是让某个盒子附着到鼠标上进行移动,但是想要做好做完善却相当难
对于用户来说,拖拽使用本身很简单,可是要求和场景可能是各式各样千奇百怪的。这意味着只是单纯的做到还不行,还得考虑各种边界条件,和用户体验的问题,而为了做到这些,过程本身和已有的功能可能还会产生某种程度的冲突
所以尽量不要因为某些库不尽如人意就轻易放弃了,能用就多坚持下吧,如果没有做好准备就打算0开自己写,个人经验这就是个火坑
sortablejs 从使用到放弃
因为公司业务对拖拽有着强需求和高要求,我开始了相关尝试,以我目前找到的可选项来说,sortablejs
是最里边最轻量最灵活的。公司项目用的 Vue3
可是相关 vue3
的拖拽库我认为当下还是有所欠缺,所以 sortablejs
是我最初的选择
此处对场景的分类进行精简,我目前的业务如下
- 拖拽收藏夹中,连续又普通列表里的手柄,进行交换
- 拖拽表格,列或行,进行交换
难点如下
- 收藏夹列表里每一条点击是会跳转的,这要求拖拽手柄时要停止时间冒泡,意味着我需要能在拖拽时拿到事件对象进行某些操作
- 表格列哪些能拖我需要是动态可配,它可能是不连续的,比如4个列,我只能拖动1和4两列
- 不是每个UI库的表格,都有办法知道什么时候渲染完成了所有显示列,可是我需要初始化绑拖拽逻辑我又必须得能知道
- 当列表内容发生变化,dom节点本身,以及列表数量可能就变掉了,需要初始化一次,后续每次都要动态走一遍绑定流程或者判断
- 因为能拖拽的内容可能不连续,所以需要在拖完后自己拿开始和结束的节点,自定义找下标进行交换
- 需要在拖拽前能动态根据自定义事件来过滤,否则那些和被拖拽的内容本身存在冲突,需要动态决定当前下是否允许拖拽
- sortablejs 在交换表格列时,有些库会出现内容错位,在使用虚拟滚动表格时,越是靠后的元素,越是百分百复现(有可能是ui库表格的计算冲突,因为基本都会监听表格的尺寸和位置变化的)
以上问题基本处于叠加态,大多数场景下我会需要同时处理以上所有问题
sortablejs
本身使用起来很简单,而对于我的需求来说,它会变成一个模棱两可的状态
因为它本身提供了诸多开箱即用功能,可是配套的相关文档和例子并不详细,文档我看的是github
的仓库里的,如果说这不是最新的我也无法反驳了
用 sortablejs
写时各种辛酸泪,也是无力吐槽了,最终导致的结果是,看文档我觉得它做不到,0开自己写,无意间翻看源码,渐渐的感觉应该是能推导出,通过某些特殊的做法,可能,也许,大概,也能做到我的需求吧,不过这个时间点我已经做了个小型的,能解决我所有问题的小库了
明确最终的拖拽驱动方式
设计结构前必须要明确一个前提,就是我们最终想要的效果,它的驱动按照我自己的花可以分为
- dom 驱动
- 关系 驱动
DOM驱动本质是,先拖拽DOM在通过关系同步数据
好处在于,能够提供更多开箱即用的,我们大概率会用得到的功能。 sortablejs
就属于此类
缺点是对于特殊要求的支持比较弱,因为拖动的逻辑本身是写死在库中的,拖动过程还会伴随着各种动画效果。尽管库中会提供各种配置,如果恰巧有些功能没有,那就只能自行发挥或者妥协了
关系驱动本质是,他只处理拖动前后的位置关系,而不会改变相关的 DOM
好处是它和数据驱动的框架会更加契合(Vue
,React
),因为只是处理关系结构,拿到最终改变后的关系用户能够随意处理,不存在所谓 DOM 变化了,数据同步异常的问题,灵活性会相当的高
缺点是变得不在那么的开箱即用了,比如没有了动画,不会交换DOM,很多功能都得自己手写,用起来很麻烦
关系驱动的补足
拖拽驱动方式我没找到类似的说法,目前应该是属于是我发明的词,这也是我最终的选择
对于大多数需求来说,变换dom驱动的 sortablejs
使用方式也是勉强能做的,只是对于我的需求来说这种方式会更加适合我,对于相比之下缺失的功能,如果动画,交换,等等的解决方式也很简单
说白了就是在设计好的拖拽结构的每个节点上,设置相应的插件系统,通过不同的插件我也能做到dom
级别立即生效的结果
这里最特殊的是动画,因为关系驱动的动画,基本只能交给开发者自己搞,不过好在vue
有 transition-group
动画组件
可以认为如果设计的足够好,关系驱动的结构是能覆盖dom驱动绝大多数的场景,反之则很困难,甚至做不到,但灵活的代价就是复杂和麻烦了
拖拽的结构设计
明确了驱动行为,我们就可以针对性的做相关的结构设计,根据我观察到的经验可以分成以下部分
- 容器
- 筛选
- 惰性
- 属性
- 事件
- 滚动
- 预设
- 插件
这里每个部分都能做单独的设计,其中我认为必须要解决的是
- 容器
- 筛选
为了扩展要考虑的是
- 插件
- 预设
为了更好的使用体验要考虑的是
- 惰性
- 属性
- 事件
- 滚动
容器
容器可以分成 3 个
- 临时范围容器
- 拖拽容器
- 滚动容器
比如我提到的,不知道表格的列是在什么时候渲染完成的,就可以通过惰性判断的方式实现
我们可以配置一个临时范围容器,这里可以是最外层的表格。我们给表格加鼠标按下事件,触发的同时在进行可拖拽内容的收集,以及拖拽事件的初始化
拖拽容器就是包含了一坨子能拖拽元素的盒子,拖拽排序的前提是有序,而一个盒子内的个别可拖拽元素则是最小排序单位
滚动容器通常就是拖拽容器,我们需要判断是否和滚动容器的可视化边界靠的足够近,此时需要能够自动滚动才行
惰性筛选
对于能否拖拽的筛选类型可能有很多,大的方向有
- 我的哪些列在这次排序中,是允许被拖拽的
- 我当前操作的元素是否允许被拽拖,如果允许,那我应该合适开始拖
为了更好的用户体验,设计 API 时可以提供些内置的预设
通过配置 class/style/attrs/...
选项就能自动筛选出,哪些是能拖拽的,哪些是不能的
拖拽行为
实现拖拽有 2 种方式
- 使用浏览器的拖拽事件(我用的)
- 自己搞一套,这会更加的灵活
浏览器提供的拖拽事件,搭配上一些其他自定义事件,我个人认为是完全够用了
我想也许很多人没用过自带的拖拽事件,这里简单概述下都能做到哪些行为
- 通过 dom 属性允许节点能被拖拽,拖拽样式是现成的,是否禁用的判断也是现成的
- 能知道在拖拽的是哪个节点
- 能知道被拖放到哪个节点上了(不是所有地方都能放置的)
- 能知道,哪些走浏览器的拖拽事件的元素,是否正在被拖拽,进入到了自己的头上,或者离开了
通过这些事件我们就能知道,从拖拽开始,到移动,到进入离开,到最终放下
那么我们要做的就是在这些阶段前后设置插件机制即可,各种行为大多都可以交给插件实现
会不会放出来给大家用
目前是做了一版,只能说是,如果只针对我的场景来说,够用,好不好用不见得
写了很多,设计了很多,但其实我也没有全部都做到,对于各种边界条件和设计还需要时间和业务上的沉淀,当然没空专心搞也是一方面
抽空写篇文章也想看看有没有对我想法进行补足,或者不同见解的,欢迎讨论
转载自:https://juejin.cn/post/7387999151413215267