使用 SortableJS 实现拖拽排序

前言

后台管理、看板、表单设计器等场景里,经常需要让用户通过拖拽调整列表顺序。
自己监听 mousedown / mousemove 再操作 DOM,代码量大,还要处理触摸端、动画和边界情况。
SortableJS 是一个轻量的原生 JS 拖拽排序库,不依赖 jQuery 或 Vue,几行代码就能让列表支持拖拽重排。
本文介绍安装方式、基础用法、与数据数组同步,以及 Vue 3 项目中的常见集成思路。
下文示例以浏览器环境为主,版本号可按项目实际调整。

依赖

SortableJS 可通过 npm 安装,也可在静态页面里直接用 CDN 引入。

npm

下面命令将 SortableJS 安装为项目依赖。

1
npm install sortablejs

CDN

若只是快速验证或在无构建工具的项目中使用,可在 HTML 中引入 CDN 脚本。

1
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>

实现

HTML 结构

SortableJS 作用于一个容器元素,容器内的直接子元素才是可拖拽项。
下面是一个最简单的列表示例。

1
2
3
4
5
<ul id="sortable-list">
<li data-id="1">第一项</li>
<li data-id="2">第二项</li>
<li data-id="3">第三项</li>
</ul>

每个 <li> 对应一条可排序项,容器 #sortable-list 将作为 Sortable 的挂载目标。

基础初始化

引入 Sortable 后,对容器调用 Sortable.create 即可启用拖拽排序。

1
2
3
4
5
6
7
8
import Sortable from 'sortablejs'

const listEl = document.getElementById('sortable-list')

Sortable.create(listEl, {
animation: 150,
ghostClass: 'sortable-ghost',
})

animation 控制换位时的过渡动画时长(毫秒)。
ghostClass 是拖拽过程中占位元素附加的 CSS 类名,便于自定义半透明样式。

配合占位样式,拖拽时的视觉反馈会更清晰。

1
2
3
4
.sortable-ghost {
opacity: 0.4;
background: #f0f0f0;
}

读取排序结果

拖拽结束后,DOM 顺序已经改变,但业务数据往往还保存在 JS 数组里。
可在 onEnd 回调里按当前 DOM 顺序重建数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let items = [
{ id: 1, name: '第一项' },
{ id: 2, name: '第二项' },
{ id: 3, name: '第三项' },
]

Sortable.create(listEl, {
animation: 150,
onEnd() {
const ids = [...listEl.children].map((el) => Number(el.dataset.id))
items = ids.map((id) => items.find((item) => item.id === id))
console.log('新顺序', items)
},
})

onEnd 在拖拽完成时触发,此时可根据子元素的 data-id 重新排列 items 数组,再提交给后端或写入本地状态。

常用配置

SortableJS 提供了丰富的选项,下面几个在业务里最常见。

1
2
3
4
5
6
7
8
9
10
11
Sortable.create(listEl, {
handle: '.drag-handle', // 仅允许通过指定手柄拖拽
draggable: '.sort-item', // 可拖拽项的选择器(容器非纯子元素列表时有用)
disabled: false, // 设为 true 可临时禁用排序
filter: '.ignore', // 匹配到的元素不可拖拽
group: 'shared', // 同名 group 可在多个列表间互相拖入
animation: 150,
onEnd(evt) {
console.log('从索引', evt.oldIndex, '移到', evt.newIndex)
},
})

handle 适合表格行、卡片等「整行可点但只想让图标区域可拖」的场景。
group 可用于看板列之间拖动任务卡片。
evt.oldIndexevt.newIndex 可直接用于数组的 splice 重排,不必再遍历 DOM。

若已有数组,用索引更新往往更简洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
function moveItem(arr, from, to) {
const next = [...arr]
const [removed] = next.splice(from, 1)
next.splice(to, 0, removed)
return next
}

Sortable.create(listEl, {
animation: 150,
onEnd(evt) {
items = moveItem(items, evt.oldIndex, evt.newIndex)
},
})

销毁实例

组件卸载或不再需要排序时,应调用 destroy 释放事件监听,避免内存泄漏。

1
2
3
4
const sortable = Sortable.create(listEl, { animation: 150 })

// 页面离开或组件卸载时
sortable.destroy()

验证

本地起一个静态页面或 dev server,打开浏览器控制台即可验证。

  1. 用鼠标按住列表项上下拖动,观察 DOM 顺序是否变化。
  2. onEndconsole.log 输出数组,确认与界面顺序一致。
  3. 若使用了 handle,点击非手柄区域应无法拖动。

Chrome DevTools 的 Elements 面板也可实时查看 <li> 节点顺序是否与预期一致。

扩展

Vue 3 集成

在 Vue 3 中不必手写 DOM 同步,常用做法是使用基于 SortableJS 封装的 vuedraggable(包名 vuedraggable,Vue 3 对应 v4 版本)。

先安装依赖。

1
npm install sortablejs vuedraggable@next

模板里用 v-model 绑定数组,拖拽会自动更新数据顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<draggable v-model="list" item-key="id" animation="150" ghost-class="sortable-ghost">
<template #item="{ element }">
<div class="sort-item">{{ element.name }}</div>
</template>
</draggable>
</template>

<script setup>
import { ref } from 'vue'
import draggable from 'vuedraggable'

const list = ref([
{ id: 1, name: '第一项' },
{ id: 2, name: '第二项' },
{ id: 3, name: '第三项' },
])
</script>

若项目约定「Vue 文件不写 TS」,可将上述脚本放到同名 .js 文件中再 import.vue

多列表互相拖入

看板场景下,给多个列表容器设置相同的 group 名称,即可在同一组内跨列拖动。

1
2
3
4
5
const groupName = 'kanban'

Sortable.create(document.getElementById('todo'), { group: groupName, animation: 150 })
Sortable.create(document.getElementById('doing'), { group: groupName, animation: 150 })
Sortable.create(document.getElementById('done'), { group: groupName, animation: 150 })

跨列拖动时,每个列表维护各自的数组,在 onAdd / onRemove 回调里同步数据即可。

总结

SortableJS 适合快速为列表、表格行、看板卡片添加拖拽排序能力。
核心步骤是:准备容器与子项 DOM → Sortable.create 初始化 → 在 onEnd 中同步业务数组 → 不用时 destroy
需要与 Vue 响应式深度结合时,优先考虑 vuedraggable,减少手写 DOM 与数组双向同步的代码。
配置 handlefiltergroup 等选项即可覆盖大多数产品交互,无需从零实现拖拽逻辑。