React-Draggable:快速创建你的可拖动组件

4,820 阅读5分钟

React-Draggable 是一个 React 生态里用于创建可拖动元素的工具库,其使用及其简单,本文就来介绍。

创建项目

使用 Vite 的 React 模板创建项目:

npm create vite@latest react-draggable-demo -- --template=react
cd react-draggable-demo
# 使用 VS Code 打开
code .

安装依赖,并启动项目:

npm install
npm run dev

访问 http://localhost:5173/:

安装 React-Draggable

npm install react-draggable

删除 src/index.css 中的内容,修改 src/App.jsx:

import Draggable from 'react-draggable'

function App() {
  return (
    <>
      <Draggable>
        <div style={{ display: 'inline-flex', padding: '10px', border: '1px solid black', cursor: 'move' }}>I can now be moved around!</div>
      </Draggable>
    </>
  )
}

export default App

效果如下:

就这样,我们就非常快速的创建了一个可拖动元素。

作用原理

React-Draggable 的拖动实现原理很简单,就是通过给子元素添加 transform: translate(0px, 0px) 实现的。

当然,还添加了一个默认的类名 .react-draggable,目前只是个标注,并没有设置样式,并无作用,忽略即可。

当我们执行拖动时,React-Draggable 会根据当前的拖动调整子元素的 transform 值,从而实现元素在横向、纵向上的移动。

根据 transform 移动元素有一个好处——不过你的子元素定位形式如何(相对、绝对说是默认的静态)都能正确移动。

这样呢,就会带来一个问题:如果拖动元素本身已经应用了 CSS transform,那么就会被 <Draggable> 覆盖。比如下面这样:

<Draggable>
  <div style={{ display: 'inline-flex', padding: '10px', border: '1px solid black', cursor: 'move', transform: 'translate(50px, 50px)' }}>I can now be moved around!</div>
</Draggable>

然而展示的效果是下面这样:

transform: 'translate(50px, 50px)' 被覆盖成 transform: 'translate(0px, 0px)'。

为了避免覆盖,这个时候就要借助一个中间元素了,类似下面这样:

<Draggable>
  <span>
    ...
  </span>
</Draggable>

渲染效果如下:

发现拖放元素进行了正确的偏移。

常用功能

以上只是简单介绍了 <Draggable> 的使用,其组件上还有很多 props 用于设置其他拖放相关的表现。下面就来介绍比较常用的功能。

axis:指定为单轴拖动

<Draggable> 创建出来的拖动元素默认可以按照 X 或 Y 轴上移动。如果你只希望元素只能按照某个方向上拖动,那么可以借助 axis prop。

axis prop 有 2 个可以设置的值:xy

当设置成 y 时,那么只能在垂直方向上拖动。

- <Draggable>
+ <Draggable axis='y'>

效果:

当设置成 x 时,那么只能在水平方向上拖动。

- <Draggable>
+ <Draggable axis='x'>

效果:

handle:指定拖动源

<Draggable> 包裹的元素默认情况下,整个都是一个拖动源,但某些情况下,我们只希望为包裹元素的局部元素开发拖放能力——大家可以想一想浏览器顶部 Bar。

这一点 <Draggable> 也能帮我们做到——通过 handle prop。

<Draggable handle=".handle">
  <div style={{ display: 'inline-flex', flexDirection: 'column' }}>
    <div className="handle" style={{ cursor: 'move', padding: '10px', border: '1px solid' }}>Drag from here</div>
    <div style={{ padding: '20px', border: '1px solid', marginTop: '-1px' }}>This readme is really dragging on...</div>
  </div>
</Draggable>

handle prop 的值是一个 CSS selector 字符串,指定内部的某个元素作为拖放源。以上面代码为例,我们设定内部的 <div className="handle"> 作为我们的拖放源。最终效果如下:

会发现拖动内容区无效,只有头部元素是可拖放的。当然了,最终的拖放 transform 变动还是应用在最外层的 div 元素的(而非 .handle 元素)。

bounds: 限定拖放边界

<Draggable> 还有 1 个 bounds prop,可以指定可拖放的范围。

可以将 bounds 设定 "parent"

<div style={{ position: 'relative', height: '50vh', width: '50vw', border: '1px solid' }}>
<Draggable
  handle=".handle"
  bounds="parent"
>...</Draggable>
</div>

"parent" 表示只能在父元素范围内移动。

注意,父元素上的 position: 'relative' 必须要设定,否则会被外部 body 默认的 8px 外边距影响,产生偏差。

当然,bounds 还可以设置成是一个 CSS Selector 字符串

<div className='drag-area' style={{ position: 'fixed', inset: 0, border: '2px solid red' }}></div>
<Draggable
  handle=".handle"
  bounds=".drag-area"
>
  {/* ... */}
</Draggable>

以上,我们将 <Draggable> 可以拖动的区域限定在 .drag-area 元素范围内,这个元素的范围正好覆盖整个视口区域内。

这样我们将能讲拖放元素限定在整个能看到的范围内。

注册拖放事件

<Draggable> 有 3 个可供注册的拖放事件,分别是 onStart、onDrag 和 onStop。

const handleStart = (event, data) => {
  console.log('Drag started', event, data)
}

const handleDrag = (event, data) => {
  console.log('Dragging', data)
}

const handleStop = (event, data) => {
  console.log('Drag stopped', event, data)
}

<Draggable
  handle=".handle"
  onStart={handleStart}
  onDrag={handleDrag}
  onStop={handleStop}
>
{/* ... */}
</Draggable>

我们来看操作效果:

onStart 在点击时触发,onDrag 在拖动时触发,onStop 在松开鼠标时触发。

事件处理函数中,第一个 MouseEvent 参数很少用,主要还是第二个 data 参数比较有用。

type DraggableEventHandler = (e: Event, data: DraggableData) => void | false;
type DraggableData = {
  node: HTMLElement,
  // lastX + deltaX === x
  x: number, y: number,
  deltaX: number, deltaY: number,
  lastX: number, lastY: number
};

类似下面这样:

里面包含当前元素的迁移量 x, y。

这个 x, y 就可以作为下次的初始迁移量使用,初始迁移量可以通过 defaultPosition prop 设定。

<Draggable
  defaultPosition={{x: 82, y: 15}}
>...</Draggable>

效果:

其他可供使用 Props API 可以参考官方文档

其他

不过在你使用 React-Draggable 的过程中,会在控制台看到一些告警信息。

这块 API 文档里也有说明:

// If running in React Strict mode, ReactDOM.findDOMNode() is deprecated.
// Unfortunately, in order for <Draggable> to work properly, we need raw access
// to the underlying DOM node. If you want to avoid the warning, pass a `nodeRef`
// as in this example:
//
// function MyComponent() {
//   const nodeRef = React.useRef(null);
//   return (
//     <Draggable nodeRef={nodeRef}>
//       <div ref={nodeRef}>Example Target</div>
//     </Draggable>
//   );
// }

我们按照说明,只要引入一个 nodeRef,从 <Draggable> 拿到,再给到内部 <div> 即可消除。

import { useRef } from 'react';
import Draggable from 'react-draggable'

function App() {
  const nodeRef = useRef(null);

  return (
    <>
      <div className='drag-area' style={{ position: 'fixed', inset: 0, border: '2px solid red' }}></div>
      <Draggable
        handle=".handle"
        bounds=".drag-area"
        nodeRef={nodeRef}
      >
        <div ref={nodeRef} style={{ display: 'inline-flex', flexDirection: 'column' }}>
          <div className="handle" style={{ cursor: 'move', padding: '10px', border: '1px solid' }}>Drag from here</div>
          <div style={{ padding: '20px', border: '1px solid', marginTop: '-1px' }}>This readme is really dragging on...</div>
        </div>
      </Draggable>
    </>
  )
}

export default App

再来看看控制台,就看不见告警信息了。

总结

本文介绍了 React-Draggable 拖放库的使用,介绍了其简单使用、作用机制以及常用功能。

希望本文对你的工作能有所帮助,感谢阅读,再见。