『译』React DND(Drag and Drop for React)-概述

1,501 阅读8分钟

原文链接:React DnD官方文档

概述

    React DND 和大多数其他的拖拽组件不同,你刚开始使用的时候你可能会有些害怕。但是,一旦你了解了其设计核心的一些概念,它就会变得很好理解。我建议你在阅读该文档剩余部分之前先看看这些概念。 其中一些概念类似于Flux和Redux架构。这不是巧合,因为React DnD在内部使用Redux。

Backends(对原生拖拽事件的封装库)

    React DnD建立在 HTML5 drag and drop API之上。 因为它可以对已拖动的DOM节点进行屏幕快照,并将其用作“拖动预览”,这样你就不必在光标移动时进行任何绘制相关的操作,同时这个API也是处理文件删除事件的唯一方法。

    但是,HTML5拖放API也有一些缺点。 它在触摸屏上不起作用,并且与其他浏览器相比,它在IE上提供的定制化更少。

    这就是为什么在React DnD中以可插入方式实现HTML5 drag and drop API的原因。 你不必非得用它,你完全可以根据原生的触摸事件,鼠标事件等来封装自己的实现。这种可插拔的实现在React DnD中称为Backends。 该库现在只在HTML backend中使用,未来可能会有更多地方会用到。

    backends的作用类似于React的综合事件系统:它们都是把浏览器差异抽象出来并处理本地的DOM事件。不同的是,Backends并不依赖React或者React的综合事件系统。底层实现中,backends做的就是原生的Dom事件转换成React DnD能处理的内部 Reduxactions

Items and Types

    与Flux(或Redux)一样,React DnD操作的是数据,而不是视图。当你在屏幕上拖动某个对象时,我们并不是理解为正在拖动某个组件或DOM节点。相反,我们理解成一个有某种TypeItem正在被拖动。

   Item是什么呢?Item就是一个普通的js对象,描述了被拖动节点信息的对象。举个栗子,在一个看板应用中,你拖动一个卡片的时候,Item可能就长得像{cardId:42}。在象棋游戏中,当你拿起一个棋子的时候,Item可能就像是{fromCell:'C5',piece:'queen'}使用对象对拖动的数据描述可以保持组件间的解耦

   什么是type呢?type就是一个字符串(或者一个Symbol),它是某一类Item在项目中的唯一标志符。在看板应用中,你可能有一个叫'card'的type代表你拖动的卡片,可能有一个叫做'list'的type管理这些可拖动卡片的列表。在象棋应用中,你可能只有一个'piece'的type

   Types很有用,在你的项目越来越大的时候,你可能有很多组件是可拖拽,但是你并不想让所有现有的drop target突然对新的Item有反应。Types可以让你明确的知道哪些Drag SourcesDrop Targets是匹配的。你可能会在应用程序中枚举类型常量来保存所有的Type,就像Redux中保存actiontype一样。

Monitors

   拖放一定是有状态的。是否在进行拖动,当前是否正在有Item被操作这些状态都需要保存在一个地方。

   React DnD通过monitors(对内部状态存储的封装)将这些状态暴露给你的组件。monitors让你可以更新你组件的props来更改拖动的状态。

   对于每一个需要跟踪拖动状态的组件,你可以定义一个collecting function来从monitor中追踪和组件相关的状态。然后,React DnD会及时调用你的collecting function并将其返回值合并到组件的props中。

   比方说当你拖动一枚棋子的时候,你想高亮棋盘格。一个为Cell(棋盘格子)定做的collecting function大概就长这样子:

function collect(monitor) {
    return {
      highlighted: monitor.canDrop(),
      hovered: monitor.isOver()
    }
  }

   它表示让React DnDhighlightedhovered的最新值作为props传给所有的Cell(棋盘格子)实例

Connectors

   如果用backend来处理Dom事件,但是组件用React来描述Dom,backend要如何知道去监听哪个Dom节点呢?答案是ConnectorsConnector可以让你的render方法中注册一个预定义的角色(a drag source,a drop preview,a drop target)

   事实上,connector要作为我们上面提到的collect function中的第一个参数,让我们来看看要怎样使用它来明确一个drop target

function collect(connect, monitor) {
  return {
    highlighted: monitor.canDrop(),
    hovered: monitor.isOver(),
    connectDropTarget: connect.dropTarget()
  }
}

   在这个组件的render方法中,我们可以拿到从monitor中包含的数据和从connector中包含的方法了

render() {
  const { highlighted, hovered, connectDropTarget } = this.props;

  return connectDropTarget(
    <div className={classSet({
      'Cell': true,
      'Cell--highlighted': highlighted,
      'Cell--hovered': hovered
    })}>
      {this.props.children}
    </div>
  );
}

   connectDropTarget可以通知React DnD当前组件挂载Dom节点可以作为一个有效的drop target,并且它的hoverdrop事件会在backend中处理。具体实现是通过把一个callback ref(ref={element=>this.xxx=element})附加到你提供的React element上。connector中返回的方法是被缓存过的,所以不会中断shouldComponentUpdate的优化。

Drag Sources and Drop Targets

   到目前为止,我们已经介绍了用来解决Dom事件相关的backends、用来表示数据的ItemsTypes,用来描述哪些props需要被React DnD注入到你组件中的collecting function(monitors,connectors)

   但是我们如何配置我们组件的props?我们如何表现拖放事件的效果?接下来让我们来认识一下Drag SourcesDrop Targets-两个React DnD主要的抽象单元。它们两个把types,items,拖拽效果还有collecting functions和你的组件绑定在一起

   当你想让一个组件可以拖动,你需要将该组件用drag source declaration包装。每一个drag source都需要注册一个确定的type,需要实现一个从组件的props中生成item的方法,还可以选择指定一些方法来处理dragdrop事件。drag source declaration还能让被包装组件指定一个collecting function

   drop targetsdrag sources类似,唯一不同点就是drop target可能一次性注册多个itemtype,另外drop target可以处理它的hoverdrop而不是生成item

高阶组件和装饰器

   你要如何包装你的组件呢?包装到底是什么意思?如果你从来没有接触过高阶组件,去看看这篇文章(其实去React官网看看就好),它讲的很详细。 高阶组件就是一个函数,参数是一个React组件,返回值是另外一个组件库提供的包裹组件还在自己的render方法中渲染你的组件,传递props,同时可以添加一些有用的行为等。

   在React DnD中,DragSourceDropTarget,还有其他高级的暴露方法,实际上都是高阶组件。它们赐予了你的组件一些dragdrop的魔法。

   有一点需要注意的是,使用他们的时候需要调用两次。举例来说,下面是如果用DragSource来包裹你的组件:

import { DragSource } from 'react-dnd'

class YourComponent {
  /* ... */
}

export default DragSource(/* ... */)(YourComponent)

   注意,在指定DragSource的第一个参数并调用后,还有第二次函数调用,参数为你的组件。这叫做柯里化,或者叫做函数部分应用,这对修饰器的开箱即用是很有必要的:

import { DragSource } from 'react-dnd'

@DragSource(/* ... */)
export default class YourComponent {
  /* ... */
}

   你不是非得用这个语法,但是如果你喜欢用,你可以配置你的Babel,并将{"stage":1}添加进你的.babelrc file

   如果你不打算用修饰器,函数部分应用仍然可以派上用场。因为您可以使用_.flow 之类的函数组合助手在JavaScript中组合多个DragSourceDropTarget声明。 使用装饰器,您可以堆叠装饰器以达到相同的效果。使用装饰器可以达到同样的效果

import { DragSource, DropTarget } from 'react-dnd'
import flow from 'lodash/flow'

class YourComponent {
  render() {
    const { connectDragSource, connectDropTarget } = this.props
    return connectDragSource(
      connectDropTarget()
      /* ... */
    )
  }
}

export default flow(DragSource(/* ... */), DropTarget(/* ... */))(YourComponent)

变形金刚合体

下面就是一个封装Card组件的例子

import React from 'react'
import { DragSource } from 'react-dnd'

// Drag sources and drop targets only interact
// if they have the same string type.
// You want to keep types in a separate file with
// the rest of your app's constants.
const Types = {
  CARD: 'card'
}

/**
 * Specifies the drag source contract.
 * Only `beginDrag` function is required.
 */
const cardSource = {
  beginDrag(props) {
    // Return the data describing the dragged item
    const item = { id: props.id }
    return item
  },

  endDrag(props, monitor, component) {
    if (!monitor.didDrop()) {
      return
    }

    // When dropped on a compatible target, do something
    const item = monitor.getItem()
    const dropResult = monitor.getDropResult()
    CardActions.moveCardToList(item.id, dropResult.listId)
  }
}

/**
 * Specifies which props to inject into your component.
 */
function collect(connect, monitor) {
  return {
    // Call this function inside render()
    // to let React DnD handle the drag events:
    connectDragSource: connect.dragSource(),
    // You can ask the monitor about the current drag state:
    isDragging: monitor.isDragging()
  }
}

function Card(props) {
  // Your component receives its own props as usual
  const { id } = props

  // These two props are injected by React DnD,
  // as defined by your `collect` function above:
  const { isDragging, connectDragSource } = props

  return connectDragSource(
    <div>
      I am a draggable card number {id}
      {isDragging && ' (and I am being dragged now)'}
    </div>
  )
}

// Export the wrapped version
export default DragSource(Types.CARD, cardSource, collect)(Card)

现在你对React DnD学的差不多了,接下来你可以从tutorial开始