Hi,大家好,在经历了两周多的 “写 Bug”、“改 Bug” 死循环后,又一个迭代终于在习以为常的加班生活中结束啦!联想到最近在 Github 上发起的 “996.icu” 事件,不禁令人由衷地感慨生活不易,所谓“”起风了,唯有努力生存”。其实,我反对是加班常态化所导致的无效加班,既然努力工作是为了更好的生活,可如果因此而模糊了工作和生活的界限,这到底是一件好事还是一件坏事呢?想想每个周末被工作群里消息支配的失落感,我希望我有可以自由支配的时间,即使我看起来比别人年轻,即使我下班后依旧孤身一人,因为用时间来换钱这件事情,着实是件性价比不高的事情,货币会一天天地贬值直至我们老去,可那些失去的时间就永远地失去了。好了,“业精于勤荒于嬉”,今天我们来说前端中实现拖拽排序这件事情。
其实,这件事情说起来挺尴尬的,我们曾经为用户提供过某种**”智能“**的体验,我们通过对用户的行为进行分析,为其推荐了个性化的菜单项,甚至根据用户的使用频率对菜单进行了排序。可事实上用户的反响并不是非常强烈,在经过一段时间的使用后,用户依然觉得这个功能相当地”鸡肋“,这件事情告诉我们一个真相,即无论是产品设计还是需求研讨,最好不要轻易地代入用户的角色。最终的结果是我们打算为用户提供自定义的功能,考虑到操作的便利性问题,我们放弃了那种通过上下箭头按钮进行排序的方案,这样就回到了本文的主题,如何在前端中对一组列表进行拖拽排序,最终我们选定了两组方案,它们分别是Nestable和Sortable。
Nestable 方案
Nestable 是一个基于 jQuery 的插件,是一个在 Github 上开源的项目,据作者声称,这是一个"拖放具有鼠标和触摸兼容性的分层列表"的方案。这里针对触摸兼容性的支持可以忽略不计,因为如今都 9012 年了,博主依然在做传统前端页面的开发,这里博主最感兴趣的一点是,它可以支持分层列表,换言之,我们的列表元素是可以有层级关系、是可以嵌套的,唯一令人有点不爽的就是它依赖 jQuery 了,在这样一个连 Github 和 Bootstrap 都在努力移除 jQuery 的时代,没有 jQuery 的历史包袱,意味着我们可以大胆地去做现代前端应该做的事情。好了,我们来看看 Nestable 具体是怎么使用的吧!首先,我们定义一个简单的 HTML 结构:
<div class="dd">
<ol class="dd-list">
<li class="dd-item" data-id="1">
<div class="dd-handle">Item 1</div>
</li>
<li class="dd-item" data-id="2">
<div class="dd-handle">Item 2</div>
</li>
<li class="dd-item" data-id="3">
<div class="dd-handle">Item 3</div>
<ol class="dd-list">
<li class="dd-item" data-id="4">
<div class="dd-handle">Item 4</div>
</li>
<li class="dd-item" data-id="5">
<div class="dd-handle">Item 5</div>
</li>
</ol>
</li>
</ol>
</div>
接下来,我们可以使用如下的 JavaScript 代码来初始化整个列表,果然,一股 jQuery 风扑面而来:
$('.dd').nestable({
/* config options */
});
然后,我们就可以看到下面的效果:
怎么样?看起来效果还不错吧!不过博主在前期调研的过程中发现,它对于复杂的层级关系就无能为力啦,可能是博主打开的姿势不对吧!如果希望对列表做更深层次的定制,它需要配置的属性会非常非常的多,而且它有一套内在约束在里面,譬如 className、nodeName 等等,虽然这些都可以去配置,但要想像作者一样运用得好,依然是需要花费大量时间来学习它的 API。
说到这里,对于 Nestable,我唯二喜欢的一个 feature 是,它可以实时地获取到排序后的节点信息,而且是序列化后的 JSON 格式哦,因为当我们要保存用户的排序结果时,有这样一个接口简直太棒啦有木有!这里需要说明的是,所有具备类似**data-**属性的节点都可以被序列化,熟悉前端的朋友一定知道,这是一个 HTML5 中的扩展功能,可以让我们在节点上附带更多的数据信息,在 Bootstrap 中经常需要用到这一特性。
$('.dd').nestable('serialize');
继续以这个例子为例,我们将会得到下面的 JSON 信息:
[{"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 作为基础样式。首先,我们写一个简单的“列表组”:
<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 来给这个列表“施加”魔法——巴拉能量:
var ele = document.getElementById('items');
var sortable = Sortable.create(ele);
然后我们就可以发现,这个基于 Bootstrap 的列表居然可以拖拽啦!
OK,我们继续给这个例子来点魔法,可以让列表元素在拖动的时候高亮显示:
var sortable = Sortable.create(ele, {
animation: 150,
ghostClass: 'blue-backgroun-class'
});
可以注意到,拖拽时动画会变得更流畅,被拖拽的元素会以蓝底白字高亮显示:
和 Nestable 类似,我们可以指定一个回调函数来获得排序后的结果,注意到我们这里指定一个 dataIdAttr,它告诉 Sortable 我们将用哪一个值作为数据的主键,从 data-text 这个命名就可以看出,它的数据是维护在类似**data-**的属性上的,假设我们这里希望获得排序后的菜单,那么,它的打开方式是这样的:
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);
}
好了,现在可以看到,随着我们对列表进行拖拽,每次都会获得更新以后的列表数据,显然,我们可以将这个结果存到任何地方,这样就可以按用户定义的方式去加载一个列表。
以上就是 Soratble 的基本用法,关于更多的使用细节,官方文档了解一下。
HTML5 原生方案
OK,说完了 Nestable 和 Sortable 这两个第三方的解决方案,下面我们来说说基于 HTML5 的原生方案。HTML5 标准问世以来,有很多有意思的东西被吸收到标准之中,拖放(drag & drop)就是其中之一。在此之前,我们需要写大量的 JavaScript 代码来实现这个功能。现在,HTML5 中原生支持拖放 API,我们不妨考虑通过它来实现一个可拖拽的列表,这里我们继续沿用基于 Bootstrap 的例子。
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 对象。了解了这下,我们就可以做出一个**”简陋“**的拖拽排序功能:
本文小结
这篇文章主要分享了三种实现列表拖拽排序的方案,在技术选型阶段,主要选择 Nestable 和 Sortable 这两种方案,前者对层级节点提供的序列化支持非常好,但经过一番折腾后,发现要想像作者一样用好这个插件,着实是件困难的事情,而且貌似作者已经不再维护这个项目了,最近的代码提交历史大概是 6 年前,毕竟属于 jQuery 的辉煌时代已经过去,何况是一个基于 jQuery 的插件呢?所以,个人不建议在正式项目中使用 Nestable。相比之下,Sortable 的定位要更高一点,它不再局限于某个 UI 框架上,理论上任何前端项目都可以使用,从文档的完整性和易用性上,都要比 Nestable 要更胜一筹。原本一开始打算写这两种方案的,后来觉得 HTML5 中提供了拖拽相关的 API 接口,这种方式不失为一种解决方案。虽然提到 HTML5 就让人联想到兼容性,可都 2019 年了,连浓眉大眼的微软(巨硬)都开始在 Edge 里使用 Chrome 内核了,兼容性问题还算是个问题吗?所以,这篇文章实际上介绍了三种解决方案,具体使用哪一种,大家可以根据实际情况来决定,好啦,这篇博客就写到这里,谢谢大家,晚安!