精读《结合 React 使用原生 Drag Drop API》

1 引言

拖拽是前端很是常见的交互操做,但显然拖拽是强 DOM 交互的,而 React 绕过了 DOM 这一层,那么基于 React 的拖拽方案就一定值得聊一聊。html

结合 How To Use The HTML Drag-And-Drop API In React 这篇文章,让咱们谈谈 React 拖拽这些事。前端

2 概述

原文说的比较简单,笔者先快速介绍其中重点部分。react

首先拖拽主要的 API 有 4 个:dragEnter dragLeave dragOver drop,分别对应拖入、拖出、正在当前元素范围内拖拽、完成拖入动做。git

基于这些 API,咱们能够利用 React 实现一个拖入区域:github

import React from "react";

const DragAndDrop = props => {
  const handleDragEnter = e => {
    e.preventDefault();
    e.stopPropagation();
  };
  const handleDragLeave = e => {
    e.preventDefault();
    e.stopPropagation();
  };
  const handleDragOver = e => {
    e.preventDefault();
    e.stopPropagation();
  };
  const handleDrop = e => {
    e.preventDefault();
    e.stopPropagation();
  };
  return (
    <div
      className={"drag-drop-zone"}
      onDrop={e => handleDrop(e)}
      onDragOver={e => handleDragOver(e)}
      onDragEnter={e => handleDragEnter(e)}
      onDragLeave={e => handleDragLeave(e)}
    >
      <p>Drag files here to upload</p>
    </div>
  );
};
export default DragAndDrop;

preventDefault 指的是阻止默认响应,这个响应多是跳转页面之类的,stopPropagation 是阻止冒泡,这样一样监听了事件的父元素就不会收到响应,咱们能够精准做用于嵌套的子元素。api

接下来是拖拽状态管理,提到了 useReducer,顺便复习一下用法:微信

...
const reducer = (state, action) => {
  switch (action.type) {
    case 'SET_DROP_DEPTH':
      return { ...state, dropDepth: action.dropDepth }
    case 'SET_IN_DROP_ZONE':
      return { ...state, inDropZone: action.inDropZone };
    case 'ADD_FILE_TO_LIST':
      return { ...state, fileList: state.fileList.concat(action.files) };
    default:
      return state;
  }
};
const [data, dispatch] = React.useReducer(
  reducer, { dropDepth: 0, inDropZone: false, fileList: [] }
)
...

最后一个关键点在于拖入后的处理,利用 dispatch 增长拖入文件、设置拖入状态便可:dom

const handleDrop = e => {
  ...
  let files = [...e.dataTransfer.files];

  if (files && files.length > 0) {
    const existingFiles = data.fileList.map(f => f.name)
    files = files.filter(f => !existingFiles.includes(f.name))

    dispatch({ type: 'ADD_FILE_TO_LIST', files });
    e.dataTransfer.clearData();
    dispatch({ type: 'SET_DROP_DEPTH', dropDepth: 0 });
    dispatch({ type: 'SET_IN_DROP_ZONE', inDropZone: false });
  }
};

e.dataTransfer.clearData 函数用于清除拖拽过程当中产生的临时变量,这些临时变量能够经过 e.dataTransfer.xxx = 的方式赋值,通常用于拖拽过程当中值的传递。函数

总结一下,利用 HTML5 的 API 将拖拽转化为状态,最终经过状态映射到 UI。spa

原文内容仍是比较简单的,笔者在精读部分再拓展一些更体系化的内容。

3 精读

现阶段拖拽主要分为两种,一种是 HTML5 原生规范的拖拽,这种方式在拖拽过程当中不会影响 DOM 结构。另外一种是彻底所见即所得的拖拽方式,拖拽过程当中 DOM 位置会随之变更,好处是能够当即反馈拖拽结果,固然缺点是华而不实,一旦用在生产环境,这种拖拽过程可能致使页面结构频繁跳动,反而看不清拖拽效果。

因为本文也采用了第一种拖拽方案,由于笔者再从新整理一遍本身的封装思路。

从使用角度反推,假设咱们拥有一个拖拽库,那一定要拥有两个 API:

import { DragContainer, DropContainer } from 'dnd'

const DragItem = (
  <DragContainer>
    {({ dragProps }) => (
      <div {...dragProps} />
    )}
  </DragContainer>
)

const DropItem = (
  <DropContainer>
    {({ dropProps }) => (
      <div {...dropProps} />
    )}
  </DropContainer>
)

DragContainer 包裹能够被拖拽的元素,DragContainer 包裹能够被拖入的元素,而至于 dragPropsdropProps 须要透传到子元素的 dom 节点,是为了利用 DOM API 控制拖拽效果,这也是拖拽惟一对 DOM 的要求,双方元素都须要有实体 DOM 承载。

而上面例子中给出 dragPropsdropProps 的方式属于 RenderProps,咱们能够将 children 看成函数执行以达到效果:

const DragContainer = ({ children, componentId }) => {
  const { dragProps } = useDnd(componentId)

  return children({
    dragProps
  })
}

const DropContainer = ({ children, componentId }) => {
  const { dropProps } = useDnd(componentId)

  return children({
    dropProps
  })
}

那么这里建立了一个自定义 Hook useDnd 接收 dragPropsdropProps,这个自定义 Hook 能够这么写:

const useDnd = ({ componentId }) => {
  const dragProps = {}
  const dropProps = {}

  return { dragProps, dropProps }
}

接下来,咱们就要分别实现 dragdrop 了。

drag 来讲,只要实现 onDragStartonDragEnd 便可:

const dragProps = {
  onDragStart: ev => {
    ev.stopPropagation()
    ev.dataTransfer.setData('componentId', componentId)
  },
  onDragEnd: ev => {
    // 作一些拖拽结束的清理工做
  }
}

stopPropagation 的做用在原文简介中已经介绍过了,setData 则是通知拖拽方,当前拖拽的组件 id 是什么,这是因为拖拽由 drag 发起而由 drop 响应,所以必须有个数据传输过程,而 dataTransfer 就最适合作这件事。

对于 drop 来讲,只要实现 onDragOveronDrop 便可:

const dropProps = {
  onDropOver: ev => {
    // 作一些样式处理,提示用户此时松手会将元素防止在何处
  },
  onDrop: ev => {
    ev.stopPropagation()
    const componentId = ev.dataTransfer.getData('componentId')
    // 经过 componentId 修改数据,经过 React Rerender 刷新 UI
  }
}

重点在 onDrop,它是实现拖拽效果的 “真正执行处”,最终经过修改 UI 的方式更新数据。

存在一种场景,一个容器既能够被拖动,也能够被拖入,这种状况通常这个组件是个容器,但这个容器能够被拖入到其余容器中,能够自由嵌套。

实现这种场景的方式就是将 DragContainerDropContainer 做用到一个组件上:

const Box = (
  <DragContainer>
    {({ dragProps }) => (
      <DropContainer>
        {({ dropProps }) => {
          <div {...dragProps} {...dropProps} />
        }}
      </DropContainer>
    )}
  </DragContainer>
)

之因此能嵌套,在于 HTML5 的 API 容许一个元素同时拥有 onDragStartonDrop 这两种属性,而上面的语法不过是同时将这两种属性传给组件 DOM。

因此,动手实现一个拖拽库就是这么简单,只要活用 HTML5 的拖拽 API,结合 React 一些特殊语法便够了。

4 总结

最后留下一个思考题,许多具备拖拽功能的系统都具有 “拖拽 placeholder” 的功能,即拖拽元素的过程当中,在其 “落点” 位置展现一条横线或竖线,引导出松手后元素位置落点,如图所示:

那么这条辅助线是经过什么方式实现的呢?欢迎在评论区留言!若是你有辅助线实现方案解析的文章,欢迎分享,也能够期待笔者将来专门写一篇 “拖拽 placeholder” 实现剖析的精读。

讨论地址是: 精读《手写 JSON Parser》 · Issue #233 · dt-fe/weekly

若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名( 创意共享 3.0 许可证
相关文章
相关标签/搜索