React 最佳实践:亲手码一个「可拖放列表」

1,319 阅读2分钟

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

前言

拖放 在实际业务中也是一个常见的功能,比如通过拖放列表元素进行排序、拖放元素在不同的容器中穿梭等。提到拖放,我们想到的是原生 DOM 事件的操作、元素移动时位置的计算以及动画等等,第一反应必然是找第三方拖放库 ‘帮助’ 我们完成需求咯。然而,第三方库虽然功能强大,但很多功能你未必需要,杀鸡焉用牛刀呢?

所以我们可以根据实际场景的复杂度来选择实现方式,第三方库的强大功能 or 自己实现最精炼的代码

自己动手(代码不过百)

实现一个列表拖放排序的功能,选中元素后,元素实时跟随鼠标移动,并且列表实时更新,如下图: dragdrop.gif

准备工作

在 React 中实现拖放的技术要点:

  • 熟悉鼠标事件:MouseDownMouseUpMouseMove
  • 如何触发拖放开始(MouseDown)和判断拖放结束(MouseUp
  • 如何实现拖放元素位置的移动,可以用两种方案
    • 原元素跟着鼠标移动
    • 创建一个新元素,作为原元素的影子,跟随鼠标移动
  • 在组件中维护拖放的状态

拖放开始

在行元素上监听 Mousedown 事件,在 Mousedown 中初始化拖放状态:

  • dragging:true,拖放中
  • startPageY,记录鼠标开始位置(纵向)
  • draggingIndex,选中元素的 index
<li onMouseDown={(evt) => handleMounseDown(evt, i)} style={getDraggingStyle(i)}>
  {text}
</li>
const handleMounseDown = (evt, index) => {
  setDragging(true);
  setStartPageY(evt.pageY);
  setDraggingIndex(index);
};

样式上高亮拖放元素并且添加跟随鼠标效果,translateX 可以产生视觉上悬浮的效果,其中的 offsetPageY 的值将会在拖放过程中(MouseMove)计算得来:

const getDraggingStyle = (index) => {
  if (index !== draggingIndex) return {};
  return {
    backgroundColor: 'lightsteelblue',
    transform: `translate(10px, ${offsetPageY}px)`,
  };
};

拖动中

拖放中最关键的地方是:该在哪个节点上监听 MouseMove 事件?

用户拖动时,鼠标是可以全页面跑的,如果在拖动元素( <li/> )或者父元素( <ul/> )上监听事件都不合适。此时有一个方案:创建一个透明的全局的 fixed mask,既覆盖了鼠标的运动轨迹,又防止触发其他元素。

{
  dragging && (
    <div
      className="my-dnd-mask"
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
    />
  );
}
.my-dnd-mask {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0);
}

在 MouseMove 中,就是核心计算代码:

  • 计算偏移值(offset),let offset = evt.pageY - startPageY;
  • 当偏移值大于行高时,进行位置交换(move),并重新设置 DraggingIndexStartPageY
  • 当偏移值小于行高时,操作同上
  • 当偏移值没有超过行高,只需要设置 offsetPageY
const move = (arr, startIndex, toIndex) => {
  arr = arr.slice();
  arr.splice(toIndex, 0, arr.splice(startIndex, 1)[0]);
  return arr;
};
const handleMouseMove = (evt) => {
  let offset = evt.pageY - startPageY;
  if (offset > lineHeight && draggingIndex < list.length - 1) {
    // move down
    offset -= lineHeight;
    setList((pre) => move(pre, draggingIndex, draggingIndex + 1));
    setDraggingIndex((pre) => pre + 1);
    setStartPageY((pre) => pre + lineHeight);
  } else if (offset < -lineHeight && draggingIndex > 0) {
    // move up
    offset += lineHeight;
    setList((pre) => move(pre, draggingIndex, draggingIndex - 1));
    setDraggingIndex((pre) => pre - 1);
    setStartPageY((pre) => pre - lineHeight);
  }
  setOffsetPageY(offset);
};

拖放结束

设置 dragging: false,mask 层消失, startPageYdraggingIndex 初始化。

const handleMouseUp = () => {
  setDragging(false);
  setStartPageY(0);
  setDraggingIndex(-1);
};

完整代码

完整代码可以看 这里

拖放库

02.gif

找了一个在 React 最有名的拖放库,react-beautiful-dnd

安装

# yarn
yarn add react-beautiful-dnd

# npm
npm install react-beautiful-dnd --save

使用 react-beautiful-dnd

import { DragDropContext } from 'react-beautiful-dnd';

基础使用

需要我们管理的部分:

  • 展示的数据
  • 拖放结束后的动作(列表重排序)
  • 拖放时的变化(样式)

其余的事情,react-beautiful-dnd 会办好的。

Dom 结构

<DragDropContext onDragEnd={onDragEnd}>
  <center>
    <Droppable droppableId="droppable">
      {(provided, snapshot) => {
        return (
          <div ref={provided.innerRef}>
            {items.map((item, index) => (
              <Draggable key={item.id} draggableId={item.id} index={index}>
                {(provided, snapshot) => (
                  <div ref={provided.innerRef}>{item.content}</div>
                )}
              </Draggable>
            ))}
            {provided.placeholder}
          </div>
        );
      }}
    </Droppable>
  </center>
</DragDropContext>

拖放结束,重新排序

// 元素移动
const move = (arr, startIndex, toIndex) => {
  arr = arr.slice();
  arr.splice(toIndex, 0, arr.splice(startIndex, 1)[0]);
  return arr;
};

const onDragEnd = (result) => {
  if (!result.destination) {
    return;
  }
  setItems((pre) => move(pre, result.source.index, result.destination.index));
};

拖放中,样式管理

// 设置样式
const getItemStyle = (isDragging, draggableStyle) => ({
  userSelect: 'none',
  padding: grid * 2,
  margin: `0 0 ${grid}px 0`,
  // 拖拽的时候,item 背景变化
  background: isDragging ? 'lightgreen' : '#ffffff',
  ...draggableStyle,
});

const getListStyle = (isDraggingOver) => {
  return {
    // 拖拽的时候,list 背景变化
    background: isDraggingOver ? 'darkgreen' : 'gray',
    padding: 8,
    width: 250,
  };
};

<Droppable droppableId="droppable">
  {(provided, snapshot) => {
    return (
      <div style={getListStyle(snapshot.isDraggingOver)}>
        {items.map((item, index) => (
          <Draggable>
            {(provided, snapshot) => (
              <div
                style={getItemStyle(
                  snapshot.isDragging,
                  provided.draggableProps.style,
                )}
              ></div>
            )}
          </Draggable>
        ))}
      </div>
    );
  }}
</Droppable>;

效果预览

02.gif

完整代码

完整代码可以看 这里

进阶操作

详细的 API 介绍和进阶操作(容器间的拖拽 & 容器可滚动),看这边

React 最佳实践