原生 drag api 可实现列内拖拽,需设卡片 draggable="true"、列容器 draggable="false",dragover 时 preventdefault,drop 时用 children 索引定位;flex 布局下用 flex: 0 0 300px 和 scroll-snap 防抖动;框架中 key 须用稳定 id,状态更新后微任务延迟滚动;移动端需 touch 降级并禁用 passive。

用原生 drag API 实现列内拖拽,别碰 SortableJS 就想省事
原生 drag 事件链(dragstart → dragover → drop)能跑通列内排序,但默认不支持跨列。很多人一上来就引入 SortableJS,结果发现它在嵌套结构或动态列渲染时频繁触发 ghost element 错位、onEnd 回调丢失索引——其实只是没关掉它的 swapThreshold 或没重置 group name。
实操建议:
- 每列容器设
draggable="false",只给卡片设draggable="true" - 监听列容器的
dragover时必须调用event.preventDefault(),否则drop不触发 -
dragstart中用dataTransfer.setData("text/plain", cardId)传 ID,别传 DOM 节点,避免跨列时节点被移走后找不到父容器 - 列容器的
drop事件里,用Array.from(column.children).indexOf(event.target)算插入位置,比event.clientY计算更稳
Flexbox 布局下固定列宽 + 滚动容器,防拖拽时布局抖动
用 display: flex 排列列容器时,如果列宽写死为 300px 但没限制子元素最小宽度,拖拽中卡片临时脱离文档流,Flex 容器会重算剩余列宽度,导致其他列“抽搐”。更麻烦的是,当看板内容多需要横向滚动时,overflow-x: auto 容器若没加 scroll-snap-type: x mandatory,拖拽释放瞬间容易卡在半格位置。
实操建议:
- 列容器用
flex: 0 0 300px(即不伸缩、不压缩、固定宽),内部卡片用min-width: 0防文本撑破 - 外层看板容器加
display: flex; overflow-x: auto; scroll-behavior: smooth - 每列加
scroll-snap-align: start,配合外层scroll-snap-type: x mandatory,让滚动停靠对齐 - 拖拽开始时临时加
pointer-events: none到非目标列,避免误触滚动条
Vue/React 中状态更新与 DOM 同步的时机陷阱
在响应式框架里,把卡片数组存在组件 state 中,拖拽后直接 splice 数组再 setState,看似合理,但 DOM 渲染滞后于 drop 事件回调,导致下一次拖拽的 event.target 拿到的是旧节点引用。尤其 React 的 useEffect 依赖数组漏写 cards,或 Vue 的 v-for key 没绑定唯一 ID,都会让 Vue/React 复用错误的 DOM 元素。
实操建议:
- 拖拽结束时,先用
event.currentTarget拿到目标列 DOM,再通过dataset.columnId映射到对应 state 数组,而不是靠index直接 push - React 中确保
key是卡片的稳定 ID(如card.id),别用数组索引;Vue 中用:key="card.id"并禁用transition-group的move动画,它会干扰拖拽定位 - 状态更新后,用
setTimeout(() => {}, 0)延迟一次微任务再执行滚动定位,避开 React/Vue 的批量更新合并
移动端 Safari 的 drag 兼容补丁必须手写
Safari iOS 16.4+ 才真正支持原生 drag,之前版本完全静默。而安卓 Chrome 对 dragstart 的 dataTransfer 写入限制极严——只接受 "text/plain" 类型,且长度超 500 字符就丢数据。指望 DragEvent 兼容所有端是错的。
实操建议:
- 用
if ('ontouchstart' in window)检测触屏设备,降级为touchstart/touchmove/touchend模拟拖拽(用transform: translate3d()移动卡片,避免 layout thrashing) - 移动端不依赖
dataTransfer,改用全局变量缓存当前拖拽卡片 ID 和起始列 ID - Safari 上禁用
-webkit-user-drag: none,否则draggable="true"失效 - 所有拖拽相关事件监听器都用
{ passive: false }显式声明,否则 iOS 上touchmove默认被阻止
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











