当前位置: 首页 > ds >正文

React--》掌握react构建拖拽交互的技巧

在这篇文章中将深入探讨如何使用react-dnd,从基础的拖拽操作到更复杂的自定义功能带你一步步走向实现流畅、可控且用户友好的拖拽体验,无论你是刚接触拖拽功能的初学者还是想要精细化拖拽交互的经验开发者,都能从中找到适合自己的灵感和解决方案。

目录

react-dnd操作

拖拽排序操作

拖拽移动操作

react-beautiful-dnd操作

dnd-kit操作

react-dnd操作

        react-dnd:一个用于在react应用中实现拖拽和放置功能的库,它为开发者提供了一套灵活且可扩展的工具使得在react中处理拖拽交互变得简单且高效,通过react-dnd开发者可以轻松地创建拖拽组件、设置拖拽目标以及处理拖拽过程中各个阶段的事件(如开始拖拽、拖拽过程中、放置等),它不仅支持基础的拖拽功能还允许开发者自定义拖拽行为、指定可放置区域、调整拖拽元素的样式等,详情请阅读官方文档:地址 ,当然也可以去看看源码:地址 进行学习,接下来我们终端执行如下命令进行安装react-dnd,以下是使用拖拽的具体步骤:

npm install react-dnd react-dnd-html5-backend

包裹容器:接下来我们开始实现拖拽操作,react-dnd提供一个上下文提供者DndProvider,它用于在应用中启用拖拽功能并且是实现拖拽交互的基础,所有需要拖拽行为的组件都必须包裹在DndProvider中

HTML5Backend是react-dnd的一个后端实现,用于处理浏览器中的拖拽操作,通过引入react-dnd可以在浏览器中启用标准的拖拽行为,代码如下所示:

import Drag from './components/drag'
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';const App = () => {return (<DndProvider backend={ HTML5Backend }><Drag></Drag></DndProvider>)
}export default App

拖拽源:接下来我们开始设置拖拽源操作,react-dnd提供了useDrag用来提供对拖拽元素的一些操作,具体的代码逻辑如下所示:

import { useDrag } from "react-dnd";
import "./index.less";const Draggable = () => {/*** 参数1:返回值是一个对象,主要放一些拖拽物的状态* 参数2:ref实例,只要将它注入到DOM中该DOM就会变成一个可拖拽的DOM*/const [{ isDragging }, dragRef]: any = useDrag(() => ({type: "box", // 拖拽的类型,要和接收者那边的类型对应上item: { id: "1" }, // 拖拽的数据,要和接收者那边的数据对应上collect: (monitor) => ({ // 拖拽过程中的一些状态,要和接收者那边的数据对应上isDragging: monitor.isDragging(),}),}));return <div ref={dragRef} className="drag"></div>;
};export default Draggable;

通过如上代码,调整一下css样式之后,我们就可以对我们定义的div盒子进行拖拽操作了,如下:

如果想隐藏在拖拽过程中原本为啥的元素,可以通过参数1中的对象来确定是否在拖拽中,如果拖拽的话直接隐藏原本元素就好了,代码如下:

import { useDrag } from "react-dnd";
import "./index.less";const Draggable = () => {/*** 参数1:返回值是一个对象,主要放一些拖拽物的状态* 参数2:ref实例,只要将它注入到DOM中该DOM就会变成一个可拖拽的DOM*/const [{ isDragging }, dragRef]: any = useDrag(() => ({type: "box", // 拖拽的类型,要和接收者那边的类型对应上item: { id: "1" }, // 拖拽的数据,要和接收者那边的数据对应上collect: (monitor) => ({ // 拖拽过程中的一些状态,要和接收者那边的数据对应上isDragging: monitor.isDragging(),}),}));if (isDragging) {return <div ref={dragRef}></div>;}return <div ref={dragRef} className="drag"></div>;
};export default Draggable;

当然我们也可以通过props传参的方式来实现不同拖拽的id的拖拽源,如下所示:

拖拽区域:接下来我们开始设置拖拽区域操作,react-dnd提供了useDrop用来提供对拖拽区域的一些操作,具体的代码逻辑如下所示:

import { useDrop } from "react-dnd";
import "./index.less";const Droppable = () => {const [{ isOver }, drop]: any = useDrop(() => ({accept: 'box', // 只接受box类型的数据collect: (monitor) => ({ // 收集器,用来获取拖拽过程中的一些信息isOver: monitor.isOver() }),drop: (item) => { // 放置事件处理函数console.log(item)}}))return <div ref={drop} className="drop"></div>;
};export default Droppable;

然后接下面我们定义几个拖拽源,然后将拖拽元素拖拽到拖拽区域里面,可以执行放置时间的处理函数从而打印一下对应的数据,如下所示:

drop这个回调事件可以写的很复杂这里我们可以将其抽离出去通过props来实现调用,useDrop支持第二个参数[state]作为依赖项,当你通过[state]将状态传入useDrop,你实际上是在告诉react当state发生变化时重新执行useDrop钩子,useDrop就能根据最新的state来决定目标区域的行为或者重新计算是否可以接受拖拽等,如下所示:

import { useDrop } from "react-dnd";
import "./index.less";const Droppable = ({ handleDrop, state, text, children }: any) => {const [{ isOver }, drop]: any = useDrop(() => ({accept: 'box', // 只接受box类型的数据collect: (monitor) => ({ // 收集器,用来获取拖拽过程中的一些信息isOver: monitor.isOver() }),drop: (item) => handleDrop(item) // 放置事件处理函数}), [state])return <div ref={drop} className="drop">{text}{children}</div>;
};export default Droppable;

拖拽排序操作

        拖拽排序:说白了就是改变原数据的位置顺序,这里我们使用了immutability-helper用于简化js中不可变数据操作的工具包,它提供一些简单API来更新嵌套的对象或数组而不会直接修改原数据,这样可以避免直接变更数据确保数据的不可变性,终端执行如下命令安装:

npm install immutability-helper

immutability-helper通常用于React中尤其是更新状态时,帮助避免直接改变state的问题,比如更新嵌套对象或者数组的某个值时immutability-helper会返回一个新的对象而不是修改原对象例如使用它更新数组中的元素:

import update from 'immutability-helper';const state = [1, 2, 3];
const newState = update(state, { 1: { $set: 4 } });console.log(newState); // [1, 4, 3]

接下来我们在拖拽排序中使用,其中useCallback确保了moveCard和 renderCard函数不会在每次组件渲染时被重新创建,这样避免了不必要的重新渲染和性能开销。如下所示:

import update from 'immutability-helper'
import { useCallback, useState } from 'react'
import { Card } from './Card'const Index = () => {const [cards, setCards] = useState([{ id: 1, text: 'Write a cool JS library' },{ id: 2, text: 'Make it generic enough' },{ id: 3, text: 'Write README' },{ id: 4, text: 'Create some examples' },{ id: 5, text: 'Spam in Twitter and IRC to promote it (note that this element is taller than the others)' },{ id: 6, text: '???' },{ id: 7, text: 'PROFIT' },])const moveCard = useCallback((dragIndex: number, hoverIndex: number) => {setCards((prevCards: any) => update(prevCards, {$splice: [[dragIndex, 1],[hoverIndex, 0, prevCards[dragIndex]],],}))}, [])const renderCard = useCallback((card: any, index: number) => {return (<Card type='card' key={card.id} index={index} id={card.id} text={card.text} moveCard={moveCard} />)}, [])return (<><div style={{ width: '400px' }}>{cards.map((card, i) => renderCard(card, i))}</div></>)
}export default Index

然后接下来我们将拖拽源和放置源写在同一个div区域内,这样标签内容就既可以拖拽又可以放置,具体代码如下所示,这里我们通过在div元素设置data-handler-id来唯一地标识该div元素,方便对其进行操作:

import { useRef } from 'react'
import { useDrag, useDrop } from 'react-dnd'const style = {border: '1px dashed gray',padding: '0.5rem 1rem',marginBottom: '.5rem',backgroundColor: 'white',cursor: 'move',
}export const Card = ({ id, text, index, moveCard, type }: any) => {const ref = useRef<HTMLDivElement>(null)const [{ handlerId }, drop] = useDrop({accept: type,collect: (monitor) => ({ // 收集器,用来获取拖拽过程中的一些信息handlerId: monitor.getHandlerId(), // 设置拖拽源的唯一标识,用来区分不同的拖拽源}),hover(item: any, monitor) {if (!ref.current) returnconst dragIndex = item.index // 拖拽元素的索引位置const hoverIndex = index // 鼠标悬停元素的索引位置if (dragIndex === hoverIndex) returnconst hoverBoundingRect = ref.current?.getBoundingClientRect() // 获取鼠标悬停元素的边界信息const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 // 鼠标悬停元素的中点位置const clientOffset: any = monitor.getClientOffset() // 获取鼠标在页面中的位置信息const hoverClientY = clientOffset.y - hoverBoundingRect.topif (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return // 如果拖拽元素在鼠标悬停元素的上方,并且鼠标位置在元素的中点下方,则不执行移动操作if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return // 如果拖拽元素在鼠标悬停元素的下方,并且鼠标位置在元素的中点上方,则不执行移动操作moveCard(dragIndex, hoverIndex) // 调用moveCard函数,实现拖拽排序效果item.index = hoverIndex // 更新拖拽元素的索引位置},})const [{ isDragging }, drag] = useDrag({type,item: () => ({ id, index }),collect: (monitor: any) => ({isDragging: monitor.isDragging(),}),})const opacity = isDragging ? 0 : 1drag(drop(ref))return (<div ref={ref} style={{ ...style, opacity }} data-handler-id={handlerId}>{text}</div>)
}

最终呈现的效果如下所示:

拖拽移动操作

        拖拽移动:说白了就是改变原数据的left和top值,借助依赖项的变化来控制拖拽源不断改变其位置信息,如下我们定义一个拖拽源并且设置其依赖项的内容,如下所示:

import { useDrag } from 'react-dnd'const style: any = {position: 'absolute',border: '1px dashed gray',backgroundColor: 'white',width: '50px',height: '50px',cursor: 'move',
}
export const Box = ({ id, left, top, children, type }: any) => {const [{ isDragging }, drag]: any = useDrag(() => ({type,item: { id, left, top },collect: (monitor) => ({isDragging: monitor.isDragging(),}),}), [id, left, top])if (isDragging) return <div ref={drag} />return (<div ref={drag} style={{ ...style, left, top }}>{children}</div>)
}

然后我们通过如下代码来设置其规定只能在范围内进行移动,超出范围自动回到原本位置:

import update from 'immutability-helper'
import { useCallback, useState } from 'react'
import { useDrop } from 'react-dnd'
import { Box } from './Box'const styles: any = {width: 300,height: 300,border: '1px solid black',position: 'relative',
}const Index = () => {const [boxes, setBoxes] = useState<any>({a: { top: 20, left: 80, title: 'box1' },b: { top: 180, left: 20, title: 'box2' },})const moveBox = useCallback((id: string, left: number, top: number) => {setBoxes(update(boxes, {[id]: { $merge: { left, top } },}),)}, [boxes, setBoxes])const [, drop]: any = useDrop(() => ({accept: 'box',drop(item: any, monitor) { // 拖拽结束时触发的事件处理函数const delta: any = monitor.getDifferenceFromInitialOffset() // 获取鼠标移动的距离let left = Math.round(item.left + delta.x) // 计算新的left值let top = Math.round(item.top + delta.y) // 计算新的top值// 最小和最大边界限制left = Math.max(1, Math.min(left, 250));top = Math.max(1, Math.min(top, 250));moveBox(item.id, left, top) // 更新box的位置return undefined // 返回undefined,表示拖拽结束},}), [moveBox])return (<div style={{ display: 'flex', margin: '100px', gap: '30px' }}><div ref={drop} style={styles}>{Object.keys(boxes).map((key) => {const { left, top, title } = boxes[key]return (<Box key={key} id={key} left={left} top={top} type='box'>{title}</Box>)})}</div><div>box1: x坐标:{ boxes.a.left } - y坐标:{ boxes.a.top }<br/>box2: x坐标:{ boxes.b.left } - y坐标:{ boxes.b.top }</div></div>)
}export default Index

react-beautiful-dnd操作

        react-beautiful-dnd:是一个用于react的拖放(drag-and-drop)库,旨在帮助开发者在react应用中实现漂亮且易于使用的拖放交互,它提供了一个高效流畅且可访问的拖放体验,常用于实现类似列表排序卡片拖动等功能,终端执行如下命令安装:

npm install react-beautiful-dnd --save

它和react-dnd的区别主要在于其专注于排序方面的内容,优势如下,缺点就是React-beautiful-dnd 不支持React 高版本和严格模式,并且也是好几年没有维护了,大家需要根据自身情况选择是否去使用

1)拖放排序:支持列表项(如任务卡、文件等)的排序,可以拖动列表项改变其顺序。

2)跨列拖放:可以在多个列或容器之间拖动元素。

3)响应式:它的设计考虑了响应式和可访问性,使得即使是在移动设备上或使用键盘的用户也能够顺利使用拖放功能。

4)流畅动画:提供平滑的动画效果,使拖放过程更自然和易于理解。

5)高效:优化了性能,能够在大量元素的情况下依然流畅运行。

我们可以通过访问 链接 来查看其具体的实现案例操作,可以看到拖拽非常的丝滑,右侧菜单还提供了各种场景下的案例操作:

接下来我们就写一个简单的示例进行演示一下,代码如下所示:

import { useState, useCallback } from 'react';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';// 设置数组的初始元素
const getItems = (count: any) =>Array.from({ length: count }, (_: any, k) => k).map(k => ({ id: `item-${k}`, content: `item ${k}`}));// 重新排序数组元素
const reorder = (list: any, startIndex: any, endIndex: any) => {const result = Array.from(list);const [removed] = result.splice(startIndex, 1);result.splice(endIndex, 0, removed);return result;
};
const grid = 8;
// 获取拖拽元素的样式
const getItemStyle = (isDragging: any, draggableStyle: any) => ({userSelect: 'none',padding: grid * 2,margin: `0 ${grid}px 0 0`,background: isDragging ? 'lightgreen' : 'grey',...draggableStyle,
});
// 获取列表的样式
const getListStyle = (isDraggingOver: any) => ({background: isDraggingOver ? 'lightblue' : 'lightgrey',display: 'flex',padding: grid,overflow: 'auto',
});const App = () => {const [items, setItems] = useState(getItems(6));const onDragEnd = useCallback((result: any) => {// 是否拖拽到了其他位置if (!result.destination) return;const reorderedItems: any = reorder(items,result.source.index,result.destination.index);setItems(reorderedItems);}, [items]);return (<DragDropContext onDragEnd={onDragEnd}><Droppable droppableId="droppable" direction="horizontal">{(provided: any, snapshot: any) => (<div ref={provided.innerRef} style={getListStyle(snapshot.isDraggingOver)} {...provided.droppableProps}>{items.map((item, index) => (<Draggable key={item.id} draggableId={item.id} index={index}>{(provided: any, snapshot: any) => (<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}style={getItemStyle( snapshot.isDragging, provided.draggableProps.style)}>{item.content}</div>)}</Draggable>))}{provided.placeholder}</div>)}</Droppable></DragDropContext>);
};export default App;

dnd-kit操作

        dnd-kit: 是一个用于实现拖放(drag-and-drop)交互的react库,它提供了一组高效灵活的API使开发者能够轻松构建具有拖放功能的应用,通过在浏览器中直接操作DOM元素并处理拖动放置以及元素重排的过程,使得用户能够在界面中拖动元素动态地改变其位置或顺序,终端执行如下命令安装:

npm install @dnd-kit/core

其主要优势如下所示:

1)简化拖放功能:封装了拖放的核心逻辑开发者无需从头开始编写复杂的拖放机制

2)高度自定义:提供了丰富的API开发者可以自定义拖动行为、动画效果、边界限制、拖动过程中元素的样式等

3)支持触摸屏和桌面设备:同时支持鼠标和触摸事件,适应不同设备。

4)性能优化:设计注重性能,通过高效的状态管理和渲染机制保证即使在复杂场景下也能流畅运行。

我们可以通过访问 链接 来查看其具体的实现案例操作,可以看到拖拽非常的丝滑,右侧菜单还提供了各种场景下的案例操作:

接下来我们就写一个简单的示例进行演示一下,代码如下所示:

import { useState } from 'react'
import { DndContext, useDroppable, useDraggable } from "@dnd-kit/core";// 设置放置和拖拽组件
const Droppable = (props: any) => {const {isOver, setNodeRef} = useDroppable({ id: props.id });return (<div ref={setNodeRef}>{props.children}</div>);
}
const Draggable = (props: any) => {const {attributes, listeners, setNodeRef, transform} = useDraggable({ id: props.id });const style = transform ? {transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,} : undefined;return (<div ref={setNodeRef} style={style} {...listeners} {...attributes}>{props.children}</div>);
}
export default function Index() {const containers = ['A', 'B', 'C','D','E'];const [parent, setParent] = useState(null);const draggableMarkup = (<Draggable id="draggable"><div style={{width:100,height:100,background:'pink',cursor:'move'}}>可拖拽组件</div></Draggable>);const handleDragEnd=(event: any)=> {const {over} = event;setParent(over ? over.id : null);}return (<DndContext onDragEnd={handleDragEnd}><div style={{display:'flex',justifyContent: 'space-between',paddingTop:50}}>{containers.map((id) => (<Droppable key={id} id={id}><div style={{width:200,height:200,border:'1px solid #000'}}>{parent === id ? draggableMarkup : '放置源'}</div></Droppable>))}</div>{parent === null ? draggableMarkup : null}</DndContext>);
}

http://www.xdnf.cn/news/3894.html

相关文章:

  • Linux ACPI - ACPI系统描述表架构(2)
  • 【Redis】Redis常用命令
  • 软件架构之旅(6):浅析ATAM 在软件技术架构评估中的应用
  • 蓝桥杯15届国赛 合法密码
  • 嵌入式系统基础知识
  • 【Hive入门】Hive与Spark SQL集成:混合计算实践指南
  • python使用cv2在图片上标点
  • Python语句类型与格式规范研究
  • RT-Thread studio的驱动5.1.0报错修改
  • c++学习
  • 算法笔记。质数筛算法
  • 一种实波束前视扫描雷达目标二维定位方法——论文阅读
  • 短信登录功能实现(黑马点评)
  • 高中数学联赛模拟试题精选学数学系列第6套几何题
  • QT —— QWidget(1)
  • 白皮解读:数据流通关键技术白皮书【附全文阅读】
  • MNN 支持 DeepSeekVL
  • shell入门
  • 通过Docker部署Prometheus + Grafana搭建监控平台【超详细版】
  • 驱动总裁v2.19(含离线版)驱动工具软件下载及安装教程
  • 实用在线工具箱OmniTools
  • Python硬核革命:从微控制器到FPGA的深度开发指南
  • 多模态大语言模型arxiv论文略读(五十七)
  • Java响应式编程
  • DeepSeek实战--蒸馏
  • Java快速上手之实验六
  • Scrapy框架之【settings.py文件】详解
  • 开源项目实战学习之YOLO11:ultralytics-cfg-models-rtdetr(十一)
  • 强化学习:山地车问题
  • 【信息系统项目管理师】【论文】项目背景-通用部分(可背诵)