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可以在浏览器中启用标准的拖拽行为,代码如下所示:

javascript 复制代码
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用来提供对拖拽元素的一些操作,具体的代码逻辑如下所示:

javascript 复制代码
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中的对象来确定是否在拖拽中,如果拖拽的话直接隐藏原本元素就好了,代码如下:

javascript 复制代码
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用来提供对拖拽区域的一些操作,具体的代码逻辑如下所示:

javascript 复制代码
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来决定目标区域的行为或者重新计算是否可以接受拖拽等,如下所示:

javascript 复制代码
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来更新嵌套的对象或数组而不会直接修改原数据,这样可以避免直接变更数据确保数据的不可变性,终端执行如下命令安装:

javascript 复制代码
npm install immutability-helper

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

javascript 复制代码
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函数不会在每次组件渲染时被重新创建,这样避免了不必要的重新渲染和性能开销。如下所示:

javascript 复制代码
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元素,方便对其进行操作:

javascript 复制代码
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) return
      const dragIndex = item.index // 拖拽元素的索引位置
      const hoverIndex = index // 鼠标悬停元素的索引位置
      if (dragIndex === hoverIndex) return
      const hoverBoundingRect = ref.current?.getBoundingClientRect() // 获取鼠标悬停元素的边界信息
      const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 // 鼠标悬停元素的中点位置
      const clientOffset: any = monitor.getClientOffset() // 获取鼠标在页面中的位置信息
      const hoverClientY = clientOffset.y - hoverBoundingRect.top
      if (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 : 1
  drag(drop(ref))
  return (
    <div ref={ref} style={{ ...style, opacity }} data-handler-id={handlerId}>{text}</div>
  )
}

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

拖拽移动操作

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

javascript 复制代码
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>
  )
}

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

javascript 复制代码
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应用中实现漂亮且易于使用的拖放交互,它提供了一个高效流畅且可访问的拖放体验,常用于实现类似列表排序卡片拖动等功能,终端执行如下命令安装:

javascript 复制代码
npm install react-beautiful-dnd --save

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

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

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

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

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

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

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

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

javascript 复制代码
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元素并处理拖动放置以及元素重排的过程,使得用户能够在界面中拖动元素动态地改变其位置或顺序,终端执行如下命令安装:

javascript 复制代码
npm install @dnd-kit/core

其主要优势如下所示:

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

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

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

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

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

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

javascript 复制代码
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>
    );
}
相关推荐
程序员Bears3 小时前
深入理解现代JavaScript:从ES6+语法到Fetch API
前端·javascript·python·es6
IoOozZzzz4 小时前
ES6-Set-Map对象小记
前端·javascript·es6
松树戈4 小时前
idea结合CopilotChat进行样式调整实践
前端·javascript·vue.js·copilot
change_fate6 小时前
AbortController 取消请求
javascript·http
Dovis(誓平步青云)6 小时前
【数据结构】励志大厂版·初阶(复习+刷题)排序
c语言·数据结构·经验分享·笔记·算法·排序算法·推荐算法
EQ-雪梨蛋花汤6 小时前
【方案分享】基于Three.js和Stencil Buffer的AR实物遮挡方案,支持不规则动态区域(AR地下设施、AR虚实遮挡)
javascript·ar·restful
咖啡の猫8 小时前
JavaScript基础-分支流程控制
开发语言·javascript
香蕉可乐荷包蛋9 小时前
Three.js在vue中的使用(二)-动画、材质
javascript·vue.js·材质
钢铁男儿15 小时前
Python基本语法(函数partial)
前端·javascript·python