Hi,大家好,在经历了两周多的“写Bug”、“改Bug”死循环后,又一个迭代终于在习以为常的加班生活中结束啦!联想到最近在Github上发起的“996.icu”事件,不禁令人由衷地感慨生活不易,所谓”起风了,唯有努力生存“。其实,我反对是加班常态化所导致的无效加班,既然努力工作是为了更好的生活,可如果因此而模糊了工作和生活的界限,这到底是一件好事还是一件坏事呢?想想每个周末被工作群里消息支配的失落感,我希望我有可以自由支配的时间,即使我看起来比别人年轻,即使我下班后依旧孤身一人,因为用时间来换钱这件事情,着实是件性价比不高的事情,货币会一天天地贬值直至我们老去,可那些失去的时间就永远地失去了。好了,”业精于勤荒于嬉“,今天我们来说前端中实现拖拽排序这件事情。

其实,这件事情说起来挺尴尬的,我们曾经为用户提供过某种”智能“的体验,我们通过对用户的行为进行分析,为其推荐了个性化的菜单项,甚至根据用户的使用频率对菜单进行了排序。可事实上用户的反响并不是非常强烈,在经过一段时间的使用后,用户依然觉得这个功能相当地”鸡肋“,这件事情告诉我们一个真相,即无论是产品设计还是需求研讨,最好不要轻易地代入用户的角色。最终的结果是我们打算为用户提供自定义的功能,考虑到操作的便利性问题,我们放弃了那种通过上下箭头按钮进行排序的方案,这样就回到了本文的主题,如何在前端中对一组列表进行拖拽排序,最终我们选定了两组方案,它们分别是NestableSortable

Nestable方案

Nestable是一个基于jQuery的插件,是一个在Github上开源的项目,据作者声称,这是一个”拖放具有鼠标和触摸兼容性的分层列表”的方案。这里针对触摸兼容性的支持可以忽略不计,因为如今都9012年了,博主依然在做传统前端页面的开发,这里博主最感兴趣的一点是,它可以支持分层列表,换言之,我们的列表元素是可以有层级关系、是可以嵌套的,唯一令人有点不爽的就是它依赖jQuery了,在这样一个连Github和Bootstrap都在努力移除jQuery的时代,没有jQuery的历史包袱,意味着我们可以大胆地去做现代前端应该做的事情。好了,我们来看看Nestable具体是怎么使用的吧!首先,我们定义一个简单的HTML结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div class="dd">
<ol class="dd-list">
<li class="dd-item" data-id="1">
<div class="dd-handle">Item 1div>
li>
<li class="dd-item" data-id="2">
<div class="dd-handle">Item 2div>
li>
<li class="dd-item" data-id="3">
<div class="dd-handle">Item 3div>
<ol class="dd-list">
<li class="dd-item" data-id="4">
<div class="dd-handle">Item 4div>
li>
<li class="dd-item" data-id="5">
<div class="dd-handle">Item 5div>
li>
ol>
li>
ol>
div>

接下来,我们可以使用如下的JavaScript代码来初始化整个列表,果然,一股jQuery风扑面而来:

1
2
3
$('.dd').nestable({
/* config options */
});

然后,我们就可以看到下面的效果:

nestablejs-demo
nestablejs-demo

怎么样?看起来效果还不错吧!不过博主在前期调研的过程中发现,它对于复杂的层级关系就无能为力啦,可能是博主打开的姿势不对吧!如果希望对列表做更深层次的定制,它需要配置的属性会非常非常的多,而且它有一套内在约束在里面,譬如className、nodeName等等,虽然这些都可以去配置,但要想像作者一样运用得好,依然是需要花费大量时间来学习它的API。

说到这里,对于Nestable,我唯二喜欢的一个feature是,它可以实时地获取到排序后的节点信息,而且是序列化后的JSON格式哦,因为当我们要保存用户的排序结果时,有这样一个接口简直太棒啦有木有!这里需要说明的是,所有具备类似data-属性的节点都可以被序列化,熟悉前端的朋友一定知道,这是一个HTML5中的扩展功能,可以让我们在节点上附带更多的数据信息,在Bootstrap中经常需要用到这一特性。

1
$('.dd').nestable('serialize');

继续以这个例子为例,我们将会得到下面的JSON信息:

1
[{"id":1},{"id":2},{"id":3,"children":[{"id":4},{"id":5}]}]

不过,遗憾的是,貌似作者已经不打算维护这个项目啦,最后一次维护时间已经是6年前,毕竟属于jQuery的辉煌时代都已经过去,何况是基于jQuery的一个插件呢?可这种频繁修改DOM结构引发浏览器重绘的操作,在大前端时代会消失吗?或许并不会。关于这个项目更多的使用细节,大家可以到它的Github主页去了解。

Sortable方案

Sortable相比Nestable好的一点就是,它对自己的定位是“一个用于可重新排序的拖放列表的JavaScript库”。它不再局限于jQuery这样一个方案上,事实上它支持Vue、React、Angualr、Knockout等将近7个框架,除了支持常规的列表以外,还支持Grid中元素的拖拽,文档相比Nestable要更为完善一点,所以要在项目中使用的话,我个人更推荐Sortable。我们一起来看看如何使用Sortable吧,这里我们选择Bootstrap作为基础样式。首先,我们写一个简单的“列表组”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ul class="list-group" id="items">
<li class="list-group-item" data-id="0">
Menu1
li>
<li class="list-group-item" data-id="1">
Menu2
li>
<li class="list-group-item" data-id="2">
Menu3
li>
<li class="list-group-item" data-id="3">
Menu4
li>
ul>

接下来,我们通过JavaScript来给这个列表“施加”魔法——巴拉能量:

1
2
var ele = document.getElementById('items');
var sortable = Sortable.create(ele);

然后我们就可以发现,这个基于Bootstrap的列表居然可以拖拽啦!

sortablejs-demo-1
sortablejs-demo-1

OK,我们继续给这个例子来点魔法,可以让列表元素在拖动的时候高亮显示:

1
2
3
4
var sortable = Sortable.create(ele, {
animation: 150,
ghostClass: 'blue-backgroun-class'
});

可以注意到,拖拽时动画会变得更流畅,被拖拽的元素会以蓝底白字高亮显示:

sortablejs-demo-2
sortablejs-demo-2

和Nestable类似,我们可以指定一个回调函数来获得排序后的结果,注意到我们这里指定一个dataIdAttr,它告诉Sortable我们将用哪一个值作为数据的主键,从data-text这个命名就可以看出,它的数据是维护在类似data-的属性上的,假设我们这里希望获得排序后的菜单,那么,它的打开方式是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
var ele = document.getElementById('items');
var result = document.getElementById('result');
var sortable = Sortable.create(ele, {
animation: 150,
dataIdAttr: 'data-text',
onUpdate: onUpdate,
ghostClass: 'blue-backgroun-class'
});

function onUpdate(evt){
var data = sortable.toArray();
result.innerText = "当前排序结果为:" + JSON.stringify(data);
}

好了,现在可以看到,随着我们对列表进行拖拽,每次都会获得更新以后的列表数据,显然,我们可以将这个结果存到任何地方,这样就可以按用户定义的方式去加载一个列表。

sortablejs-demo-3
sortablejs-demo-3

以上就是Soratble的基本用法,关于更多的使用细节,官方文档了解一下。

HTML5原生方案

OK,说完了Nestable和Sortable这两个第三方的解决方案,下面我们来说说基于HTML5的原生方案。HTML5标准问世以来,有很多有意思的东西被吸收到标准之中,拖放(drag & drop)就是其中之一。在此之前,我们需要写大量的JavaScript代码来实现这个功能。现在,HTML5中原生支持拖放API,我们不妨考虑通过它来实现一个可拖拽的列表,这里我们继续沿用基于Bootstrap的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var dragElement = null;
var source = document.querySelectorAll('.list-group-item');

for(var i = 0; i < source.length; i++){
source[i].addEventListener('dragstart',function(ev){
dragElement = this;
},false);

source[i].addEventListener('dragenter', function(ev){
if(dragElement != this){
this.parentNode.insertBefore(dragElement,this);
}
}, false)

source[i].addEventListener('dragleave', function(ev){
if(dragElement != this){
if(this == this.parentNode.lastElementChild || this == this.parentNode.lastChild){
this.parentNode.appendChild(dragElement);
}
}
}, false)
};

document.ondragover = function(e){e.preventDefault();}
document.ondrop = function(e){e.preventDefault();}

这里唯一需要注意的地方,就是要给每一个className为list-group-item的元素添加draggable属性,并设置该属性为true,这是使用HTML5拖放API的一个前提,换言之,只有draggable的元素才可以被拖拽。那么,HTML5中针对拖放的API有哪些呢?针对拖放事件,我们可以抽象出三种角色,它们分别是:

  • 源对象:即对拖拽的对象。它有dragstart、drag和dragend三个事件。

  • 过程对象:即被拖拽的对象,在拖拽过程中经过的中间对象,它有dragenter、dragover和dragleave三个事件。

  • 目标对象:即被拖拽的对象,最终所放置的对象,它只有一个drop事件。

而在所有的拖拽事件中,都提供了一个数据传递对象dataTransfer,用于在源对象和目标对象间传递数据。例如,我们可以通过setData()来向dataTransfer存入数据,通过getData()来从dataTransfer读取数据,通过clearData()来清理dataTransfer中的数据。此外,还可以通过setDragImage()、effectAllowed属性 和 dropEffect 属性来设置拖拽过程中的图标、拖放的视觉效果等。这里需要注意的是,IE浏览器不支持dataTransfer对象。了解了这下,我们就可以做出一个”简陋“的拖拽排序功能:

sortablejs-demo-4
sortablejs-demo-4

本文小结

这篇文章主要分享了三种实现列表拖拽排序的方案,在技术选型阶段,主要选择Nestable和Sortable这两种方案,前者对层级节点提供的序列化支持非常好,但经过一番折腾后,发现要想像作者一样用好这个插件,着实是件困难的事情,而且貌似作者已经不再维护这个项目了,最近的代码提交历史大概是6年前,毕竟属于jQuery的辉煌时代已经过去,何况是一个基于jQuery的插件呢?所以,个人不建议在正式项目中使用Nestable。相比之下,Sortable的定位要更高一点,它不再局限于某个UI框架上,理论上任何前端项目都可以使用,从文档的完整性和易用性上,都要比Nestable要更胜一筹。原本一开始打算写这两种方案的,后来觉得HTML5中提供了拖拽相关的API接口,这种方式不失为一种解决方案。虽然提到HTML5就让人联想到兼容性,可都2019年了,连浓眉大眼的微软(巨硬)都开始在Edge里使用Chrome内核了,兼容性问题还算是个问题吗?所以,这篇文章实际上介绍了三种解决方案,具体使用哪一种,大家可以根据实际情况来决定,好啦,这篇博客就写到这里,谢谢大家,晚安!