当前位置: 首页 > 工具软件 > dnd kit > 使用案例 >

React DnD简明教程

佘飞鸣
2023-12-01

React DnD简明教程

概述

React Dnd不同于其他的拖拽库,如果你以前没有用过它可能会被吓到。然而,一旦你了解了它设计的一些核心概念,它将变得有意义。我建议你在阅读文档其他部分之前,先阅读这些核心概念。

这些核心概念和flux/redux架构相似。这并不是巧合,因为React DnD内部就是用的Redux。

Backends(后端)

React DnD是基于HTML5的拖放API搭建的。基本原因是这个API能够截取拖拽的dom节点作为一个拖拽预览。它的便利性就在于你不必为拖拽时的鼠标绘制样式。这个API也是处理文件拖拽上传的唯一方式。

不幸的是,HTML5拖拽API也有缺点。它不支持触摸屏,并且它在IE上提供的可定制机会要少于其他浏览器。

这就是为什么React DnD把HTML5的拖拽支持作为一种可插拔的方式来实现。你不必用它。你可以自己做一个不同的实现,基于触摸事件,鼠标事件,甚至完全不同的东西。一些可插拔的(拖拽接口)实现在React DnD被称作“backends”。React DnD目前仅提供了HTML5 backends,但是以后可能会添加更多的实现。

Backends承担的角色有点像React的事件合成系统:他们都抽象了浏览器的事件和DOM事件实现的区别。虽然有相似点,React DnD的backends却并不依赖React或事件合成系统。在底层,backends做的事情就是把DOM事件转换成React DnD可以处理的Redux actions。

Items and Types(项和类型)

像Flux(或Redux),React DnD用数据作为真实的处理源,而不是界面。当你在屏幕上拖动一些东西时,我们不说是一个组件或者一个DOM节点被拖动了。事实上,我们说是某个特定的type(类型)的item(项)被拖动了。

什么是item?Item就是一个纯js对象,用以描述什么被拖动了。举个例子,在看板应用中,当你拖动一个卡片,item看起来可能像{cardId:42}。在一个象棋游戏中,当你捡起一个棋子,item看起来可能像{fromCell: 'C5', piece: 'queen'}。用纯对象描述拖拽数据,可以帮助你解耦组件和使组件间互无干扰。

什么是type?type是一个字符串(或一个symbol)在应用中唯一标识items的完整类型。在看板应用中,可能有一个“card”类型代表可拖动的卡片,同时有一个“list”类型用以表示由这些卡片组成的可拖动的列表。在象棋应用中,可能只有一个“piece”类型。

Types(类型)是有用的,随着你应用的增长,你可能想让更多东西可拖动,但是你不一定想放置目标突然接受新的items(项)。Types能让你指定哪些拖拽源和放置目标可兼容。你可能会枚举出所有的type常量,就像枚举Redux的action types一样。

Monitors(监控器)

拖拽本身是有状态的。要么拖动操作正在进行,或者没有。要么有一个类型和一个项,或者没有。状态一定存在于某处。

React DnD通过一些被称作monitors的包装了内部状态存储的封装器把这些状态暴露给组件。这些封装器让你更新组件props来响应拖拽状态的改变。

每个组件都需要跟踪拖放状态,你可以定义一个收集函数来获取从监控器返回的一些细节。React DnD负责及时调用收集函数将返回结果合并到你的组件props中。

比如说你想在某个棋子被拖动后高亮棋盘格子。为Cell组件定义的收集函数可能像这样:

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

这个函数会指示React DnD传递最新的highlighted和hovered的值给所有的Cell组件实例的属性。

Connectors (连接器)

如果backend掌控的是DOM事件,而组件用的是React(虚拟DOM)描述DOM,那么backend怎么知道哪些DOM节点应该被监听勒?通过输入connectors。连接器让你在render函数中分配一条预定规则(拖拽源,拖拽预览,放置目标)给DOM节点。

事实上,connector作为第一个参数被传递给我们上面提到的收集函数。我们来看看怎样用它指定一个放置目标:

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

在组件的render方法中,能同时获得从监控器放回的数据,和从连接器返回的函数。

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组件的根节点是一个有效的放置目标,并且backend应该掌控它的hover和drop事件。在内部它通过你指定的React元素的引用回调来工作。这个函数通过连接器memoized后返回的,所以它不会中断shouldComponentUpdate 的优化。

Drag Sources and Drop Targets(拖拽源和放置目标)

截止目前我们已经介绍了和DOM打交道的backends,代表项和类型的数据,以及收集函数,得益于监控器和连接器,你能描述React DnD应该将哪些props注入到组件中。

但是我们如何配置组件来实际接受这些注入的props呢?我们怎样执行具有副作用的拖放事件呢?是时候来会会drag sources(拖拽源)和drop targets(放置目标)了,这是React DnD主要的抽象单位。它们才是真正把类型、项、副作用操作,和收集函数连接到你组件中的东西。

无论何时你想让你的组件或者它的一部分可拖动,你都需要用拖拽源声明来包装这个组件。每个拖拽源都需要注册一个特定的类型,并且必须实现一个通过组件props生成项的方法。它也可以选择性的指定其他一些方法来处理拖放事件。拖放源声明还允许你为给定组件指定收集函数。

放置目标和拖拽源十分相似。唯一的区别在于一个放置目标可以同时注册几个项类型,而不是提供一个项,它能掌控自己的hover和drop事件。

Higher-Order Components and ES7 decorators (高阶组件和ES7修饰器)

你怎么包装你的组件?包装纠结是什么意思?如果你以前没有用过高阶组件,先读读这篇文章,它详细介绍了这个概念。

所谓高阶组件就是一个函数,获取一个React组件类并返回另一个不同的组件类。

The wrapping component provided by the library renders your component in its render method and forwards the props to it, but also adds some useful behavior.(这句目前不知道怎么翻译,只好原文引用了。)

在React DnD中,DragSource和DropTarget,以及其他的一些顶级的对外函数,实际上都是高阶组件。他们把拖放魔法带入到你的组件里。

一个关于使用它们的警告就是他们需要两个应用函数。举个例子,这里是如何用DragSource包装YourComponent:

import { DragSource } from 'react-dnd'; 
class YourComponent { /* ... */ } 
export default DragSource(/* ... */)(YourComponent);

注意,在指定DragSource的参数的时候发生了第一次调用,然后再最后传递你的组件类的地方,发生了第二次调用。这被称为函数柯里化,或者偏函数用法,并且有必要创造性的使用ES7的修饰符语法。

你不必使用这个语法,但是如果你喜欢,你就能用Babel转换你的代码,并且在.babelrc文件里设置{ "stage": 1 }

即使你不打算用ES7,偏函数用法也是依然有益的,因为它可以帮你在ES5或ES6中用有合并功能的辅助工具如 _.flow,合并几个DragSource 和 DropTarget 声明。在ES7中,你只需堆叠使用这些修饰符就能达到同样效果。

import { DragSource } from 'react-dnd';

@DragSource(/* ... */)
@DropTarget(/* ... */)
export default class YourComponent {
  render() {
    const { connectDragSource, connectDropTarget } = this.props
    return connectDragSource(connectDropTarget(
      /* ... */
    ))
  }
}

Putting It All Together (总结)

下面是一个包装现有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);
  }
};

// Use the decorator syntax
@DragSource(Types.CARD, cardSource, (connect, monitor) => ({
  // 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()
}))
export default class Card {
  render() {
    // Your component receives its own props as usual
    const { id } = this.props;

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

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

现在你已经掌握了足够的信息去探索文档余下的部分了。

这个实例将是一个不错的开始。

参考链接:https://liunianmou.gitbooks.io/react-dnd/content/chapter1.html

 类似资料: