本文详解如何在 React.js 中封装拖拽排序组件。通过采用 HTML5 原生 Drag and Drop API,结合 React 的数据驱动特性,利用 value 和 onChange 控制组件顺序。文章解析了 draggable、onDragStart、onDrop 等关键属性的用法,强调通过修改数据触发虚拟 DOM 重渲染来维持排序,而非直接操作 DOM 节点。
React 拖拽排序组件设计思路
在 React.js 技术栈下,组件封装的首要原则是明确输入与输出接口。核心逻辑遵循数据驱动模式,即通过数据状态来渲染页面布局,并直接控制拖拽元素在列表中的顺序。
考虑到无需兼容 IE8 等老旧浏览器,本方案直接采用 HTML5 原生的拖放(Drag and drop)接口来实现交互效果。若项目对浏览器兼容性有更高要求,也可退而求其次,通过监听鼠标点击事件来模拟类似功能。
最终实现的交互效果如下所示:

HTML5 拖放 API 属性解析
实现前需熟悉 HTML5 拖放相关的属性规范,MDN 文档提供了详尽的说明,参考链接为 。
需注意 React 对事件名称的转换规则:React 会在原生事件名前添加 "on" 前缀,并将后续字符转为驼峰命名法。例如,原生的 click 事件在 React 中应写作 onClick。
本组件主要涉及以下拖放相关属性:
- draggable:当该属性设置为 true 时,表示当前元素允许被拖拽。
- onDragStart:当控件开始被拖拽时触发此事件。该方法提供 dataTransfer.setData() 接口,用于将必要数据存入对象,以便在后续其他事件中调用。
- onDragOver:用于定义当前控件是否允许接收被拖拽的元素。通常在此方法中调用 preventDefault() 以阻止默认行为,确保元素可被放置。
- onDragEnter:当被拖拽元素进入另一个可接受区域时触发。利用此事件可以添加视觉反馈,如高亮显示放置目标。
- onDragLeave:当被拖拽元素离开当前可接受区域时触发。可用于监听并移除之前添加的移入视觉效果。
- onDrop:当控件被“释放”到一个有效的放置目标位置时触发。在此方法中处理数据逻辑,并通过调用 onChange 回调将更新后的 value 暴露给父组件。
其中,draggable 和 onDragStart 是“拖拽源”需要设置的属性;而 onDragOver、onDragEnter、onDragLeave 和 onDrop 则是“放置目标”需要设置的属性。但在本拖拽排序组件的设计中,每个列表元素既具备拖拽源的功能,也具备放置目标的功能。
组件数据结构与排序逻辑实现
第二步,作为 React 组件,按照惯例将输入属性定义为 value,并暴露 onChange 事件以监听 value 的变化。同时,通过 onChange 将最新数据传递给父组件,并暴露一个 sortKey 属性,用于指定排序时使用的字段键名。由于涉及排序且允许自定义每个元素的内部子组件,输入数据格式被定义为一个数组对象,其中 content 字段可接受 ReactNode。
value: [
{
content: 'div1',
code: '01',
sort: 0,
},
{
content: 'div2',
code: '02',
sort: 1
},
{
content: 'div3',
code: '03',
sort: 2
},
{
content: 'div5',
code: '05',
sort: 5
},
{
content: 'div4',
code: '04',
sort: 4
}]
根据传入的 value 生成可排序组件的各个节点,关键实现代码如下:
// 生成拖拽组件
createDraggleComponent(data, sortKey, style, uId) {
return data.sort(this.compare(sortKey)).map((item) => {
return (
<div
className={styles.content}
key={item.code}
draggable={true}
onDragEnter={this.dragenter.bind(this)}
onDragLeave={this.dragleave.bind(this)}
onDragStart={this.domdrugstart.bind(this, item[sortKey], item.code, uId, item)}
onDrop={this.drop.bind(this, item[sortKey], data, sortKey, uId)}
onDragOver={this.allowDrop.bind(this)}
style={{ ...style }}>{item.content}</div>
)
})
}
render() {
const { value, sortKey, style } = this.props;
return (
<Row>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{this.createDraggleComponent(value, sortKey, style)}
</div>
</Row>
)
}
上述代码中涉及的具体属性与方法实现如下:
// 拖动事件
domdrugstart(sort, code, ee) {
ee.dataTransfer.setData("code", code);
ee.dataTransfer.setData("sort", sort);
}
// 拖动后鼠标进入另一个可接受区域
dragenter(ee) {
ee.target.style.border = '2px dashed #008dff';
ee.target.style.boxShadow = '0 0 8px rgba(30, 144, 255, 0.8)';
}
// a拖到b,离开b的时候触发
dragleave(ee) {
ee.target.style.border = '1px solid grey';
ee.target.style.boxShadow = '';
}
// 对象排序
compare(key) {
return (obj1, obj2) => {
if (obj1[key] < obj2[key]) {
return -1;
} else if (obj1[key] > obj2[key]) {
return 1;
}
return 0
}
}
// 当一个元素或是选中的文字被拖拽释放到一个有效的释放目标位置时
drop(dropedSort, data, sortKey, ee) {
ee.preventDefault();
const code = ee.dataTransfer.getData("code");
const sort = ee.dataTransfer.getData("sort");
if (sort < dropedSort) {
data.map(item => {
if (item.code === code) {
item[sortKey] = dropedSort;
} else if (item[sortKey] > sort && item[sortKey] < dropedSort + 1) {
item[sortKey]--;
}
return item;
});
} else {
data.map(item => {
if (item.code === code) {
item[sortKey] = dropedSort;
} else if (item[sortKey] > dropedSort - 1 && item[sortKey] < sort) {
item[sortKey]++;
}
return item;
});
}
this.props.onChange(data)
}
allowDrop(ee) {
ee.preventDefault();
}
核心实现机制与注意事项
值得注意的关键点在于:在控制元素顺序时,并未使用 .target.before(document.getElementById({id})) 这类直接操作 DOM 节点的方法。相反,每次触发 onDrop 事件时,仅处理数据的 sort 字段排序,并通过 onChange 事件将新数据暴露给父组件。通过改变 value 值触发虚拟 DOM 的重新渲染,从而间接控制元素的显示顺序。
根据公司内部需求,在此基础上还实现了拖拽复制功能,具体细节待后续整理记录。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。